java中的一个关键字,可作为方法,代码块的修饰符,目的是保证修饰范围(方法or代码块的生命周期)的线程安全性。
synchronzed是互斥锁,它既能保证资源不能被多个线程同时占用,又能够保证资源在各线程之间的可见性。
当一个线程试图进入到受保护的代码段落中,必须先获得到锁,待方法执行完成后,将锁释放。
public class Main {
public int count = 0;
public synchronized void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Main mainObj = new Main();
for(int i = 0 ; i < 10 ; i++){
new Thread(() -> {
for (int j = 0 ; j < 10 ; j++){
mainObj.increase();
}
}).start();
}
}
}
当synchronized关键字修饰普通方法时,锁为当前对象this,我们可以理解为修饰代码块时,括号里填的是this关键字。
在JVM层面通过 ACC_SYNCHRONIZED 修饰符实现。
public class Main {
public int count = 0;
public static void main(String[] args) throws InterruptedException {
Main mainObj = new Main();
for(int i = 0 ; i < 10 ; i++){
new Thread(() -> {
for (int j = 0 ; j < 10 ; j++){
synchronized (mainObj){
mainObj.count++;
}
}
}).start();
}
}
}
当synchronized修饰代码块时,括号内填写的是锁对象,这个可以是任意对象。
在JVM层面通过 monitorenter 和 monitorexit 指令实现。
public class Main {
public static int count = 0;
public static void increase(){
count++;
}
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < 10 ; i++){
new Thread(() -> {
for (int j = 0 ; j < 10 ; j++){
increase();
}
}).start();
}
}
}
static关键字修饰的资源为共享资源,若保证其线程安全则需要锁它的class文件。
在JVM层面通过 ACC_SYNCHRONIZED 修饰符实现。
在java中,一切对象都可以成为锁对象。锁对象最重要的两个元素是 markword 和 monitor机制。
其中 mark word 的作用是标记当前锁对象被那个线程占用。
monitor 的作用是既保证了临界区对于线程的互斥又实现了没有竞争到锁的对象能够被阻塞和唤醒。
mark word:
参考链接
- 由来 : java对象由 对象头,实例字段,填充区域组成。
而对象头又由 mark word 和 klass point 组成。- 含义: 用来存储对象自身的运行时数据。
- 作用: 当前锁对象被线程占用时,mark word中会存储当前线程的ID,标识了被该线程占用。
monitor机制:
参考链接每个对象都有一个监视器,在同步代码块中,JVM通过monitorenter和monitorexist指令实现同步锁的获取和释放功能
当一个线程获取同步锁时,即是通过获取monitor监视器进而等价为获取到锁,monitor的实现类似于操作系统中的管程.
工作原理:
- 线程如果获得监视锁成功,将成为该监视锁对象的拥有者
- 在任一时刻,监视器对象只属于一个活动线程(Owner)
- 拥有者可以调用wait方法自动释放监视锁,进入等待状态
在jdk1.6以后进行了一系列的锁优化。
描述:当锁是轻量级锁的时候,当前线程竞争的锁处于锁定状态,则通过执行一段无意义的空循环让线程等待一段时间,不会被立即挂起,看持有锁的线程是否很快释放锁,如果锁很快被释放,那当前线程就有机会不用阻塞就能拿到锁了,从而减少切换,提高性能(线程的阻塞 - 唤醒 需要CPU切换上下文,消耗资源。)。当超过自旋时间,则线程挂起。
缺陷: 自旋时间的长短无法合理设置。容易出现刚刚挂起,请求的锁就释放了。
为了解决自旋时间无法合理设置的问题,自适应自旋锁通过前一次在同一个锁上的自旋时间和锁的持有者的状态来决定自旋的次数,换句话说就是自旋的次数不是固定的,而是可以通过分析上次得出下次,更加智能。
将多次分段的加锁合并为一次加锁。将临界区的范围扩大。
StringBuff是一个线程安全的类,下面是我们经常用到的方法,请关注synchronized修饰符。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
当遇到如下操作时候,将会频繁的进行获取锁,释放锁的操作。这时jvm将会进行锁范围的扩大。
stringBuffer.append("线");
stringBuffer.append("程");
stringBuffer.append("安");
stringBuffer.append("全");
在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。
public void vectorTest(){
Vector vector = new Vector();
for (int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM 可以明显检测到变量 vector 没有逃逸出方法 #vectorTest() 之外,所以 JVM 可以大胆地将 vector 内部的加锁操作消除。
锁主要存在四种状态,依次是:无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态。它们会随着竞争的激烈而逐渐升级。注意,锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
Java线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要依靠操作系统从当前用户态转换到核心态中,这种状态转换需要耗费处理器很多时间,对于简单同步块,可能状态转换时间比用户代码执行时间还要长,导致实际业务处理所占比偏小,性能损失较大
引入轻量级锁的主要目的,是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统切换上下文产生的性能消耗。
当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
由于修改mark word是CAS操作,所以会有一定的资源开销,因此它提升性能的依据是绝大多数锁在整个生命周期不会存在竞争。如果打破这个规则,那么被阻塞的线程相比重量级锁还额外多了CAS操作的开销。
引入偏向锁主要目的是:为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁的CAS开销。
下面来分析一下JVM是如何减少这些CAS操作的:
一个线程第一次访问临界区时,首先需要获取锁。当的到锁的时候,会在锁的mark word中记录下自己的线程ID并将锁标记设置为偏向锁。那么这个线程下一次进入临界区的时候,将不会对mark word进行CAS操作,只需要检查mark word中是否是存储着本线程ID的偏向锁。
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。获得偏向锁的线程被挂起,当到达全局安全点(safepoint,在这个时间点上没有字节码正在执行,和GC相关)偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
和轻量级锁相同,当存在锁的竞争时,偏向锁升级轻量级锁会有很多额外操作(stop the world)。所以在锁通常存在竞争的情况下应该禁用:-XX:-UseBiasedLocking=false。
锁 | 优点 | 缺点 |
---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 |