Java中的锁.归类概览

1.乐观锁 与 悲观锁

2.自旋与适应性自旋

自旋:尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋原理:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗cup的,所以需要设定一个自旋等待的最大时间,即自旋锁时间阈值。

自旋锁时间阈值:JVM对于自旋周期的选择,JDK1.5这个周期限度是写死的,在JDK1.6引入了适应性自旋锁,适应性自旋锁其自旋周期不固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态共同决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化,因此存在一个自旋周期的确定过程。

3.公平锁与非公平锁

4.共享锁 与 独占锁

J.U.C. 提供的加锁模式分为共享锁和独占锁

共享锁允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

  1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
  2. java 的并发包中提供的 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问, 或者被一个写操作访问,但两者不能同时进行。

独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 和sychronized是以独占方式实现的互斥锁

独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

5.可重入锁

线程内的多个流程支持获取同一把锁。

同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

可重入锁最大的作用是避免死锁

6.多线程 锁的状态

多线程竞争锁的状态总共有四种:无锁状态、偏向锁、轻量级锁、重量级锁。

锁升级/锁膨胀:单向升级

Synchronized

独占式的悲观锁、可重入锁。

Java对象头

Java对象头由三部分组成:

  • Mark Word

    存储对象的hashCode或锁信息等

  • Class Metadata Address

    存储到对象类型数据的指针

  • Array length

    数组的长度(当对象为数组时)

Java对象头中Mark Word存储结构如图所示(以32位JVM虚拟机为例)。

6.1 无锁

不锁住资源,多线程只能有一个修改成功,其余线程会重试。

6.2 偏向锁

同一个线程执行不同流程同步资源时,自动获取资源。大多数情况,锁不仅不存在多线程竞争,而且总是由同一线程多次获取,因而由此优化。

当一个线程访问同步块并获取锁时,会在Java对象头和帧栈中的锁记录里面存储锁偏向的线程id,该线程再次访问同步块时不需要进行CAS操作来进行加锁、解锁,只需测试Java对象头中Mark Word里是否存储指向当前线程的偏向锁。

偏向锁获取过程:

  1. 访问Java对象头中Mark Word中偏向锁的标志位是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 如果为可偏向状态,则测试Mark Word中线程ID是否指向当前线程,如果是,进入步骤5获取锁执行同步代码块,否则进入步骤3继续尝试。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,则证明该偏向锁指向的线程存在,执行4。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,设置锁标志位为00(变为轻量级锁),偏向锁标识位为0,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
  5. 执行同步代码。
场景:线程2竞争线程1持有的偏向锁 导致锁升级过程
1、线程1持有偏向级锁;
2、线程2来竞争锁对象;
3、判断当前对象头是否是偏向锁;
4、判断拥有偏向锁的线程1是否还存在;
	线程1不存在,直接设置偏向锁标识为0(线程1执行完毕后,不会主动去释放偏向锁)。使用cas替换偏向锁线程ID为线程2,锁不升级,仍为偏向锁;
	线程1仍然存在,暂停线程1。设置锁标志位为00(变为轻量级锁),偏向锁为0;
5、从线程1的空闲monitor record中读取一条,放至线程1的当前monitor record中;
6、更新mark word,将mark word指向线程1中monitor record的指针;
7、继续执行线程1的代码;
8、锁升级为轻量级锁;
9、线程2自旋来获取锁对象;

偏向锁的特点:无竞争不锁,有竞争挂起,转为轻量锁。

偏向锁的撤销:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。

偏向锁的开启:Java6、7均默认开启,应用程序启动几秒后才激活,可配置JVM关闭延迟或直接关闭偏向锁。

6.3 轻量级锁

多线程竞争同步资源时,未获取资源的线程自旋等待锁释放。

轻量级锁适应的场景:线程通过自旋等待获取锁,实现线程交替执行同步块。

锁升级:如果在自旋一定次数后仍未获得锁,那么轻量级锁将会升级成重量级锁。

在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁。jdk1.6以后加入了自适应自旋锁Adapative Self Spinning),自旋的次数不再固定,由jvm自己控制,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

6.4 重量级锁

多线程竞争同步资源时,未获取资源的线程阻塞自旋等待被唤醒。

Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高。

JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。 JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入上文的“轻量级锁”和 “偏向锁”。

6.5 锁的对比

优点 缺点 适用场景
偏向锁 加锁与解锁无额外消耗 多线程锁竞争时存在偏向锁的撤销,产生额外消耗 只有一个线程会访问同步块
轻量级锁 多线程竞争通过CAS自旋实现,线程交替执行同步块,提高程序响应速度 始终获取不到锁的线程会持续自旋消耗CPU 追求响应时间,同步块执行速度块
重量级锁 多线程竞争不会自旋消耗CPU 线程阻塞,响应慢 追求吞吐量,同步块执行慢(周期长)

最后,内容推荐。