Java中划分锁的方式不同,就产生了各种对锁的定义,如果不能清楚知道不同锁的特点那么就很容易将JUC中的锁弄混淆,在学习JUC方面知识之前先学习下锁的划分。
对于悲观锁和乐观锁划分关键是:是否默认在进行多线程操作时,总认为有其他线程会进行同步操作。
悲观锁
:在进入同步方法的时候都会获取当前同步锁对象,直到退出同步方法时才会释放同步锁对象。如果有线程A和线程B,同他们都会访问obj对象的同步方法。当线程A获取到obj对象的同步锁,在执行某些方法的时候,这时候线程B企图获取 obj对象的同步锁就会失败,这个时候必须等待,直到线程A释放掉 obj对象的同步锁,线程B才能执行obj的同步方法
java中的悲观锁是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取,获取不到,才会转为悲观锁,如RetreenLock
乐观锁
:乐观锁是一种读多写少,遇到并发写的性能可能会变低,每次去拿数据的时候都认为不会修改,所以不会上锁。java乐观锁基本都通过CAS
操作实现的,cas是一种依赖系统指令集实现原子操作,比较当前变量值与预期值是否一样,如果相同就使用系统原语进行更新。
独占锁
其实就是悲观锁机制实现的锁、共享锁
就是乐观锁机制实现的锁。
public class Demo{
public synchronized void functionA(){
System.out.println("iAmFunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("iAmFunctionB");
}
}
可重入锁
:当线程拥有对象的同步方法锁后可访问其他同步方法 ,java中synchronized和ReentrantLock都是可重入锁
不可重入
:当线程拥有对象的同步方法锁后再访问其他同步方法也需要排队或等待
公平锁
是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
非公平锁
不能保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁
synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过构造函数进行设置
Synchronized
在Java中Synchronized
是我们常用的同步互斥手段
,它是一个悲观锁
设计下的独占锁
,并且可重入
。Synchronized在获取到同步锁后,其他线程将会阻塞,对于阻塞或者唤醒一个线程都需要操作系统来完成,这就需要从用户态切换到核心态,这样的操作就需要消耗很多处理器时间,具有很强的性能损耗
,因此Synchronized在1.6中做了很多的优化,减少频繁的切换到核心态。
在1.6之中为了提高Synchronized的性能,增加了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等来高效地共享数据,解决竞争问题
。
自旋锁
:在多线程操作中共享数据的锁定可能是和短暂的,避免线程在很短的时间内做用户态的切换,那么在允许多线程并行的基础上,让后面请求锁的线程“等待一下”,等待过程不放弃处理器的执行时间,为了让线程等待只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自适应自旋
是对自旋锁的优化,自旋锁的缺点就是如果锁占用的时间很长,那么就会浪费处理器资源,并且还对处理器有要求。加入自适应自旋就是自旋的时间不再固定,而是由前一个在同一个锁上的自旋时间以及锁的拥有者的状态决定,如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋很可能再次成功,应此可能允许等待时间更长;如果某一个锁上,很少自旋成功,那么将跳过自旋过程,避免资源浪费。虚拟机使用这一的策略和技术完成对Synchronized的一种优化
锁消除
是指在虚拟机及时编译在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除如:消除锁的判断主要来于逃逸分析的数据支持,如果判断在一段代码中,堆上的数据都不会逃逸出去从而被其他线程访问到,那么就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁就自热无效
public String concatString(String s1,String s2,String s3){
StringBuffer sb=new SrringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
对于上述代码,StringBuffer.append()方法都具有一个同步块,锁就是对象sb,但sb变量永远不会被其他线程所访问,其一直动态作用域被限制在concatString方法中,其他线程并不能访问。因此此处的锁可以被安全的消除掉。
锁粗化
一段代码里面对同一个对象进行反复的加锁和解锁,那么也会带来性能的损耗,应该将锁同步范围扩展(粗化)到整个操作序列的外部。如上面代码就是扩展到第一个append()操作之前直至追后一个append()操作之后,这样就只需要加一次锁
轻量级锁的作用是在没有多线程的前提下,减少重量级锁在使用操作系统互斥量产生的新能消耗
。在无竞争的情况下获取锁使用的是CAS
操作。CAS操作主要是用于更新对象头中thread ID, 要理解轻量级锁,得先了解HotPost虚拟机的对象头部分,对象头中一部分保存这GC年龄代,哈希码等信息,官方成为Mark Work,在32位的HotPost下,PostMark Work的32bit存储空间中,有25bit用于存储对象哈希码,4bit用于存储对象的分代年龄,2bit用于存储锁标志位,1bit固定为0;
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空、不需要记录信息 | 11 | GC标记 |
偏向锁ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
偏向锁
是在无竞争的情况下把整个同步都消除掉
,偏向锁的意思是同步锁会偏向第一次获得它的线程,如果在接下来的执行中,该锁没有其他线程获取,则持有偏向锁的线程将永远不需要再进行同步
。使用偏向锁需要虚拟机默认支持,如果在没有设置的情况下将不支持偏向锁。接下来用一张图讲解轻量级锁和偏向锁的转换关系
首先根据偏向锁可用
和不可用
分为左右两种逻辑
可偏向左边上面部分
:如果偏向锁可用,初始化的的对象标志位为01,那么这是一个未锁定、未偏向但是可偏向的对象
可偏向左边下面部分
:当第一次线程获取到锁的时候,那么虚拟机将头中的标志位设置为01
,同时使用CAS操作将线程的thread ID写入到Mark Work中
,成功后,持有偏向锁的线程再进入这个锁的相关同步块时,都不用做同步操作。
对象Mark Work中写入了thread ID状态下,对象可能处于锁定状态或未锁定状态。如果是在锁定
情况下,有另一个线程去获取这个锁时,偏向锁模式将结束,将回到轻量级锁定。
如果是在没有锁定
状态下,有另一个线程去获取这个锁时,偏向锁模式将结束,将回到未锁定、未偏向但是可偏向状态,如果撤销偏向,将回到未锁定、不可偏向对象
不可偏向右上部分
:如果偏向锁不可用,初始化的的对象标志位为01
,那么这是一个未锁定、不可偏向对象
不可偏向中间部分
:当有一个线程获取同步对象时,如果同步对象没有被锁定,那么在当前线程的栈帧中开辟一个叫“Lock Record”的空间保存一份同步对象的Mark Word部分的拷贝,并且使用CAS操作将这个对象的Mark Word更新指向栈帧的“Lock Record”
,如果成功,那么将标志位改为00
;如果更新失败,但当前线程已经拥有对象锁,那就直接运行同步块代码;否则说明有线程在争夺锁,那么轻量级锁就不再有效,要膨胀成重量级锁
不可偏向下面部分
:当同步对象已经是轻量级锁,但有另一个线程在竞争锁资源时,轻量级锁将膨胀成重量级锁,锁的标志位状态值变为“10”
,Mark Word中存储的指向重量级锁的指针,后面等待锁的线程也将进入阻塞状态。