Java 锁
摘录自 不可不说的Java“锁”事

乐观锁 VS 悲观锁
乐观锁
-
概念:对于同一个数据的并发操作,乐观锁认为自己使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新的时候去判断之前的数据有没有被别的线程更新过,使用无锁编程实现,常采用 CAS+版本号 机制来判断。
-
适合场景:读操作比较多的场景,不加锁的特点使得读操作的性能大幅提升。
-
代码调用:
// synchronized public synchronized void test() { // 操作同步资源 } // ReentrantLock private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock(); }
悲观锁
-
概念:对于同一个数据的并发操作,悲观锁认为自己使用数据时一定有别的线程会修改数据,因此在获取数据时会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。
-
适合场景:写操作比较多的场景,先加锁可以保证写操作时数据正确。
-
代码调用:
// 保证多个线程使用的是同一个 AtomicInteger private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); -
CAS(Compare And Swap),是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程间的变量同步。
CAS 设计三个操作数:需要读写的内存值 V;进行比较的值 A;要写入的新值 B
当且仅当 V 的值等于 A 值时,CAS 通过原子方式(“比较+更新”整体是一个原子操作)用新值 B 来更新 V 的值,否则不执行任何步骤。一般,“更新”是一个不断重试的过程。
CAS 缺点:
- ABA 问题:CAS 需要在操作值的时候检测内存值是否发生变化,没变化才会更新内存值。但如果内存值先是 A,后来变成了 B,再变成 A,那么 CAS 检测发现值没有发生变化,但其实是发生了变化。可以在变量前添加版本号解决 ABA 问题
- 循环时间开销大:CAS 操作如果长时间不成功,会导致一直自旋,给 CPU 带来非常大的开销
- 只能保证一个共享变量的原子操作:对一个共享变量执行操作时,CAS 可以保证原子操作,但对多个共享变量操作时,无法保证操作的原子性

自选锁 VS 适应性自旋锁
阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器的时间。如果同步代码中的内容过于简单,状态转换消耗的时间可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁缺点:它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁指的是锁的状态,专门针对 synchronized 的。
概念补充:
- Java 对象头:synchronized 是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在 Java 对象头里面。以 Hotspot 虚拟机为例,Hotspot 对象头包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
- Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。
- Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
- Monitor:可以理解为一个同步工具或者一种同步机制。通常描述为一个对象。每个 Java 对象有一把看不见的锁,称为内部锁或者 Monitor 锁。
- Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时monitor中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
- synchronized 关键字通过 Monitor 来实现线程同步,Monitor 是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的线程同步。

无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
锁升级机制

公平锁 VS 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
可重入锁 VS 非可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
public class Widget {
public synchronized void doSomething() {
System.out.println("function 1 exec");
doOther();
}
public synchronized void doOther() {
System.out.println("function 2 exec");
}
}
在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
synchronized和lock的区别
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
- 性能上来说,在资源竞争不激烈的情形下,Lock性能稍微比synchronized差点(编译程序通常会尽可能的进行优化synchronized)。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
Java 中 synchronized 和 ReentrantLock 有什么异同
-
可重入性:
两者都是可重入锁
-
功能:
- 都是用来协调线程对共享对象、变量的访问
- 都保证了可见性和互斥性
-
锁的实现:
对于 Synchronized,它是 Java 语言的关键字,是原生语法层面的互斥,需要 JVM 实现。而 ReentrantLock 是 JDK 1.5 以后提供的 API 层面的互斥锁,需要 lock 和 unlock 方法配合 try/finally 语句块来完成。
-
性能的区别:
- 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。
- 在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
-
功能的区别:
- 便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
- 锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
-
ReentrantLock 独有的能力:
- 公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
- 提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
- 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。
- https://www.cnblogs.com/javastack/p/12787771.html
- 深入分析Synchronized原理
AQS
- AQS(AbstractQueuedSynchronizer) 是一个 Java 线程同步的框架,是 JDK 中很多锁工具的核心实现框架
- 在 AQS 中,维护一个信号量 volatile int state 和一个线程组成的 FIFO 双向链表队列。其中,这个线程队列用来给线程排队,state 用来控制线程排队或者放行。在不同场景有不同的用处
- 在可重入锁(ReentrantLock)场景下,state 用来表示加锁的次数。0 表示无锁,每加一次锁,state 加 1,释放锁 state 减 1,可重入锁就是多次进行 lock 操作。
独享锁 VS 共享锁
独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。