synchronise一种常见的解决并发问题最常用的方式,面试对于synchronise考察主要在于其原理实现,如何使用等方面,以及和lock对比等问题,我们一起看来下下面几个问题,小伙伴们是否能回答上来
对象锁包括方法锁(默认锁对象为当前实例对象this)和同步代码块锁
方法锁修饰的是普通方法,锁对象默认为this
public synchronized void method() {}
同步代码块锁
synchronized(实例化对象) {
//这里是同步代码块
}
this对象
synchronized(this) {
//这里是同步代码块
}
类锁指的是synchronize修饰静态的方法或者指定锁对象为Class对象
修饰静态方法
public static synchronized void method() {
}
修饰指定锁对象为class对象
synchronized(类名.class){
}
Synchronize实现加锁和释放锁主要是通过一个monitor对象来完成,主要设计两个指令monitorenter和monitorexit两个指令来完成,同一个时间内一个monitor锁只能被一个线程获得,每一个对象在同一时间内只能与一个monitor锁相关联。 同时一个对象在尝试获取和这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下三种情况:
monitorexit指令:会释放对monitor(锁)的所有权,释放过程就是将锁的计数器减1
JVM中monitorenter和monitorexit字节码依赖于底层操作系统Mutex lock 锁来实现,但是由于Mutex Lock需要将当前线程挂起并且从用户态切换成内核态来执行,会严重影响程序运行性能。在JDK1.6中引入很多优化,比如锁粗化,锁消除,轻量级锁,偏向锁,适应性自旋等技术减少锁操作开销。
JDK1.6开始,对于Synchronize的实现机制进行了重大调整,增加了自适应CAS自旋,锁消除,锁粗化,偏向锁,轻量级锁优化策略,Synchronize同步锁一共有四种状态:无锁,偏向锁,轻量级锁,重量级锁,会随着竞争情况升级(只能升不能降)
线程的阻塞和唤醒需要CPU从用户态转化为核心态,频繁的阻塞和唤醒会影响到系统的并发,同时在很多应用上面,对象锁的锁状态只会持续很短一段时间,为了这很短的时间频繁阻塞和唤醒线程也不值当,这里就引入了自旋锁。自旋锁就是一个线程尝试获取某一个锁的时候,如果锁被其他线程占用,就会一直循环检测锁是否被释放,而不是将线程挂起获取进入睡眠状态。
优点/缺点:如果等待持有锁的线程很快释放锁,自旋效率就很好,如果太长就会一直占用CPU处理器的时间,浪费系统资源,一般会对自旋的时间有个限制,如果没有获取到锁,线程就会被挂起。
在JDK1.6引入了自适应自旋锁,线程如果这一次自旋成功,那么下一次自旋的次数就会更多,因为虚拟机认为上次你都成功了,下一次增加次数肯定也会成功。
通过逃逸分析发现代码没有资源竞争的可能,但是代码自己加了锁,虚拟机编译的时候就会直接去掉这个锁。比如在操作字符串进行相加的时候,由于String是一个不可变类,对字符串的连续操作是通过生成新的String来进行的,在JDK1.5之前会使用StringBuffer对象连续进行append()操作,查看源码可以知识,StringBuffe的StringBuffer方法加synchronise的。在JDK1.5之后,转化为StringBuide对象进行连续append()操作,这个操作不加synchronise,因为这是局部使用,不存在资源竞争
public static String lockElimination(String s1, String s2, String s3) {
String s = s1 + s2 + s3;
return s;
}
锁粗化就是将连续多个加锁,解锁操作连接在一起,扩展成一个范围更大的锁,还有个例子就是在for循环中加synchronize关键字,这里也会扩大锁的范围避免频繁拿到锁进行上下文切换
/**
* StringBuffer的append操作会加Synchronize关键字,一个append加一个,这谁能受得了,JVM会优化成一个synchronise操作
*/
public static String lockCoarsening(String s1, String s2, String s3) {
StringBuffer sb=new StringBuffer();
sb.append("1");
sb.append("2");
sb.append("3");
return sb.toString();
}
这两种锁既是一种优化策略,也是一种膨胀过程,引入轻量级锁的主要目的是:在没有多线程竞争的前提下,减少传统重量级使用操作系统互斥而产生的性能消耗。关闭偏向锁或者多线程竞争偏向锁的时候会导致偏向锁升级为轻量级锁。
偏向锁是为了解决大多数情况下虽然加了锁,但是没有竞争的发生,甚至是同一个线程反复获得这个锁。主要过程如下
首先JVM需要设置可用偏向锁,进程访问同步代码块并且获取到锁的时候,会在对象头和栈帧的锁记录里面存储取到的偏向锁的线程ID,下次你在来的时候首先检查对象头MarkWord是不是存储的是这个线程的ID,如果是,直接进去,不是的话,会出现两种情况
轻量级锁是偏向锁的产物,线程同步的时候,会在当前线程的栈帧记录中创建一个锁记录(Lock Record)的空间,JVM请求的时候会使用CAS将锁对像头Make Word拷贝到锁记录里面。如果CAS成功,就会将对象头里面的标准为01更新为00获取到锁,失败的话说明线程出现竞争,锁膨胀为重量级锁
重量级锁是通过对象内部监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换内核态,切换成本最高
感觉有帮助的同学还请点赞关注,这将对我是很大的鼓励~,公众号有自己开始总结的一系列文章,需要的小伙伴还请关注下个人公众号程序员fly,希望能一起成长。
https://www.cnblogs.com/aspirant/p/11705068.html
https://www.cnblogs.com/aspirant/p/11470858.html
https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html