(1)概念
同事务的原子性概念一样,对多个操作的处理,要么全部成功,要么全部失败;
synchronized锁可以实现原子性,基于阻塞的锁机制,效率低。可能导致优先级高的线程一直被阻塞,使用不当可能导致死锁,锁机制粒度大;
为了解决以上synchronized锁的缺点,Java提供了Atomic类系列原子操作类;
(2)CAS实现原子性
底层是调用native方法,操作系统支持的CAS指令,比如intel的汇编指令cmpxchg;
每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作;
实际应用中,通过循环语句不停地尝试执行CAS操作,直到新值赋值成功为止,Atomic类就是基于此方案来实现的;
ABA问题:
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
循环时间长开销大:
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
(3)Java中CAS相关类
CAS + volatile + native方法,实现原子操作,避免使用synchronized高开销;
native方法:Unsafe.objectFieldOffset(),是本地方法,拿到“原来”值的内存地址,返回值是valueOffset;
主要是提供原子的方式更新数组里的整型;
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组;
原子更新引用类型;
利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。
原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。
AtomicIntegerFieldUpdater //原子更新整型的字段的更新器。
AtomicLongFieldUpdater // 原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater // 原子更新引用类型里的字段。
高并发、写多读少情况下,CAS不断自旋可能会成为AtomicLong的性能瓶颈;
相比AtomicLong,LongAdder具有更好的性能,代价是消耗更多的内存空间;
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。类似于ConcurrentHashMap中的“分段锁”;
而LongAdder最终结果的求和,并没有使用全局锁,返回值不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,所以只能得到某个时刻的近似值,这也就是LongAdder并不能完全替代LongAtomic的原因之一;
除了新引入LongAdder外,还有引入了它的三个兄弟类:LongAccumulator、DoubleAdder、DoubleAccumulator;
(1)什么是线程安全性
我们可以这么理解,我们所写的代码在并发情况下使用时,总是能表现出正确的行为;反之,未实现线程安全的代码,表现的行为是不可预知的,有可能正确,而绝大多数的情况下是错误的。
(2)线程安全的类
如果要实现线程安全性,就要保证我们的类是线程安全的。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
实现方案:
避免并发最简单的方法就是线程封闭;
就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对象就算不是线程安全的也不会出现任何安全问题;
栈封闭:
使用局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
ThreadLocal:
ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
没有任何成员变量的类,就叫无状态的类,这种类一定是线程安全的。
让状态不可变,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
但是要注意,一旦类的成员变量中有对象,上述的final关键字保证不可变并不能保证类的安全性,为何?因为在多线程下,虽然对象的引用不可变,但是对象在堆上的实例是有可能被多个线程同时修改的,没有正确处理的情况下,对象实例在堆中的数据是不可预知的。
我们最常使用的保证线程安全的手段,使用synchronized关键字,使用显式锁,使用各种原子变量,修改数据时使用CAS机制等等。
(1)什么是死锁
相互持有对方的锁,死锁导致程序卡住;
互斥条件:一段时间内某资源只由一个线程占用,不能破坏,因为锁的作用就是想让多个线程互斥;
请求与保持条件:线程至少保持一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
**不剥夺条件:**指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
**环路等待条件:**死锁时存在线程相互等待阻塞的资源,比如P0等待P1、P1等待P2、P2等待P0;
死锁条件简单说:
死锁是必然发生在多操作者(M>=2个)争夺多个资源(N>=2个,且N<=M)才会发生;
争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁;
争夺者对拿到的资源不放手;
(2)如何避免死锁
解决死锁,破坏上述锁的四个必要条件之一即可:
不要同一个代码块中持有多个锁;
尽量使用juc包封装的并发类;
尽量降低锁粒度,尽量不要几个功能使用同一把锁;
尽量减少同步的代码块;
(3)死锁的场景及解决
场景:
Mysq1处理死锁:
频繁报错:Deadlock found when trying to get to lock; try restarting transaction.
-- 1.查看当前的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
-- 2.查看当前锁定的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-- 3.查看当前等锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 4.杀死进程id(就是上面命令的trx_mysql_thread_id列)
kill 线程ID
危害:
如何排查死锁:
jps显示死锁进程号;
jstack pid 查看具体的锁信息;