解决线程安全都有哪几种方式?
1. 锁(悲观锁,乐观锁)
2.原子类Atomic(CAS比较并赋值,防止线程睡觉,从用户态转换成内核态,自旋重试10次)
3.Threadlocal
悲观锁
synchronized介绍
1. synchronized是基于JVM实现的,是java的一个关键字,可以修饰方法、对象、代码块,但是锁得都是对象;
2. 底层是mutix互斥变量;
3. 加锁:monitor enter 释放锁:monitor exit;
4.不可以手动加锁释放锁;
5.1.6之前是一个重量型锁,效率慢,因为线程一旦抢不到锁,会阻塞状态(睡觉),并且会有死锁的情况。1.6之后进行了优化,锁升级;
6.是一个非公平锁并且可重入,不能指定等待时间
synchronized底层实现
java中每一个new的对象中,都有一一绑定的monitor对象(c++实现),由他来操作synchronized底层的mutex互斥变量达到锁的效果
synchronized 1.6锁优化(升级,消除,粗化)
一旦线程抢不到锁,那就会从用户态变成内核态,还要去重新抢占cpu资源去拿锁,很耗费性能,所以才要进行优化
一 锁升级
当一个对象只有在无锁的时候,对象头markdown才会存储hashcode,如果这个时候调用他的hashcode方法会从无锁直接升级成重量级锁,因为没有地方存threadId了,也就是没有偏向锁了
对象头markdown中的分代年龄 大小为4bit位 最大是15,再大就放不下了,所以垃圾回收,青年代到老年代要经过15次才到老年代
1. 无锁
没有任何锁竞争的场景(可以看下面的锁消除的代码例子)
2.偏向锁
只有单线程执行任务(比如单线程多次调用synchronized同步代码块),对象头中的markdown会记录拿到锁的线程id,如果下次又来获取锁的话会直接获取到
3.轻量锁
有两个或两线程执行任务(并发不高),可能第一个线程执行个几毫秒后就释放锁了(稍微自旋一下就抢到锁了)
4.重量锁
有多个线程在执行任务(并发高),并且还很耗时很久不释放锁,CAS就吃不消了,那其他没抢到锁的线程就直接睡觉去了
二 锁粗化
java代码中,有连续多个synchronized代码块,合并成一个大的代码块,这样就不用多次上锁解锁
三 锁消除
锁消除,虽然写了synchronized代码块,但是根本没有用到锁,都没有锁竞争,所以就直接消除了
锁的升级流程:(了解即可,面试加分项)
首先当单线程执行任务的时候,对象的markdown中会记录当前持有锁的线程id,ThreadId
当再有一个线程要执行任务的时候(他会等到全局安全点 safe point 再去执行)
他会判断第一个线程是否还在存活或者是否还持有锁
如果不存活了死了,他会拿到锁(这时候还是偏向锁)
如果还在存活,并且代码块已经执行完毕了,那这时候也会拿到锁(还是偏向锁)
如果还在存活,并且还在执行代码块,这时候就不满足便向锁的概念了(就不是单线程了),就要升级成轻量级锁,需要做3步操作
1. 将第一个对象中的markdown搬到 线程栈 lock recod 空间中
2.指针指向lock recod 空间(不指向就找不到markdown了,并且也是为了节省空间,64位操作系统中一个指针就有64bit)
3.再cas一下
如果cas失败了,那就意味了出现了比较明显的竞争了,那就要升级成重量级锁
对象中的markdown会复制一份到monitor对象中,用指针和monitor对象关联上,用monitor对象来操作synchronized中的mutex互斥变量来实现重量锁
ReentrantLock介绍
1.基于JDK实现的;
2. 可以手动加锁和释放锁(lock());
3.可以设置等待时间;
4.可以是非公平也可以是公平(可以设置),也是可重入锁;
5.1.6之前性能是比synchronized高,1.6之后由于synchronized进行了优化,他俩的性能差不多
ReentrantLock的底层实现
原理其实就是利用了CAS,修改变量state+1,如果成功了就加到了锁,如果不成功AQS自旋重试一次,他之前比synchronized性能好就是因为这个CAS的操作,尽可能不让线程切换状态,会把自己包装成一个node节点,如果是头节点的话,会CAS一下抢一下锁,但是synchronized(1.6之前)是其他线程抢不到锁会去睡觉再唤醒,性能很低。
乐观锁
CAS算是乐观锁(比较并赋值),他不是真正的锁,是逻辑锁,也能达到锁的效果,但是不适用与并发高的情况,cas在并发高的情况,由于自旋空转时间久了也很浪费cpu的性能