Java_锁

Java锁分类

  • 公平锁/非公平锁
  • 乐观锁/悲观锁
  • 排它锁/共享锁
  • 互斥锁/读写锁
  • 可重入锁
  • 偏向锁/轻量级锁/重量级锁

乐观锁/悲观锁

乐观锁:假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。

乐观锁在获取数据时认为别人不会修改,所以不上锁,只是读取当前的版本号,但是在更新的时候会判断版本号是否被更新,如果一样则更新,如果不同则重新获取数据。

在读操作密集的操作里,使用乐观锁会获得大量的性能提升。常常采用CAS(Compare-and-Swap)算法,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样更新,否则失败。

悲观锁:假定会发生并发冲突,通过加锁来规避一切可能违反数据完整性的操作。

java中的悲观锁就是 Synchronized, AQS 框架下的锁则是先常识 CAS 乐观锁去获取锁,获取失败,才会转为悲观锁,如 ReentrantLock

自旋锁

自旋锁原理是:当持有锁的线程能够很快的释放锁时,那么等待获取锁的线程通过采用循环的方式不断尝试去获取锁,这样的好处是减少线程上下文切换的消耗,缺点是线程持续工作,消耗CPU资源。

自旋最大等待时间:线程自旋是需要消耗 CPU 的,如果当线程超过一定时间,仍然无法获取到锁的时候,就让线程停止自旋,进入阻塞挂起状态。

自旋锁的优缺点

自旋锁的核心是,在对于线程上下文切换时间和锁占用时间的一个取舍

对于锁占用时间很短的代码而言,等待锁的释放,是更为高效的方案。

对于锁竞争激烈,或者持有锁的线程需要长时间来执行同步代码块的场景中,线程自旋的消耗大于线程切换带来的消耗,这种情况不适用于自旋锁。

自旋锁时间阈值

JDK1.6引入适应性自旋锁,自旋的时间是由上一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。

基本认为一个线程上下文切换的时间是最佳的。

JVM 还对 CPU 的负载情况进行了优化,当平均负载小于 CPU 核心数量时则一直自旋。

当 CPU 负载压力大,自旋引起其他线程阻塞时,则直接阻塞挂起线程。

独享(排他)锁/共享锁

独享锁:每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁,独占锁是一种悲观的加锁策略,避免了读/读冲突,但是也降低了一定的并发性。

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

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

互斥锁/读写锁

互斥锁:只允许一个线程持有的锁

读写锁:一个允许被多个读进程或一个写进程持有的锁ReadWriteLock

可重入锁(递归锁)

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

1
2
3
4
5
6
7
8
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}

synchronized void setB() throws Exception{
Thread.sleep(1000);
}

若非可重入锁,setB可能不会被当前线程执行,可能造成死锁。

公平锁与非公平锁

公平锁: 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁: 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

  1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
  2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

分段锁

分段锁是一种锁的设计,对于ConcurrentHashMap而言,其实现并发的原理就是分段锁。每一个分段(Segment)都是类似于HashMap的结构,即内部拥有一个Entry数组,数组每个元素都是链表。同时每段都加上了ReentrantLock。

​ 需要放入元素是,通过HashCode来判断所属的分段,然后对该分段加锁。

​ 在统计Size时,需要获取全部的分段锁才能统计。

偏向锁-> 轻量级锁 ->重量级锁

Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

重量级锁(Mutex Lock)

synchronized是通过对象内部的Monitor(监控器锁)来实现的,事实上监控器锁本身又依赖于底层操作系统提供的 mutex lock来实现的。当我们调用系统操作时,就会让线程从用户态切换到核心态,这个切换是相当耗时的。

因此,这种依赖于操作 Mutex lock 实现的锁,我们称之为重量级锁。

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