Synchronized原理
锁区域
synchronized锁普通方法
锁的锁当前类的实例,即当前对象.
synchronized锁静态方法
静态方法中添加锁即锁定该类对应的Class对象,当多线程同时调用Class的静态方法时会进行同步.
synchronized锁代码块
可在代码块中指明锁定的对象,统称为对象锁.
锁实现原理
依赖的objectMonitor对象
源代码地址
(1)objectMonitor.hpp原文件地址(https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ef81b9152f1/src/share/vm/runtime/objectMonitor.hpp)
(2).objectMonitor.cpp原文件地址(https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ef81b9152f1/src/share/vm/runtime/objectMonitor.cpp)
(3).解析参考(https://www.cnblogs.com/webor2006/p/11442551.html)
monitor机制
monitor对象结构
可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。一般在对象头信息中存在指向该Monitor对象的指针.
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //_owner指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
monitor运行流程
_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
锁优化
锁优化后的种类
- 无锁状态
25Bit | 4Bit | 1Bit(是否是偏向锁) | 2Bit(锁标志位) |
---|---|---|---|
对象hashCode | 对象分代年龄 | 0 | 01 |
- 偏向锁状态
锁状态 | 线程ID | Epoch | 对象分代年龄 | 是否偏向锁 | 锁标志 |
---|---|---|---|---|---|
偏向锁 | 121 | ... | 2 | 1 | 01 |
- 轻量级锁
锁状态 | 指针 | 锁标记位 |
---|---|---|
轻量级锁 | xxxx | 00 |
- 重量级锁
30Bit | 2Bit |
---|---|
指向互斥量(重量级锁)的指针->指向Monitor对象 | 10 |
- 锁对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗, 和执行非同步代码方法的性能相差无几. | 如果线程间存在锁竞争, 会带来额外的锁撤销的消耗. | 适用于只有一个线程访问的同步场景 |
轻量级锁 | 竞争的线程不会阻塞, 提高了程序的响应速度 | 如果始终得不到锁竞争的线程, 使用自旋会消耗CPU | 追求响应时间, 同步快执行速度非常快 |
重量级锁 | 线程竞争不适用自旋, 不会消耗CPU | 线程堵塞, 响应时间缓慢 | 追求吞吐量, 同步快执行时间速度较长 |
锁优化的措施
- 自旋锁与自适应自旋锁
- 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步单在实际情况下不可能同步的代码进行锁消除.锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其它线程访问到,那就可以把它当作栈上的数据对待,认为它们是线程私有的,同步加锁自然就无需进行.
字符串链接代码:
public String contact(String s1,String s2) {
return s1 + s2;
}
由于String是不可变的类(final修饰类+private构造方法),对字符串的+总会生成新的String对象进行替换.在jdk1.5之前会进行相应的转化,如下:
public String contact(String s1,String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
转化后的代码添加了StringBuffer类,可以查看源码了解sb对象操作时会涉及到线程安全.但是该sb对象的作用域被限制在了contact()方法内部 ,永远不会逃逸的contact()方法外.因此虽然这里有涉及到锁的操作,在即时编译的时候可以将锁去除.
- 锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
- 偏向锁
23Bit | 2Bit | 4Bit | 1Bit | 2Bit |
---|---|---|---|---|
线程ID | epoch | 对象分代年龄 | 1 | 01 |
偏向锁是JDK1.6中引入的一项优化,它的目的是消除在无竞争情况下的同步原语,进一步提高运行性能.如果说轻量级锁是在无竞争的情况下使用CAS操作去消除使用的系统互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除了,连CAS操作都不做了.
(1).为什么会需要偏向锁
因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的.
(2).偏向锁的加锁过程
当一个线程访问同步块并获取锁时,会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程),如果测试成功,表示线程已经获得了锁; 如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁, 如果设置了,则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程.
(3).偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁.偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码).首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活,如果线程不处于活动状态,则将锁对象的对象头设置为无锁状态;如果线程仍然活着,则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程).
- 轻量级锁
30Bit | 2Bit |
---|---|
指向栈中锁记录的指针 | 00 |
一般重量级锁是通过系统互斥变量来实现的,因此避免不了系统空间转换(用户空间<->内核空间)带来的性能消耗.当出现有两个线程来竞争锁的话, 那么偏向锁就失效了, 此时锁就会膨胀, 升级为轻量级锁.
(1).加锁过程
线程在执行同步块之前, JVM会先在当前线程的栈帧中创建用户存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中.然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针. 如果成功,当前线程获得锁;如果失败,表示其它线程竞争锁,当前线程便尝试使用自旋来获取锁,之后再来的线程,发现是轻量级锁, 就开始进行自旋.当达到一定条件时(自旋超时),锁再次升级为重量级锁
(2).解锁过程
轻量级锁解锁时,会使用原子的CAS操作将当前线程的锁记录替换回到对象头,如果成功, 表示没有竞争发生;如果失败,表示当前锁存在竞争, 锁就会膨胀成重量级锁.
(3).流程解析
总结一下加锁解锁过程,有线程A和线程B来竞争对象c的锁(如:synchronized(c){}),这时线程A和线程B同时将对象c的MarkWord复制到自己的锁记录中,两者竞争去获取锁,假设线程A成功获取锁,并将对象c的对象头中的线程ID(MarkWord中)修改为指向自己的锁记录的指针, 这时线程B仍旧通过CAS去获取对象c的锁, 因为对象c的MarkWord中的内容已经被线程A改了, 所以获取失败.此时为了提高获取锁的效率, 线程B会循环去获取锁,这个循环是有次数限制的,如果在循环结束之前CAS操作成功, 那么线程B就获取到锁,如果循环结束依然获取不到锁,则获取锁失败,对象c的MarkWord中的记录会被修改为重量级锁,然后线程B就会被挂起,之后有线程C来获取锁时, 看到对象c的MarkWord中的是重量级锁的指, 说明竞争激烈,直接挂起.解锁时,线程A尝试使用CAS将对象c的MarkWord改回自己栈中复制的那个MarkWord,因为对象c中的MarkWord已经被指向为重量级锁了,所以CAS失败.线程A会释放锁并唤起等待的线程,进行新一轮的竞争.
与ReentrantLock区别
基本区别
- Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;
- Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断
- 通过Lock可以知道有没有成功获取到锁,synchronized就不灵
- Lock可以提高多个线程进行读操作的效率
锁层面的区别
- 可中断锁
响应中断的锁,Lock是可中断锁(体现在lockInterruptibly()方法),synchronized不是。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。 - 公平锁
尽量以请求锁的顺序获取锁。比如同时有多个线程在等待一个锁,当锁被释放后,等待时间最长的获取该锁,跟京牌司法拍卖一个道理。非公平锁可能会导致有些线程永远得不到锁,synchronized是非公平锁,ReentrantLock是公平锁。 - 读写锁
读写锁将对一个资源(如文件)的访问分为2个锁,一个读锁,一个写锁;读写锁使得多个线程的读操作可以并发进行,不需同步。而写操作就得需要同步,提高了效率
ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock实现了这个接口。可通过readLock()获取读锁,writeLock()获取写锁 - 绑定多个条件
一个ReentrantLock可以绑定多个Condition对象,仅需多次调用new Condition()即可;而在synchronized中锁锁对象的wait()、notify()/notifyAll()可以实现一个隐含的条件,如果要和多余的条件关联,就不得不额外的增加一个锁。
性能区别
大量线程同时竞争,ReentrantLock要远胜于synchronized。
JDK5中,synchronized是性能低效的,因为这是一个重量级操作,对性能的最大影响是阻塞的实现,挂起线程和恢复线程的操作,都需要转入内核态中完成,给并发带来了很大压力。
JDK6中synchronized加入了自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等一系列优化,官方也支持synchronized,提倡在synchronized能实现需求的前提下,优先考虑synchronized来进行同步。