Java中的锁 | 锁的种类
1. 悲/乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号
,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
1.1 CAS
- CAS全称
compare and swap
,一个CPU原子指令,在硬件层面实现的机制,体现了乐观锁的思想。 - JVM用C语言封装了汇编调用。Java的基础库中有很多类就是基于 JNI 调用C接口实现了多线程同步更新的功能。
CAS原理:
CAS有三个操作数:
- 当前主内存变量的值V
- 线程本地变量预期值A
- 线程本地待更新值B。
当需要更新变量值的时候,会先获取到内存变量值V然后跟预期值A进行比较,如果相同则更新为B,如果不同,则将最新的变量值更新到预期值中再重新尝试上面的步骤,也就是比较并交换直到成功为止。
关于CAS的原子性
如果普通线程执行加减操作, 反编译可以看到其是由三个指令构成的:
所以多线程切换
可能会造成数据更新的不同步
解决方案 : 就是对被操作的数据加锁,可以是悲观锁,可以是乐观锁,这里使用的就是基于乐观锁实现的AtomicInteger类
CAS的缺点
- ABA问题:内存对象从A变成B在变成A,CAS会当成没有变化,进而去更新值,实际是有变化的。
- 循环时间开销大:一直和预期值不对的情况下,会一直循环。
- 只能保证一个共享变量的原子操作。
2. 公平/非公平锁
- 公平锁是FIFO机制,谁先来谁就在队列的前面,就能优先获得锁
- 非公平锁支持抢占模式,先来的不一定能得到锁,所有在等待中的线程(也包括新来的线程)都有几率抢占到锁;
2.1 公平/非公平锁的代码演示
package thread;
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
public class Test6 {
public static class Service {
public ReentrantLock fairLock = new ReentrantLock(true);
public void testMethod() {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + "拿到了锁");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
ArrayList<Thread> list = new ArrayList<>();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始运行");
service.testMethod();
}
};
for (int i = 0; i < 10; i++) {
list.add(new Thread(runnable));
}
System.out.println(list);
for (Thread thread : list) {
thread.start();
}
}
}
----output----
[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]
Thread-0开始运行
Thread-1开始运行
Thread-0拿到了锁
Thread-2开始运行
Thread-2拿到了锁
Thread-3开始运行
Thread-3拿到了锁
Thread-4开始运行
Thread-5开始运行
Thread-6开始运行
Thread-4拿到了锁
Thread-1拿到了锁
Thread-6拿到了锁
Thread-7开始运行
Thread-8开始运行
Thread-5拿到了锁
Thread-9开始运行
Thread-7拿到了锁
Thread-8拿到了锁
Thread-9拿到了锁
运行的顺序:0,1,2,3,4,5,6,7,8,9.
获得锁的顺序:0,2,3,4,1,6,5,7,8,9.
把true去掉改成非公平锁
----output----
[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]
Thread-0开始运行
Thread-1开始运行
Thread-2开始运行
Thread-3开始运行
Thread-4开始运行
Thread-1拿到了锁
Thread-6开始运行
Thread-6拿到了锁
Thread-5开始运行
Thread-2拿到了锁
Thread-7开始运行
Thread-8开始运行
Thread-7拿到了锁
Thread-9开始运行
Thread-3拿到了锁
Thread-0拿到了锁
Thread-4拿到了锁
Thread-5拿到了锁
Thread-8拿到了锁
Thread-9拿到了锁
运行的顺序:0,1,2,3,4,5,6,7,8,9
拿到锁的顺序:1,6,2,7,3,6,4,5,8,9
上面的例子可以看出: 公平锁的是有顺序的,非公平锁是无序的。同样除了重入锁,重入读写锁一样可以设置非公平和公平。
由重量级细化分为四种:自旋锁,轻量级锁,偏向锁,重量级锁
自旋锁,轻量级锁,偏向锁,重量级锁
3. 读写锁
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock
适用于读多写少的并发情况。
3.1 读写锁接口
Java并发包中 ReadWriteLock
是一个接口,主要有两个方法,如下:
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
Java并发库中 ReetrantReadWriteLock
实现了 ReadWriteLock
接口并添加了可重入的特性。
3.2 ReentrantReadWriteLock
特性:
- 获取顺序
- 非公平模式 (默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。 - 公平模式
当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。
- 非公平模式 (默认)
- 可重入
允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。 - 锁降级
允许写锁降低为读锁 - 中断锁的获取
在读锁和写锁的获取过程中支持中断 - 支持Condition
写锁提供Condition实现 - 监控
提供确定锁是否被持有等辅助方法
4. 可重入锁
可重入锁有 :
- sychronized
- ReentrantLock : ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,
并且**加锁次数和释放次数要一样**
4.1 可重入锁代码演示
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
// 演示可重入锁是什么意思
public class WhatReentrant2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}