synchronized 关键字解决的是多个线程间访问资源的同步性,synchronized 关键字保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外在早期的java版本,synchronized 关键字属于重量级锁,效率低下。jdk1.6之后有了优化。
synchronized 关键字是通过字节码指令控制程序,他是作用在jvm层面的,被synchronized 关键字修饰的方法(或代码块)在编译成class文件之后,会增加三条字节码指令,synchronized 就是通过他们来实现同步控制的。
因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized
较大优化,所以现在的 synchronized
锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
jdk1.6之后对synchronized锁得实现引入了大量的优化来减少锁的开销,比如:锁状态升级、锁消除、锁粗化
优化后的synchronized 有3种锁状态分别是:
当一个线程(T1)访问同步代码块并获取锁的时候,会在对象头和栈帧的锁记录里记录存储偏向的线程ID(就是这一个线程T1的id),以后该线程来访问这个同步代码块就不需要CAS操作加锁解锁,只需要简单测试一下对象头Mark Word里面是否有指向当前线程的偏向锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的释放:偏向锁是一种出现竞争才释放锁的机制。释放锁必须在全局安全点(在这个时间点上没有正在执行的字节码)。先暂停拥有偏向锁的线程T1,然后检查T1是否还活着,如果没活着,就将锁对象的对象头的MarkWord设置为无锁状态。如果这个线程还活着,则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程).也就是说只要竞争偏向锁,持有偏向锁的对象一定会释放这个锁
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
当偏向锁失败后,虚拟机不会立即升级为重量级锁,它会先尝试采用轻量级锁机制。
轻量级锁加锁: 线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,然后线程尝试使用CAS将对象中的Mark Word替换为指向锁记录的指针。
用自己的话说就是:当升级为轻量级锁后,线程在执行同步代码块的时候,这个线程通过CAS操作来尝试获取锁。如果失败就说明有其他线程竞争锁,当前线程采用自旋的方式获取锁。
轻量级锁的释放:锁的释放也是采用的原子级的CAS操作。轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word(Mark Word)替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
重量级锁状态就是原生的synchronized。
需要注意:锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。 这种策略是为了提高获得锁和释放锁的效率。
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。自旋次数的默认值是10次,用户可以修改--XX:PreBlockSpin
来更改。
另外,在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
编译时,发现一段代码中频繁的加锁释放锁,会将前后的锁合并为一个锁,避免频繁加锁释放锁。