synchronized 的使用和特性
synchronized 锁对象
普通方法
synchronized 锁普通方法时,其锁的对象是调用该方法的实例
public synchronized void method() { // 方法体
}
静态方法
静态方法的锁对象是所属的 class,全局只有一个。
public static synchronized void staticMethod() { // 方法体
}
同步代码块
锁对象为括号内的指定对象。
synchronized(this) { // 代码块
}
synchronized 特性
有序性
读读,读写,写读,写写互斥。
可见性
可见性是指多个线程访问一个资源时,该资源的状态,值等对于其他线程都是可见的。synchronized 和 volatile 都具有可见性,其中 synchronized 对一个类或对象加锁时,一个线程如果要访问该累或对象必须先获得它的锁。这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存中,保证资源变量的可见性。
原子性
原子性指的是同一时间只有一个线程去执行代码,该操作是不能被其他线程打断的,那么就具备了原子性。
synchronized的原子性本质上是线程互斥保证的原子性。
可重入性
同一线程在持有锁的情况下,可多次获取同一锁而不会导致死锁或阻塞其他线程。这种机制通过维护锁的持有计数器实现,当线程首次获取锁时计数器设为1,每次重入增加计数,释放时减少计数,直到归零才释放锁。
synchronized 锁升级的对象头内容:
偏向锁的意义和使用前提
偏向锁就是在运行过程中,对象的锁偏向某个线程。即在开启偏向锁机制的情况下,某个线程获得锁,当该线程下次再想要获得锁时,不需要重新申请获得锁(即忽略synchronized关键词),直接就可以执行同步代码,比较适合竞争较少的情况。
JDK 1.8 下加锁会默认开启偏向锁。但是它在应用程序启动几秒后才会开启,是存在延迟启动的情况。因此可能在打印输出加锁时的信息会发现不符合偏向锁的锁标志。
当我们开启了偏向锁,并且没有延迟开启的时候,新创建的对象的 mark word 默认就是偏向锁状态的 markWord,只不过这个时候,因为没有线程争抢锁,除了我们的锁标志位和是否为偏向锁标志位,其他都是 0
延迟的关闭和偏向锁的关闭
延迟是可以关闭的。可以给 JVM 设置参数:-XX:BiasedLockingStartupDelay=0 来关闭延迟。
如果希望关闭偏向锁:-XX:-UseBiasedLocking=false
偏向锁细节
无锁状态下的 MarkWord 标志(看第一行的二进制数字):
01 表示偏向锁,是由于开启偏向锁且没有延迟开启的情况下会显示的,但是此时并没有线程争夺锁。因此其他位置都是 0 ,仅仅是锁标志位和是否为偏向锁标志位有变化。
线程加上偏向锁后:
根据上图,偏向锁加上了后会有标识线程 ID,Epoch 等信息,因此不全是 0 。
如果我们在加锁前调用 hashcode 方法,会导致后续加锁后变为轻量级锁。
原理:
-
被加锁的对象,没有真正调用或者隐式的调用(比如使用 HashMap 放入当前对象,会调用 HashCode 方法)父类 Object 的 hashCode 方法,如果一旦调用了 hashCode 方法,对象头里需要有一个存储该 hashCode 值的位置。但是我们可以从上图中看到,偏向锁中并没有地方进行 MarkWord 的保存,只有轻量级锁才会有。
-
为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID(对象头:存储线程 ID,栈帧的锁记录中:线程有自己的栈帧,LOCK RECORD: 存储当前线程 ID),以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。(是否是当前线程的偏向锁是通过ID来匹配的)
-
如果测试成功,表示线程已经获得了锁。
-
如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(CAS 竞争,替换线程 ID)