概述
使用 Synchronized 关键字是解决并发问题的最简单的一种方式之一,我们只需要使用它修饰需要被并发处理的代码块、静态方法或普通方法,虚拟机自动为它加锁和释放锁,并将不能获得锁的线程阻塞在相应的同步队列上。
Synchronized是一种互斥阻塞的同步方式:它具有原子性和可见性的特征。
- 互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块进行访问。互斥性我们也往往称为操作的原子性
- 可见性:确保在锁被释放之前,对共享变量所做的修改,(刷新到主内存)对于随后获得该锁的另一个线程是可见的。
使用方式
synchronized关键字最主要的三种使用方式:
- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁(作用于实例对象)
- 修饰静态方法,锁的是当前 Class 对象,进入同步代码前要获得当前类对象的锁(作用于类)
- 修饰代码块,锁的是 () 中的对象,指定加锁对象,进入同步代码库前要获得给定对象的锁.(作用于实例对象或者类)
修饰静态方法时会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
实现原理
synchronized 同步代码块的语义底层是基于对象内部的监视器锁(monitor),JVM 通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步
具体实现是在编译之后在同步方法调用前加入一个 monitorenter 指令,在退出方法和异常处插入 monitorexit 的指令。其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处(线程将进入同步队列),直到获取锁的线程 monitor.exit 之后同步队列中的线程才能出队并尝试继续获取锁。
该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态从运行状态变为阻塞状态,当Object的监视器占有者释放后,在同步队列中的线程就会有机会重新获取该监视器。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
虚拟机对synchronized的优化
synchronized 属于重量级锁,效率低下。为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁等。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
偏向锁
偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这种场合极有可能每次申请锁的线程都是不相同的。
偏向锁的释放:当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 Mark Word 设置为无锁或者是轻量锁状态。
轻量锁
轻量级锁是相对基于OS的互斥量实现的重量级锁而言的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用OS的互斥量而带来的性能消耗。
轻量级锁提升性能的经验依据是:对于绝大部分锁,在整个同步周期内都是不存在竞争的。如果没有竞争,轻量级锁就可以使用 CAS 操作避免互斥量的开销,从而提升效率。
加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
自旋锁和适应性自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定。