首先理解悲观锁和乐观锁:
乐观锁:
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。(有点像自旋锁)
CAS主要是三个操作:
1.获取当前值
2.当前值加一赋给目标值
3.进行CAS操作,成功跳出循环,失败就重复上述操作
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger ai = new AtomicInteger(); private int i = 0; public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<Thread>(); // 添加100个线程 for (int j = 0; j < 100; j++) { ts.add(new Thread(new Runnable() { public void run() { // 执行100次计算,预期结果应该是10000 for (int i = 0; i < 100; i++) { cas.count(); cas.safeCount(); } } })); } //开始执行 for (Thread t : ts) { t.start(); } // 等待所有线程执行完成 for (Thread t : ts) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("非线程安全计数结果:"+cas.i); System.out.println("线程安全计数结果:"+cas.ai.get()); } /** 使用CAS实现线程安全计数器 */ private void safeCount() { for (;;) { int i = ai.get(); // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值 if (ai.compareAndSet(i, ++i)) {
System.out.println(i);//并不是按顺序输出的,但所有的值都会输出来
break; } } } /** 非线程安全计数器 */ private void count() { i++; } } //结果: 非线程安全计数结果:9867 线程安全计数结果:10000
CAS操作中的三个问题:
1.ABA问题:
例子:
小灰有100元存款,要用一个提款机来提款50元。
由于提款机硬件出现问题,小灰提款操作被提交两次,开启两个线程,两个线程都是获取当前值100元,要更新成50元。
理想情况下是一个线程更新成功一个线程更新失败,只扣一次。
线程1(提款机):获取当前值100元,成功更新成50元
线程2(提款机):获取当前值100元,期望更新为50,BLOCK
线程3(小灰妈):获取当前值50元,期望更新为100元
在线程1和3执行完了以后,compare以后线程2会再次执行,就会执行两次扣钱。
如何解决这个问题?
除了比较期望值以外还要比较变量的版本号,在线程1操作的时候期望值100,版本号a01,在线程3操作完以后,期望值虽然还是变成了100,但是版本号变为了a03,在线程2去操作的时候发现版本号不一致了,就不会执行这个操作了
参考:https://www.sohu.com/a/215510186_465221
2.循环时间开销大的问题:
观察上面的代码,如果在循环的过程中长时间无法成功不能退出,那么会给cpu带来非常大的执行开销
3.只能保证一个共享变量的操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
参考:https://286.iteye.com/blog/2295165