基础概念
概念 | 解释 |
---|---|
同步资源 | 同一时间只能有一个线程占有的资源 |
自旋(忙等待) | 线程会一直尝试获取锁(死循环) |
锁分类
锁与不锁
悲观锁
- 自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- Java中,synchronized关键字和Lock的实现类都是悲观锁
- 适合写操作多的场景,加锁保证数据正确性
乐观锁
- 自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
- 无锁编程来实现,最常采用的是CAS算法
- 适合读操作多的场景,不加锁提升性能
CAS
- CAS全称 Compare And Swap(比较与交换)
- 需要读写的内存值 V
- 进行比较的值 A
- 要写入的新值 B
- 当且仅当V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值,一般情况下,“更新”是一个不断重试的操作(自旋)
- 通俗理解:不再是简单地将值更新成B;而是将值A更新成B,如果不是A,就不更新
问题
- ABA问题:但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的
- 解决:增加标识位,
A-B-A
变成了1A-2B-3A
阻塞与不阻塞
非自旋锁
- 获取不到锁就被阻塞,CPU执行切换到其他线程。等下次获取到CPU时间片时,再尝试获取锁
- 适合锁占有时间很长的操作
自旋锁
- 获取不到锁就一直尝试获取锁
- 避免了CPU切换线程的成本,但是会占用CPU时间。所以自旋一般有次数限制,超出次数,应该被阻塞
- 适合锁占有时间很短的操作
- 自旋锁在JDK4中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了适应性自旋锁
适应性自旋锁
- 自旋次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间
- 如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
插队与不插队
公平锁
- 按顺序获取锁,一个线程想要获取锁,到队列末尾排队
- 线程不会饥饿
- 吞吐效率略低
非公平锁
- 想要获取锁,会尝试插队(直接获取锁)
- 成功(锁刚好可用):直接获取到锁
- 失败:到队列末尾排队
- 可能出现后申请锁的线程先获取锁的场景
- 线程可能会饥饿
- 吞吐效果高,节省CPU切换线程的开销
共享与不共享
排他锁
- 锁只能被一个线程持有
- 线程对数据加排他锁之后,其他线程不能对数据再加任何类型的锁
- 可读可写
- JDK中的synchronized和JUC中Lock的实现类就是互斥锁
共享锁
- 锁可以被多个线程同时持有
- 线程对数据加共享锁后,其他线程可以对数据再加共享锁,不能加排他锁
- 只读
重入与不重入
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
可重入锁
- 可一定程度避免死锁
- 同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁
- Java中ReentrantLock和synchronized都是可重入锁
- 因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作
不可重入锁
- 那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁
synchronized 的四种状态
- 只能升级,不能降级
无锁
- 不锁定同步资源,依赖CAS
偏向锁
- 资源一直被同一个线程访问,那么该线程会自动获取偏向锁,降低获取锁的代价
- 线程执行完同步代码块后,线程并不会主动释放偏向锁
偏向锁的撤销
- 需要等待全局安全点(在这个时间点上没有字节码正在执行)
- 暂停持有偏向锁的线程
- 判断原持有偏向锁的线程状态
- 恢复到无锁或者轻量级锁
轻量级锁
- 当偏向锁被其他线程竞争时,会升级为轻量级锁
- 自旋锁,不会阻塞,从而提高性能
重量级锁
- 某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(锁膨胀)
- 阻塞没有获取到锁的线程
升级流程
java.util.concurrent.locks中的锁
Lock
锁接口
ReentrantLock
- 可重入实现
- 排他锁
public ReentrantLock(boolean fair)
可以设置为公平或者非公平锁
public static void lockDemo() {
// 如果不设置公平锁的话,线程0很难获取到锁
final Lock lock = new ReentrantLock();
for (int i = 0; i < 2; i++) {
final Thread thread = new Thread(() -> {
final String name = Thread.currentThread().getName();
while (true) {
try {
while (!lock.tryLock()) {
if (name.contains("1")) {
TimeUnit.SECONDS.sleep(2);
}
System.out.println(name + " is nb;");
}
} catch (Exception exception) {
exception.printStackTrace();
} finally {
lock.unlock();
}
}
}, "thread-" + i);
thread.start();
}
}
ReadWriteLock
- 读写锁接口
- 读锁。共享锁
- 写锁。排他锁
ReentrantReadWriteLock
- 可重入实现
public ReentrantReadWriteLock(boolean fair)
可以设置为公平或者非公平锁- 可以并发读
- 读的时候不可以写
- 写的时候不可以读
public static void readWriteLockDemo() {
final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
final Thread write = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 准备写数据;");
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread()