Java线程安全初识

Java线程安全初识

·

2 min read

什么是线程安全?

我的理解是由于程序使用多线程的方式运行,导致程序无法正确的得出我们期望的结果。

什么会导致线程安全问题,主要是可见性、原子性、有序性问题。详细可见:Java并发编程-可见性、原子性、有序性问题引入

线程安全的实现方式

可见性和有序性问题

主要是通过volatile、Happens-Before规则以及final关键字解决。详细可见Java并发编程-如何解决可见性和有序性问题

原子性问题

互斥锁

sychronized

死锁问题

产生死锁需要同时满足四个条件,即:

  1. 互斥:共享资源X和Y只能被一个线程占用
  2. 占有且等待:线程1已经取得共享资源x,在等待共享资源y时,不释放共享资源x
  3. 不可抢占:其他线程不可抢占线程1占有的资源
  4. 循环等待:线程1等待线程2占有的资源,线程2等待线程1占有的资源

解决死锁问题,就是解决上面四个条件的任一一个。

  1. 对于“占用且等待”条件,我们可一次性申请所有资源。
  2. 对于“不可抢占”条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可主动释放它占有的资源。
  3. 对于“循环等待”条件,按序申请,即资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请序号大的。

针对于“不可抢占”条件,sychronized申请不到资源时会进入阻塞状态,无法释放已占有的资源,我们应使用JUC提供的Lock解决

sychronized关键字的使用场景、作用范围

实现方法

利用Monitor,在使用sychronized关键字修饰的代码块,编译后自动生成相关加锁和解锁的代码,但仅支持一个条件变量,通过monitorenter和monitorexit实现。

修饰非静态方法

默认对当前实例对象this加锁

修饰静态方法

默认对当前类的Class对象加锁

管程Monitor

极客时间-Java并发编程实战-javaMESA管程模型

为什么局部变量是线程安全的?

调用栈和栈帧

CPU支持栈结构,这个栈与方法调用相关,被称为调用栈。 每个方法在调用栈中都有自己的独立空间,被称为栈帧,每个栈帧都有对应方法需要的参数和返回地址。

当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧会自动弹出,即栈帧和方法是同生共死的。 极客时间-Java并发编程实现-调用栈结构

局部变量存储位置

局部变量放到了调用栈里,如下图所示: 极客时间-Java并发编程实战-保护局部变量的调用栈结构

调用栈与线程

每个线程都有自己独立的调用栈 极客时间-Java并发编程实战-线程和调用栈的关系图

JUC中锁的分类和用途

公平锁、非公平锁

公平锁,多个线程按照申请锁的顺序来获取锁。会判断当前线程是否处于等待队列的头部,即链表的头部,如果是的话就直接获取锁。 非公平锁,没有顺序。不会判断当前线程处于等待队列的具体位置。CAS操作成功则认定为获取到锁。 synchronized是非公平锁,ReentrantLock可通过构造函数决定是公平锁还是非公平锁。

可重入锁

线程可重复获取同一把锁。 ReentrantLock在获取锁时,判断当前线程是否是之前已获取锁的线程,如果是,则直接返回true表示锁获取成功。

互斥锁(独享锁)、读写锁(共享锁)

互斥锁(独享锁)指锁一次只能被一个线程持有,读写锁(共享锁)指该锁可被多个线程持有。 synchronized和ReentrantLock都是互斥锁(独享锁),ReadWriteLock的读锁是共享锁,写锁是独占锁。

乐观锁、悲观锁

乐观锁在更新数据时会不断尝试更新,认为不加锁的并发操作是没问题的。基于CAS实现。 悲观锁认为对一个共享变量的并发操作,这个共享变量是一定会发生修改的,采取加锁方式。

乐观锁适合读操作远远大于写操作的情景,悲观锁适合写操作非常多的场景。

分段锁

对于ConcurrentHashMap来说,在put操作时,通过hashcode判断将要put的元素需要放到哪个分段,然后对分段进行加锁。当put操作不同的分段时,就可以实现并发操作。

无锁、偏向锁、轻量级锁、重量级锁

自1.6以后,java对synchronized进行了优化,当第一个线程获得了锁,锁状态变更新为偏向锁状态。

偏向锁

获取锁:当之前的线程再次获取锁时,无需再执行获取锁的过程。 锁撤销:原持有偏向锁的线程状态是非活动状态时,偏向锁撤销,锁状态更新为无锁状态。

轻量级锁

获取锁:如果每次申请锁的线程都是不相同的,则锁会升级为轻量级锁,指向栈中锁记录的指针。轻量级锁适用于线程交替执行同步块的场景。 释放锁:通过CAS操作,尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word,如果成功则完成解锁操作。如果失败则表明有其他线程获取该锁,此时锁膨胀为重量级锁。释放锁的同时,唤醒被挂起的线程。

重量级

当多个线程同时竞争锁,则轻量级锁会膨胀为重量级锁。指向互斥量的指针。未获取到锁的线程会阻塞。

自旋锁

循环检测锁标志位

可中断锁

Lock

// 支持中断的 API
void lockInterruptibly() 
  throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) 
  throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();

线程安全的集合

List

CopyOnWriteArrayList

在写操作的时候,会将共享变量复制一份出来,当写操作完成以后,再修改共享变量的内存引用地址。 不能使用迭代器删除数据,因为操作的是一个副本,不会修改到实际的共享变量。

Map

ConcurrentHashMap

  • KEY和VALUE不允许为空
  • KEY是无序的

    ConcurrentSkipListMap

  • KEY和VALUE不允许为空
  • KEY是有序的
  • 实现方案是使用SkipList(跳表)数据结构

    Set

    CopyOnWriteArraySet

    类似于CopyOnWriteArrayList

    ConcurrentSkipListSet

    类似于ConcurrentSkipListMap

    Queue

    单端阻塞队列

    ArrayBlockingQueue

    使用数组实现

    LinkedBlockingQueue

    使用链表实现

    SynchronousQueue

    不持有队列,入队操作必须要等到消费者线程的出队操作

    LinkedTransferQueue

    链表实现,入队操作必须要等到消费者线程的出队操作

    PriorityBlockingQueue

    支持按照优先级出队

    DelayQueue

    支持延时出队

    双端阻塞队列

    LinkedBlockingDeque

    单端非阻塞队列

    ConcurrentLinkedQueue

    双端非阻塞队列

    ConcurrentLinkedDeque

    原子类

    原子化的基本数据类型

    AtomicBoolean

    AtomicInteger

    AtomicLong

    原子化的对象引用类型

    AtomicReference

    AtomicStampedReference

    AtomicMarkableReference

    用锁的最佳实践

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁