确保线程间能互斥地访问同步块,即同一时间只有一个线程能进入同步块
多个线程有可能同时去访问同一个变量,我们称之为临界资源。
1.synchronized 加在方法上面,锁是加在当前类的对象上面,this。
2.synchronized 加在静态方法上面,锁加在当前方法所在类的上面Test.class.
3.synchronized 加在方法中的同步块,自己定义的object上面。
synchronized (object) {
count++;
}
显示锁的使用方法:
显示锁 - ReentrantLock lock = new ReentrantLock();
lock.lock();
count++;
finally{
lock.unock();
}
Synchronize底层原理:
用unsafe方法的如下代码可以代替synchronized并且可跨越方法使用:
汇编中对应:
以上对应java8大操作中的lock和unlock操作。
在synchronized小于java1.6版本的时候效率非常低,如下,他依赖于操作系统的互斥量Mutex,需要线程从用户态切换到内核态,线程状态切换开销非常大.
后来duogli开发了一套AQS,其中一个ReentrantLock效率比Synchronize高,虽然是用纯java开发的但是效率却比java原生的Synchronize更高。
为了挽回面子,Oracle收购Java之后,有对Synchronize进行了优化,加入了偏向锁,轻量级锁等概念,称为锁的膨胀升级过程。
在汇编中Synchronized所包含的代码块会被翻译成下面2个命令来说实现锁的:
对于锁的升级信息记录在object的Mark word里面的。
我们知道一个对象包含对象头,实例数据区,对其填充位。
Mark word就在对象头中,下图是对象内存结构:
Mark work的32位是如何记录锁的状态的?
实例观测锁的升级过程:
1. 使用jol-col工具来打印mark word.
org.openjdk.jol
jol-core
0.10
JVM, 会默认延迟启动偏向锁,默认延迟4秒。因为JVM内部的启动自己的线程会有部分激烈的竞争,为了提高效率避免从偏向锁,轻量级锁,重量级锁这样一步一步的升级效率低, 所以延迟启动偏向锁,让其自己从轻量级锁开始。
2. 所以此处关闭默认延迟:
-XX:BiasedLockingStartupDelay=0
下面代码中有4个步骤,实例展示锁是如何升级的:
Object o = new Object();
//Step 1
System.out.println(ClassLayout.parseInstance(o).toPrintable());
//Step 2
synchronized(o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
//Step 3
new Thread(()-> {
synchronized(o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
//Step 4
new Thread(()-> {
synchronized(o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
执行结果:
分析结果:
Step1 执行结果:没有任何线程在object加锁,object是匿名偏向锁。
如上图为mark work的区域,操作系统分为大端模式和小端模式,windows和linux用的是小端模式,所以前面是地位后面是高位。我们将其调换与对照表一样的格式为
小端模式 -> 00000101 00000000 00000000 00000000
转换为大端模式->00000000 00000000 00000000 00000101
最后结果为101,对照表格,101位偏向锁,为什么以上来还没有加任何锁的时候就是偏向锁呢 ?
-- 这里是匿名偏向,虽然还没有任何线程在该object上加锁,但是java默认给它加了一个匿名偏向,就相当于一个锁前的准备状态。
Step2 执行结果:有一个线程在object加锁,这时候object上记录了加锁的线程号。因为没有竞争所以这里加的是偏向锁。
小端模式 -> 00000101 11101000 01110100 00000010
转换为大端模式->00000010 01110100 11101000 00000101
这里的末尾3位虽然还是101,但是可以看出,线程ID 的位置不在是0了,这里记录了锁住object的线程Id。
Step 3执行结果:存在少少量的锁竞争,所以是轻量级锁
小端模式 -> 00100000 11110100 01100000 00011010
转换为大端模式->00011010 01100000 11110100 00100000
最后2位是00, 对照表格为偏向锁。因为当step3或step4执行的时候,发现有竞争,自己自旋等待加锁,发现在自旋结束前加锁成了,所以加上了偏向锁。
step 4 执行结果:偏向锁升级成了重量级锁
小端模式 -> 11011010 10001000 00001001 00000011
转换为大端模式->00000011 00001001 10001000 11011010
最后三位是010,对照表格是重量级锁,因为刚才的step3和step4同时执行存在竞争,竞争失败的那个线程进行了锁升级,于是变成了重量级锁。
Hashcode一开始没有打印出来,因为这里默认是懒打印的,所以这里没有体现。
对象的hashcode在偏向锁状态的情况下在上图中没有标记出来,在哪里记录的呢 ?
-- 在偏向锁的状态下调用hashcode会触发锁升级,升级成轻量级锁,因为按照表格里面没有记录hashcode的地方。
可以把它理解成一个控制同步的工具,他就是一个对象用C语言定义的一个对象。在object升级成重量级锁的时候,其高位记录的就是Monitor对象的地址。在c语言中,ObjectMonitor对象中包含很多属性,介绍下面几个重要的属性,
_WaitSet -- 处于wait状态的线程会被加到这个队列中,等待Notify。
_EntryList -- 处于等待锁被block的线程会被加到这个队列中,等待锁结束。
_Owner -- 指向持有ObjectMonitor对象的线程,即,在重量级锁中进入到同步块的那个线程。
每个java对象被创建时都会为其创建一个monitor对象,java对象的对象头Mark word中有指向该monitor对象的指针,Synchronized就是使用这种方式来获取锁的,这就是为什么任何一个java object都可以作锁的原因。同时notify、notifyall,wait也需要使用到nomitor对象,如上面介绍的_WaitSet。所以为什么这些命令必须要在Snchronized块中。
关闭偏向锁:-XX:-UseBiasedLocking, JDK 1.6 之后并默认开启偏向锁