二. CAS
1. 概念
全称Compare and swap,比较 内存A 和 寄存器R 中的数据值,如果数值相同,就把 内存B和 寄存器R 的数值进行交换;
2. 特点
CAS最大的特点就是一个CAS操作是单条CPU指令,它是原子的,所以既是CAS不加锁,也能保证线程的安全;
在JDK1.8中针对于CAS提供了一个类:
CAS中的ABA问题:
ABA问题是CAS中的面试的高频问题,我们都知道,CAS是对比内存和寄存器的值,看看是否相同,就是通过这个对比,来检测内存是不是改变过,要么相同,要么不同,不同都好区别,但是有一种相同不是真正意义上的相同,而是不确定这个值中间是否发生过改变,改变了原来的东西,但是变回到原来的状态,假设原来是a,然后变成b,后来又变成a,这就是 a->b->a 问题了;
如何解决ABA问题呢???
ABA问题实质上就是一个反复横跳的问题,我们只要约定数据只能单方面变化,要么数据只能增加,要么数据只能减小,那么问题就迎刃而解了;
如果我们要求数据既能增大又能减小,我们可以约定一个版本号变量,约定版本号只能增加,并且每次修改,都会增加一个版本号,这样我们每次对比的时候就是拿版本号去对比,而不是对比数值本身,这样也能很好的解决问题了;
三. Synchronized 原理
3.1 基本特点
1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁
3.2 加锁步骤
过程:
- 最开始是没有锁的;
- 刚开始加锁,是一个偏向锁状态;
- 遇到锁竞争,就转化为轻量级锁状态(自旋锁);
- 竞争激烈时,就会变成重量级锁,这一过程交给内核阻塞等待;
关于偏向锁:
偏向锁并非是真的加锁,只是简单的标记一下,想占有这把锁,如果没有锁竞争,那就不加锁,如果有别的线程来竞争这把锁,那么就升级成轻量级锁,这样做既保证了效率,又保证了线程安全;
3.3 锁消除
锁消除是编译阶段做的一种优化手段,用来检测当前代码是否多线程任务 或者 是否有必要加锁,如果没有必要,又把锁给写了,就会在编译过程中自动把锁去掉;
假如在不涉及线程安全的问题时,我们用 synchronized 关键字对一个操作进行加锁了,那么在编译阶段,就会自动把锁进行一个消除,锁消除这个机制是一个智能化的操作,它会根据不同的代码,去判断当前的操作需不需要进行加锁,如果不需要,就会自动消除;
3.4 锁粗化
提到锁的粗化,就要先提到一个概念叫锁的粒度,锁的粒度就是 synchronized 代码块里包含代码的多少,一般认为代码越多,粒度越粗,代码越少,粒度越细;
通常写代码的情况下,我们是希望锁的粒度更小一点,因为这样串行的代码少,并发执行的代码就越多;
举个例子:
假设我们给领导打电话汇报3份工作,分两种情况:
1. 先打个电话,汇报 A 的进展,再挂电话
再打个电话,汇报 B 的进展,再挂电话
再打个电话,汇报 C 的进展,再挂电话
每次领导接电话就是一个加锁的过程,别人(其他线程)想要给领导打电话就是处于一个阻塞等待的状态,挂电话就是释放锁;当你挂断电话后,再想去汇报工作B的进展,你不一定能打进去,领导可能和别人正在通话,这样一来,你再想打进去就要阻塞等待一会,这个过程就相当于把锁的粒度拆分的更细了,但是每次都可能会阻塞等待,这样效率并不高,还可能并发其他的问题;
2. 打通一次电话,直接把A,B,C的工作进展一次性想领导汇报;
这样就避免了阻塞等待的消耗了,也大大的提升了效率;
3.5 JUC常见组件
JUC是 Java.util.concurrent 的缩写,这里是多线程并发的一个类;
1. Callable接口
这里写一个程序去实现一下这个接口:
public class Demo16 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建任务,计算从 1 加到 100 的和 Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100; i++) { sum += i; } return sum; } }; // 创建一个线程来完成这个任务 // Thread 不能直接传 callable, 外面需要再包装一层 FutureTask<Integer> futureTask = new FutureTask<>(callable); Thread thread = new Thread(); thread.start(); System.out.println(futureTask.get()); } }
上述代码也是线程创建的一种方法;
2. ReentrantLock类
可重入互斥锁 和 synchronized 定位类似,,都是用来实现互斥效果,,保证线程安全;
但是 synchronzied 是基于代码块实现来控制加锁和解锁,而 ReentrantLock 是提供了 lock 和unlock 的方法来进行加锁和解锁;
虽然 synchronized 已经在绝大多数情况下满足使用了,但是 ReentrantLock 也有自己特殊的方法:
- 使用 synchronized 加锁的时候如果发现锁被占有,会进入阻塞状态,而 ReentrantLock 提供了一个 tryLock 方法,如果发现锁被占用,不会阻塞,直接返回 false;
- synchronized 是一个非公平锁(不遵循规则),而 ReentrantLock 提供了公平和非公平两种工作模式;
- synchronized 搭配 wait 和 notify 进行唤醒等待,如果多个线程 wait 同一个对象,notify 的时候随机唤醒一个,而 ReentrantLock 搭配 Condition 这个类进行唤醒等待,并且它能指定一个线程唤醒;