并发安全问题
在多线程情况下,多个线程同时访问一个共享的、可变的资源时,会发生线程安全问题。
序列化访问临界资源是解决所有线程安全类问题的方案,在同一时刻,只能有一个线程访问这些临界资源,也叫同步互斥访问。
Java提供synchronized和Lock两种方式来实现同步互斥访问。
方法内部的局部变量不是临界资源,因为这些变量只能由当前线程自己访问,不具有共享性,所以不会发生线程安全问题。
synchronized原理
synchronized是一种对象锁(不是对引用加锁),作用粒度是对象,是可重入锁 。
加锁方式:
-
同步实例方法,加锁当前实例对象;
public synchronized void m1(){}
-
同步类方法(静态方法),加锁当前Class类对象;
public static synchronized void m2(){}
-
同步代码块,加锁括号里面的对象;
Object object = new Object(); public void m3(){ synchronized (object){} }
锁膨胀升级过程
锁的状态有四种,无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁竞争的加剧,锁会升级,锁的升级过程不可逆:
偏向锁 -> 轻量级锁 -> 重量级锁
JVM对synchronized进行了很多优化,包括偏向锁、轻量级锁、自旋锁、锁消除。
偏向锁
在大多数情况下,锁不存在多线程竞争,并且总是由同一个线程多次获得,所以为了减少同一线程获取锁的开销引入了偏向锁。
当线程获取到锁,锁就进入偏向锁模式,对象头Mark Word锁标识更改为偏向锁(01),当这个线程再次请求锁时,可以直接获取到该锁,不要任何同步操作。
偏向锁优化了锁的获取过程,提高了程序的性能。
对于无锁竞争的场合,偏向锁优化效果不错。
但锁竞争比较激烈时,申请锁的线程可能每次都不相同,偏向锁就失效了。
这时候偏向锁会升级为轻量级锁。
轻量级锁
偏向锁升级为轻量级锁,对象头锁标识变为轻量级锁标志(00)。
轻量级锁提升性能的依据是:对对大部分的锁,在整个同步周期内都不存在竞争。意思就是说虽然有多个线程访问同一个锁,但这些线程都是交替执行同步块,不会存在同一时间去访问同一个锁的场景,这种场景一旦出现,就会导致轻量级锁膨胀为重量级锁。
自旋锁
在轻量级锁失败后,JVM还会进行自旋锁的优化。依据是,在大多数情况下,线程持有锁的时间不会太长,这时候让申请锁的线程进行一定数量的空循环(自旋)等待,自旋完之后如果持有锁的线程已经释放了锁,那此时就可以获取锁了。
但如果还不能获取锁,锁就会升级为重量级锁,将线程在操作系统层面挂起。
线程的挂起是重操作,需要从用户态切换到内核态,这个状态转换需要相对比较长的时间。
自旋锁的优化依据:线程挂起消耗的时间 > 线程持有锁的时间
锁消除
通过逃逸分析可以得知方法内的一个变量是否会被方法外部访问,如果不会,JVM会将该变量的锁消除掉。
如下StringBuffer#append虽然是同步方法,但StringBuffer是一个局部变量,没有被方法外部所使用,不会多线程竞争这个StringBuffer,JVM会将这个方法中的StringBuffer#append的锁消除掉。
public String add(String str){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str);
return stringBuffer.toString();
}
逃逸分析需要JVM运行在server模式(默认),同时开启逃逸分析(默认开启)
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+EliminateLocks 表示开启锁消除