- 简介
在Java SE 1.6之前,Synchronized被称为重量级锁.在SE 1.6之后进行了各种优化,就出现了偏向锁,轻量锁,目的是为了减少获得锁和释放锁带来的性能消耗.
- Synchroized的使用(三种形式) (1) 对于普通同步方法,锁是当前实例对象.如下代码示例: 解释:对于set和get方法来说,都是在方法上使用了同步关键字,所以他们是同步方法,锁的就是当前的实例对象,怎么理解了,看下面的main方法,就是这个new的实例对象.所以他们的锁对象都是synchronizedMethod 这个实例.
private int i = 0;
public synchronized void setNum (int number) {
this.i = number;
}
public synchronized int getNum () {
return i;
}
public static void main (String[] args) {
// 启动两个线程调用get和set方法
SynchronizedMethod synchronizedMethod = new SynchronizedMethod();
new Thread(() -> {
synchronizedMethod.setNum(5);
},"set").start();
new Thread(() -> {
int num = synchronizedMethod.getNum();
System.out.println(num);
},"get").start();
}
(2) 对于静态同步方法,锁是当前类的Class对象.看代码示例: 解释:如下两个方法都是静态同步方法.所以锁是当前类的class对象,这么理解吧,静态方法是类调用的,所以锁就是这个类对象.如下代码运行结果,只有当类的第一个静态同步方法执行完毕,第二个才能执行.
/**
* synchronized 静态方法
*/
public class SynchroizedStaticMethod {
private static int i = 0;
public static synchronized void addNum () {
for (;;) {
i++;
System.out.println(Thread.currentThread().getName()+"----"+i);
if(i >= 100){
break;
}
}
}
public static synchronized void getNum () {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
public static void main (String[] args) {
new Thread(() -> {
SynchroizedStaticMethod.addNum();
},"addNum").start();
new Thread(() -> {
SynchroizedStaticMethod.getNum();
},"getNum").start();
}
}
一部分执行结果 addNum----92 addNum----93 addNum----94 addNum----95 addNum----96 addNum----97 addNum----98 addNum----99 addNum----100 getNum----100
Process finished with exit code 0 (3) 对于同步代码块,锁就是Synchronized括号里面配置的对象.如下代码实例: 解释:通过如下代码可以证明锁就是括号里面的对象,当两个方法是一个对象时,只能是获取到对象锁的方法 执行,但是是两个锁对象时,那么两个方法获取的就是不同的锁对象,所以结果不一样了.
/**
* 代码块
*/
public class SynchroizedCodeBlock {
private Object object = new Object();
public void printOne () {
synchronized (object) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + 1);
}
}
}
public void printTwo () {
synchronized (object) {
System.out.println(Thread.currentThread().getName()+"---"+2);
}
}
public static void main (String[] args) {
SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock();
new Thread(() -> {
codeBlock.printOne();
},"printOne").start();
new Thread(() -> {
codeBlock.printTwo();
},"printTwo").start();
}
}
执行结果 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printTwo---2
Process finished with exit code 0 改变下括号里面的对象
public void printTwo () {
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"---"+2);
}
}
执行结果(与第一次不一样了) printTwo---2 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1 printOne---1
Process finished with exit code 0 3.锁在什么地方(Java 对象头)
Synchronized用的锁是存在Java的对象头里的.如果对象时数组类型,则虚拟机用3个字宽存储对象头..Java对象头里的Mark Word里默认储存对象的HashCode.分代年龄和锁标记位
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashcode或锁信息等 |
32/64bit | Class Metadata Address | 存储对象数据类型的指针 |
32/64bit | Array length | 数组的长度(如果当前对象时数组) |
Mark Word 的状态变化表
4.JSE1.6对锁的优化(锁的升级与对比)
在Java SE1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
(1)偏向锁
why:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁.
what:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里储存偏向的线程ID,以后该线程在进入和退出同步代码块时不需要进行cas操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否储存着指向当前线程的偏向锁。如果测试成功,表示该线程获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁的表示是否设置成1(表示当前是偏向锁):如果没有设置,则使用cas竞争锁;如果设置了,则尝试使用cas将对象头的偏向锁指向当前线程。
偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其它线程尝试竞争偏向锁时,持有线程才会释放锁。偏向锁的撤销,需要等待全局安全节点(在这个时间点上没有正在执行的字节码)。
偏向锁的升级:如果有线程来竞争偏向锁,那么就需要判断对象头的Mark Word的线程ID和当前线程ID是否一致,如果不一致说明发送了竞争,那么就需要检查拥有偏向锁的线程是否还存活;如果没有存活,那么将对象头设置为无锁状态,当前线程和其它线程都可以去竞争偏向锁;如果存活,暂停拥有偏向锁的线程,遍历栈帧信息,判断这个线程是否还要使用这个锁对象,如果还需要,就撤销偏向锁,升级为轻量锁,如果不要继续使用,标记为无锁状态,重新偏向其它线程。如果升级为轻量锁后,应该还是拥有锁的线程先去执行。
(2) 轻量锁
why:轻量锁是为线程竞争不是很多,每个线程的执行时间不长而准备的,因为轻量锁发生竞争时,不阻塞线程,而是采用的自旋;如果竞争时就阻塞线程,而锁很快就释放了,这个线程的状态切换也是很大的消耗。
waht:线程在执行同步代码块前,jvm会先在当前线程的栈帧中创建一个用于存储锁记录的空间,并将对象头中的Mark Word替换为为指向锁记录的指针。如果成功,当前线程获取锁,如果失败,表示其它线程竞争锁,当前线程尝试使用自旋来获取锁。其实就在当前线程里面存了一份拷贝的对象头的Mark Word(官方叫displaced Mark Word),然后把对象头里面Mark Word指针指向了当前线程在一开始创建的一块锁记录空间,displaced Mark Word 是在释放锁时恢复无锁用的。这一块其实有些绕,就是怎么判断锁这一块具体参考这篇文档
轻量锁的解锁:轻量级解锁时,会使用cas操作将disolaced Mark Word替换回到对象头,如果成功,则表示没有发生竞争。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。过程如下图所示:
(3) 锁的优缺点对比
| 锁 | 优点 | 缺点 | 使用场景 | | -------- | -------- | -------- | | 偏向锁 | 加锁和解锁不需要额为的消耗,和执行非同步方法相比,仅存在纳秒级别的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 |适用于只有一个线程访问的同步块场景 | | 轻量锁 | 竞争线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗cpu |追求响应时间,同步块执行速度非常快 | | 重量级锁 | 线程竞争不使用自旋,不消耗cpu | 线程阻塞,响应时间缓慢 |追求吞吐量,同步块执行速度较长 | 5.总结(有些个人理解)
偏向锁在Java 6 和 Java 7是默认开启的,但是它在应用程序几秒钟之后才激活,如果有必要可以使用jvm的参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。偏向锁是在单线程的时候使用的,我们知道单线程其实可以不用使用同步,我的理解就是为了预防线程安全问题,因为可能会出现多线程的情况,所以我们要预防以免带来线程安全的问题;从上面表格可以看出加上偏向锁性能在纳秒级别,完全可以接受;偏向锁设置是把Mark Word的是否为偏向锁的标识使用cas设置为1,然后使用设置偏向锁的里面的线程ID,设置成功,获取锁,也不用释放锁,下次这个线程再来获取锁,只需要判断偏向锁里面有没有自己的线程ID即可;但是如果有线程在Mark Word的是否为偏向锁的标识为1时,有线程来竞争锁,那么cas设置对象头的偏向锁指向自己线程ID时就会失败,因为有线程已经指向了,那么这个时候就不满足偏向锁了,这时就需要执行锁撤销的步骤,首先等待全局安全点,然后暂停拥有偏向锁的线程,判断当前拥有锁的线程是否还是存活状态,如果没有存活,将对象头设置无锁状态;如果还存活,判断线程是否还需要执行代码块,从栈帧中找到锁记录,如果当前线程还需要执行同步代码块,就证明需要锁,这个时候升级为轻量锁,然后唤醒暂停线程,竞争线程开始自旋;如果不需要执行同步代码块了,要么恢复到无锁或者标记对象不适合偏向锁(出现竞争了)。轻量锁,说下锁升级,轻量锁在释放锁时,会使用原子的cas把原来线程里面的displaced Mark Word替换为对象头,但是如果替换失败,表示锁竞争,锁就升级为重量级锁,这个过程为什么会失败了,是因为轻量锁的自旋是有次数的,如果达到一定的次数还没有成功,这个自旋的线程就会升级为重量锁,那么此时的对象头的Mark Word就会被改变,然后暂停该线程,当前面的轻量锁在释放锁时,会发现对象头的Mark Word被修改,然后升级为重量锁,唤醒之前暂停的线程.
选自《Java 并发编程的艺术》
和参考两位大佬的博客
锁的升级
Synchronized 整个锁的流程图(厉害)