synchronized
synchronized加到代码块上时两种情况
-
synchronized(this):表示加锁的效果如同加到普通方法上 synchronized(this){} = viod synchronized get(){} ;对象锁:不跨线程保护
-
synchronized(Test.class):表示加锁的效果如同加到静态方法上 synchronized(this){} = static viod synchronized get(){} ;类锁:跨线程保护
并发:资源共享且互斥
为什么synchronized能够起到阻塞的效果
synchronized(lock) 中被锁定的对象的内存布局
jdk1.6 以后 synchronized的功能进行了优化:对锁的升级规则(无锁->偏向锁(cas)-》轻量级锁(自旋)-》重量级锁(阻塞))
锁升级的规则
假如存在线程1、线程2 两个线程 :
-
只有线程1访问-- 》偏向锁 (cas 添加线程id)
-
线程1和线程2交替访问 --》轻量级锁-自旋(不是锁,只是轮询cas更换对象的MarkWord)
-
两者同时访问-》持锁时间较长(业务处理慢)-》重量级锁
无锁--》偏向锁
偏向锁--》轻量级锁
wait/notify
线程获取锁 通过monitorenter(指令)成功后获得对象锁,其他线程进入同步队列,wait的线程释放锁进入等待队列
可见性问题
CPU层面的高速缓存带来缓存不一致问题--》可见性问题:
cpu层面解决可见性问题 引入了:
总线锁;缓存锁
缓存一致性协议(x86:MESI):表示缓存行的四种状态(会出现指令重排序:乱序执行)
总结:cpu层面仍然会存在可见性问题(但是提供内存屏障指令)
读屏障、写屏障、全屏障
对象头mark word
我们可以将上面的注释转成以下的表格
|-----------------------------------------------------------------------------------------------------------------|
| Object Header(128bits) |
|-----------------------------------------------------------------------------------------------------------------|
| Mark Word(64bits) | Klass Word(64bits) | State |
|-----------------------------------------------------------------------------------------------------------------|
| unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object | Nomal |
|-----------------------------------------------------------------------------------------------------------------|
| thread:54| epoch:2 |unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object | Biased |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_lock_record:62 |lock:2 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| ptr_to_heavyweight_monitor:62 |lock:2 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
| |lock:2 | OOP to metadata object | Marked for GC |
|-----------------------------------------------------------------------------------------------------------------|
从上面的表格,我们可以看出Java的对象头在对象的不同的状态下会有不同的表现形式,主要有三种状态,无锁状态,加锁状态,GC标记状态。那么就可以理解Java当中的上锁其实可以理解给对象上锁。也就是改变对象头的状态,如果上锁成功则进入同步代码块。但是Java当中的锁又分为很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。这三种锁的效率是完全不同、关于效率的分析会在下文分析。我们需要查看对象头,就需要用到借助JOL工具。
首先我们在项目中引入JOL的依赖,具体如下图:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
然后创建A.java
public class A{} 然后创建JOLExample1.java
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;
public class JOLExample1 {
static A a;
public static void main(String[] args) {
a = new A();
//打印JVM的详细信息
out.println(VM.current().details());
//打印对应的对象头信息
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行的结果如下
不是说Klass是64bits(8个字节)但是这儿只有4个字节,是因为我们开启了指针压缩,我们可以关闭指针压缩看看,是不是8个字节。我们只需要使用以下的JVM运行参数
-XX:-UseCompressedOops
再次运行刚才的程序,可以看到我们Klass对象是64bits(16个字节),具体如下图
看完了对象的实例数据,我们就来到了今天的重头戏,Java的对象头(在开启JVM指针压缩的情况下是12B),那么这12B存储的是什么?我们可以看下 OpenJDK的官网的解释
首先引用openjdk文档当中对对象头的解释
上述引用中提到一个java对象头包含2个word,并且包含了堆对象的布局、类型、GC状态、同步状态和标识哈希码,具体怎么包含的呢?又是哪两个word呢?
Mark word为第一个word根据文档可以知道它里面包含了锁的信息、hashcode、gc信息等等,第二个word是什么呢?
klass word 为对象头的第二个word主要指向对象的元数据。
假设我们理解一个对象头主要由上图两个部分组成(数组对象除外,数组对象的对象还包含一个数组长度),由我们的推导出Mark word是8个字节,klass word(开启指针压缩的情况下是4个字节,不开启的时候是8个字节)。我们打印出来的对象头是12个字节,所以其中的8个字节是Mark word,剩下的4个字节是klass word,但是和锁相关的就是Mark word,那么接下来要重点分析Mark word里面信息。
由最开始的64位的表格,我们可以得知在无锁的情况下Markword当中前56bit存的是对象的hashcode,我们来验证一下
修改A.java 的代码如下
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample2 {
public class A {
//占一个字节的boolean字段
private boolean flag;
}
新建一个JOLExample2.java具体代码如下
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample2 {
public static void main(String[] args) {
A a = new A();
//没有计算HashCode之前的对象头
out.println("before hash");
out.println(ClassLayout.parseInstance(a).toPrintable());
//jvm计算HashCode
out.println("jvm----------" + Integer.toHexString(a.hashCode()));
//当计算完HashCode之后,我们可以查看对象头的信息变化
out.println("after hash");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
运行的结果如下:
可以看到我们在没有进行hashcode运算的时候,所有的值都是空的。当我们计算完了hashcode,对象头就是有了数据。因为是小端存储,所以你看的值是倒过来的。前25bit没有使用所以都是0,后面31bit存的hashcode,所以第一个字节中八位存储的分别就是分代年龄、偏向锁信息、对象状态,这8bit分别表示的信息如下图所示,这个图会随着对象的状态改变而改变,下图是无锁的状态下
无锁、偏向锁、轻量锁、重量锁、GC标记( 001,101,00,10,11)
关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记
锁状态 | 锁标识 | 备注 |
---|---|---|
无锁 | 001 | 对象头中使用baised_lock + lock 一共3bit来表示无锁和偏向锁的 |
偏向锁 | 101 | 对象头中使用baised_lock + lock 一共3bit来表示无锁和偏向锁的 |
轻量锁 | 00 | 只用到了lock标识位 |
重量锁 | 10 | 只用到了lock标识位 |
GC标志 | 11 | 只用到了lock标识位 |
新建一个JOLExample3.java,代码如下:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample3 {
static A a;
public static void main(String[] args) throws InterruptedException {
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
private static void sync() {
synchronized (a) {
out.println("我不知道要打印什么");
}
}
}
}
查看运行结果如下:
上面这个程序只有一个线程去调用sync方法,应该是偏向锁,但是你会发现输出的结果(第一个字节)依然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟,如果没有偏向锁的延迟的话,虚拟机在启动的时候,可能JVM某个线程调用你的线程,这样就有可能变成了轻量锁或者重量锁,所以要做偏向锁的延迟,那我们怎么看到打印的对象头是偏向锁呢?有两种方式:第一种是加锁之前先让线程睡几秒。第二种加上JVM的运行参数,关闭偏向锁的延迟,具体的命令如下:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
第一种方式:修改JOLExample3.java如下
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample3 {
static A a;
public static void main(String[] args) throws InterruptedException {
//切记延迟一定要放在对象创建之前,不然是无效的,因为在你对象创建之前,偏向锁的延迟的时间
//没有给你睡过去,这时候,对象已经创建了,对象头的信息已经生成了。
Thread.sleep(5000);
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
private static void sync() {
synchronized (a) {
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
再次运行,查看结果如下:
可以发现已经变成了00000101,偏向锁,需要注意的after lock,退出同步后依然保持了偏向信息。
第二种方式:利用jvm参数,首先我们先关闭睡眠5秒的,然后运行配置如下:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
再次运行查看结果如下:
这时候大家会有疑问了,为什么在没有加锁之前是偏向锁,准确的说,应该是叫可偏向的状态,因为它后面没有存线程的ID,当lock ing的时候,后面存储的就是线程的ID(44969989)既然这儿存储是线程的ID,那么HashCode又存储到什么地方去了?是不是计算了HashCode就是不能偏向了?我们来验证一下,计算完HashCode,还是不是偏向锁了
我们再次修改JOLExample3.java,具体代码如下:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample3 {
static A a;
public static void main(String[] args) throws InterruptedException {
//切记延迟一定要放在对象创建之前,不然是无效的,因为在你对象创建之前,偏向锁的延迟的时间
//没有给你睡过去,这时候,对象已经创建了,对象头的信息已经生成了。
//Thread.sleep(5000);
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
a.hashCode();
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
private static void sync() {
synchronized (a) {
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
同时关闭JVM中偏向锁的延迟,运行的结果如下:
我们可以发现:在before lock的时候是可偏向的状态,lock ing的时候变成了轻量锁,after lock 的时候变成了无锁,所以我们得出对象计算了HashCode,就不是偏向锁了。
看完了偏向锁的对象头,我们再来看看轻量锁的对象头,轻量级锁尝试在应用层面解决线程同步问题,而不触发操作系统的互斥操作,轻量级锁减少多线程进入互斥的几率,不能代替互斥。
创建JOLExample4.java,代码如下:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample4 {
static A a;
public static void main(String[] args) {
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
private static void sync() {
synchronized (a) {
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
运行结果如下:
可以得出:before lock 的时候是 00000001 无锁的状态,lock ing 的时候是 01010000 轻量锁的状态,after lock 的时候是 00000001 无锁的状态。
看完了轻量锁的对象头,我们再来看看重量锁的对象头,我们先创建一个JOLExample5.java具体代码如下:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample5 {
static A a;
public static void main(String[] args) throws InterruptedException {
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1 = new Thread(()->{
synchronized (a) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
out.println("t1 release");
}
});
t1.start();
Thread.sleep(1000);
out.println("t1 lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
System.gc();
out.println("after gc()");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
private static void sync() {
synchronized (a) {
out.println("main lock ing");
out.println(ClassLayout.parseIstance(a).toPrintable());
}
}}
运行结果如下:
在加锁之前(before lock)是 00000001 无锁,这时候t1来加锁,因为只有他一个线程所以轻量锁(t1 lock ing 00010000)由于t1在run方法中睡眠了5秒,这时候主线程也来尝试加锁,这个时候就是两个线程竞争了,所以是重量锁(main lock ing 00101010)
当结束的时候,还是重量锁(afteer lock 00101010),当执行一次gc操作过后发现变成了无锁但是年龄加了1(after gc() 00001001)
还有一点需要我们注意的就是:当调用wait方法会直接变成重量锁,我们来验证一下,创建JOLExample6.java,代码如下:
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample6 {
static A a;
public static void main(String[] args) throws Exception {
a = new A();
out.println("before lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1 = new Thread(() -> {
try {
synchronized (a) {
out.println("before wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
a.wait();
out.println("after wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(5000);
synchronized (a) {
a.notifyAll();
}
}
}
运行结果如下:
既然synchronized关键字有这三种锁,我们简单的比较它们之间的性能(粗略的比较下),书写以下的代码
public class A {
int i;
public synchronized void parse() {
i++;
}
}
//关闭偏向锁延迟‐XX:BiasedLockingStartupDelay=0
public class JOLExample7 {
public static void main(String[] args) throws Exception {
A a = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for (int i = 0; i < 1000000000L; i++) {
a.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
}
先运行加上jvm参数关闭偏向锁延迟,就是偏向锁,然后运行的结果如下:
我们在开启偏向锁延迟就是轻量锁,然后运行结果如下:
最后我们在看重量锁,具体代码如下:
public class A { int i; public synchronized void parse() { JOLExample8.countDownLatch.countDown(); i++; } } import java.util.concurrent.CountDownLatch; public class JOLExample8 { static CountDownLatch countDownLatch = new CountDownLatch(1000000000); public static void main(String[] args) throws Exception { final A a = new A(); long start = System.currentTimeMillis(); //调用同步方法1000000000L 来计算1000000000L的++,对比各种锁的性能 //如果不出意外,结果灰常明显 for (int i = 0; i < 2; i++) { new Thread(() -> { while (countDownLatch.getCount() > 0) { a.parse(); } }).start(); } countDownLatch.await(); long end = System.currentTimeMillis(); System.out.println(String.format("%sms", end - start)); } }
重量级锁的执行结果如下:
最后总结的结果如下:
偏向锁 | 轻量锁 | 重量锁 |
---|---|---|
2355ms | 23564ms | 31227ms |
偏向锁 轻量锁 重量锁 2355ms 23564ms 31227ms 最后我们再画个图总结下各种锁的对象头(只画出了最重要的部分,其他的省略)
不同情况下的锁升级策略
证明偏向锁
证明偏向锁 证明偏向锁之前,咱们按下图操作,给jvm添加查看全局配置的参数:
直接运行main方法,运行结果如下所示(由于篇幅问题,只截图了关键部分)
由图中的-XX:BiasedLockingStartupDelay=4000配置可知,jvm会在启动虚拟机之后的4s后才会开启偏向锁功能。知道这个概念后,咱们再来科普下什么是偏向锁。 所谓偏向锁:即当一把锁处于可偏向状态时,当有线程持有这把锁后,这把锁将偏向于这个线程。这里提到了可偏向状态,何为可偏向状态呢?可偏向状态是指在jvm开启可偏向功能后,new出来的一个对象它都是可偏向状态,即它的标识位为101,但是没有具体的偏向某一个线程。 证明可偏向状态和偏向锁: 添加如下代码并执行:
public class Valid { public static void main(String[] args) throws InterruptedException { // 这里要注意, 一定要在创建对象之前睡眠,若我们先创建对象,可以想一想会发生什么情况! // 那肯定是不会启动偏向锁的功能呀,我们都知道加锁其实是给对象加了个标识 // 如果我们在偏向锁功能未开启之前创建了对象,很抱歉, // jvm没有那么智能,后面不会去把这个对象改成可偏向状态(是偏向锁,但是没有偏向具体 // 的线程) Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
查看运行结果
证明一个对象调用了hashcode方法后无法再被标识为偏向锁,而是升级成轻量锁
编写如下代码(相对于上述代码,仅在加锁前调用了对象的hashcode方法):
public class Valid { public static void main(String[] args) throws InterruptedException { // 这里要注意, 一定要在创建对象之前睡眠,若我们先创建对象,可以想一想会发生什么情况! // 那肯定是不会启动偏向锁的功能呀,我们都知道加锁其实是给对象加了个标识 // 如果我们在偏向锁功能未开启之前创建了对象,很抱歉, // jvm没有那么智能,后面不会去把这个对象改成可偏向状态(是偏向锁,但是没有偏向具体 // 的线程) Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); System.out.println(user.hashCode()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
证明轻量锁
-
这里说下轻量锁的概念:若线程是交替执行的,即上一个线程执行完释放锁后下一个线程再获取锁。若在jvm未开启偏向锁的过程中,对对象进行加锁时,对象直接是轻量锁。
public class Valid { public static void main(String[] args) throws InterruptedException { System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
证明偏向锁膨胀为轻量锁
public class Valid { public static void main(String[] args) throws InterruptedException { // 开启偏向锁功能 Thread.sleep(4100); System.out.println(ByteOrder.nativeOrder().toString()); User user = new User(); System.out.println("before lock" + ClassLayout.parseInstance(user).toPrintable()); synchronized (user) { System.out.println("lock ing" + ClassLayout.parseInstance(user).toPrintable()); } System.out.println("after lock" + ClassLayout.parseInstance(user).toPrintable()); // 开启线程来获取锁 Thread t1 = new Thread(() -> { synchronized (user) { System.out.println("other t1 thread get lock" + ClassLayout.parseInstance(user).toPrintable()); } }, "t1"); t1.start(); // 等待t1执行完后再打印一次锁信息 t1.join(); System.out.println("after t1 thread release lock" + ClassLayout.parseInstance(user).toPrintable()); } }
证明重量锁
-
重量锁概念:多个线程存在激烈的竞争时,锁会膨胀成重量锁,且不可逆!
-
典型案例:生产者消费者模型:
public class ValidSynchronized { static Object lock = new Object(); static volatile LinkedList<String> queue = new LinkedList<>(); public static void main(String[] args) throws InterruptedException { System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); Consumer consumer = new Consumer(); Producer producer = new Producer(); consumer.start(); producer.start(); Thread.sleep(500); consumer.interrupt(); producer.interrupt(); // 睡眠3s ==> 目的是为了让锁自己释放,防止在释放过程中打印锁的状态出现重量锁的情况 Thread.sleep(3000); System.out.println("after lock"); System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } } class Producer extends Thread { @Override public void run() { while (!isInterrupted()) { synchronized (ValidSynchronized.lock) { System.out.println("lock ing"); System.out.println(ClassLayout.parseInstance(ValidSynchronized.lock).toPrintable()); String message = UUID.randomUUID().toString(); System.out.println("生产者生产消息:" + message); ValidSynchronized.queue.offer(message); try { // 生产者自己wait,目的是释放锁 ValidSynchronized.lock.notify(); ValidSynchronized.lock.wait(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { this.interrupt(); } } } } } class Consumer extends Thread { @Override public void run() { while (!isInterrupted()) { synchronized (ValidSynchronized.lock) { if (ValidSynchronized.queue.size() == 0) { try { ValidSynchronized.lock.wait(); ValidSynchronized.lock.notify(); } catch (InterruptedException e) { e.printStackTrace(); } } String message = ValidSynchronized.queue.pollLast(); System.out.println("消费者消费消息:" + message); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { this.interrupt(); } } } } }
证明调用wait方法后,锁会升级为重量锁
public class ValidWait { public static void main(String[] args) throws InterruptedException { Thread.sleep(4100); final User user = new User(); System.out.println("before lock"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); Thread t1 = new Thread(() -> { synchronized (user) { System.out.println("lock ing"); System.out.println("before wait"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); try { user.wait(); System.out.println("after wait"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } catch (InterruptedException e) { e.printStackTrace(); } } }, "t1"); t1.start(); // 主线程睡眠3s后,唤醒t1线程 Thread.sleep(3000); System.out.println("主线程查看锁,变成了重量锁"); System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }
总结
偏向锁和hashcode是互斥的,只能存在一个。 jvm默认对偏向锁功能是延迟加载的,大概时间为4s钟,可以添加JVM参数: -XX:BiasedLockingStartupDelay=0来设置延迟时间为0。偏向锁的延迟加载关闭后,基本上所有的锁都会为可偏向状态,即mark word为101,但是它还没有具体偏向的线程信息 偏向锁退出同步块后依然也是偏向锁 重量级锁之所以重量就是因为状态不停的切换,最终映射到代码层面就是不停的调用操作系统函数(最终会调用到jvm的mutex类) 调用锁对象的wait方法时,当前锁对象会立马升级为重量级锁 偏向锁只要被其他线程拿到了,此时偏向锁会膨胀。膨胀为轻量锁