一、synchronized关键字
-
原理
当一个线程尝试访问同步代码块获同步方法时,只有得到同步代码块的锁之后才能正常进入代码块,并且在退出同步代码块或产生异常时需要释放锁。底层实现是JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
synchronized同步代码块在起始位置插入了moniterenter指令,在同步代码块结束的位置插入了monitorexit指令;而同步方法则是在普通方法的基础上加上了ACC_SYNCHRONIZED标志,方法执行时会先查看ACC_SYNCHRONIZED是否已设置,如果存在则需获取monitor之后才能继续执行方法体。
至于为什么说synchronized锁是重量级的,请看第二大点第五小点。 -
用法
a. 同步代码块:锁实例对象,锁类对象
b. 同步方法:修饰实例方法,修饰静态方法 -
synchronized锁
a. 修饰实例同步方法时,锁是当前实例对象
b. 修饰静态同步方法时,锁是当前类的Class对象
c. 对于同步方法块时,锁是Synchronized括号内匹配的对象 -
样例
public class SynchronizedTest {
//实例方法同步锁
public synchronized void demoOne(){
//doSomething
System.out.println("实例方法锁");
}
//静态方法同步锁
public static synchronized void demoTwo(){
//doSomething
System.out.println("静态方法锁");
}
//同步代码块之类锁
public void demoThree(){
synchronized (SynchronizedTest.class){
//doSomething
System.out.println("类锁");
}
}
//同步代码块之对象锁
public void demoFour(){
synchronized (this){
//doSomething
System.out.println("对象锁");
}
}
}
二、JDK1.6以后锁的优化
-
Java对象头
synchronized锁是存在Java对象头里的。Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
-
锁状态
无锁-》偏向锁-》轻量级锁-》重量级锁,随着线程竞争锁的状况升级,锁也将升级,但锁之能升级不能降级,这样是为了减少不必要的锁操纵。
-
偏向锁
在没有多个线程竞争锁的情况下,一个线程每次进入或退出同步块都需要加解锁,这是没有必要的。使用偏向锁,需要把对象头MarkWord字段偏向锁标识设为1;如果没有设置,则使用CAS进行锁竞争;如果设置了,就尝试使用CAS将对象头的偏向锁指向偏向的线程ID,以后此线程进出同步块将不用进行CAS操作,也就减少了不必要的CAS操作。
偏向锁撤销时会先暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否还活动着。如不活动了则将对象头改为无锁状态;如果还活着,那么遍历偏向锁记录之后要么重偏向,要么恢复到无锁,要么标记对象不适合作为偏向锁,然后唤醒线程。
Java偏向锁实在Java程序启动几s后再激活的,可以使用JVM参数-XX:BiasedLockingStartupDelay=0来关闭延迟。
如果不想使用偏向锁可以使用-XX:- UseBiasedLocking=false来关闭,则程序默认进入轻量锁状态。
-
轻量级锁
轻量级锁加锁时,线程在执行同步代码块前会先在当前的线程中的栈帧中创建用于存储锁记录的空间,然后复制对象头中的MarkWord字段到锁记录中。接着线程尝试使用CAS替换对象中的MarkWord字段未栈帧中锁记录的指针。成功的话当前线程才能拿到锁,否则代表由其他线程未释放此锁,解决方法就是自旋以等到锁释放时尝试获得此锁(自旋是耗费CPU的)。
轻量级锁解锁时,使用CAS操作将栈帧中锁记录替换回MarkWord。如果成功代表当前没有对象持有该锁,不存在锁竞争问题;如果失败代表锁存在竞争,当多个线程同时竞争锁时,会导致锁膨胀为重量级锁。
-
重量级锁
当多个线程竞争一个重量级状态锁时,只能由一个线程获得锁而其他想要获取此锁的线程将进入阻塞状态。当持有锁的线程释放锁之后会唤醒等待此锁的线程,被唤醒的线程会进行新一轮锁竞争.可见synchronized就是属于这一级别的锁。
-
锁的优缺点比对
图来源于:《Java并发编程的艺术》
三、下篇
参考书目:
- 《Java并发编程的艺术》