Java的锁-1
Lock接口
简介
锁是一种工具,用于控制共享资源的访问。Lock接口最常见的实现类是ReentrantLock。
Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
Lock不是用来代替synchronized,而是当synchronized不适合或不满足要求的时候,来提供高级功能的。
通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可以允许并发访问,如:ReadWriteLock里的ReadLock。
为什么synchronized不够用?
- 效率低:锁的释放情况少(要么异常了,要么执行完毕了,才会释放)、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的进程。
- 不够灵活:读写锁更加灵活,加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),比如读写锁就可以在读和写的两个场景做出不同的操作。
- 无法知道是否成功获取到锁。
为什么需要Lock?
Lock中声明了四个方法来获取锁。
方法介绍
lock();
-
lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待.
-
Lock不会像synchronized一样在异常时自动释放锁。
注意:因此最佳操作是,无论如何先在finally中写上释放锁,以保证发生异常时锁一定被释放。
private static Lock lock = new ReentrantLock(); public static void main(String...args){ lock.lock(); try { //锁定的内容 }finally { //释放锁 lock.unlock(); } }
-
lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock(就会陷入永久等待)。
tryLock();
-
由于lock有可能带来死锁问题,所以可以使用tryLock来防止永久等待。
-
tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败。
-
相比于lock,这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为。
-
该方法会立即返回,即便在拿不到锁时不会一直在那等。
tryLock(long time, TimeUnit unit);
添入了一个参数用于等待后再返回。
没有获得锁,unlock不会执行,但是lock了一定要unlock。
public class TryLockDeadLock implements Runnable {
//使用tryLock来避免死锁
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
//让他们争抢,
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 1;
r2.flag = 2;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
//没拿到就循环
for (int i = 0; i < 100; i++) {
//线程1
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("thread1 get lock1.");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("thread1 get lock2");
System.out.println("thread1 get two lock");
//拿到两把锁就退出循环
break;
} finally {
lock2.unlock();
}
} else {
System.out.println("thread1 get lock2 failed,trying.");
}
} finally {
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("thread1 get failed, trying.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//线程2
if (flag == 2) {
try {
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("thread1 get lock2.");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println("thread2 get lock1");
System.out.println("thread2 get two lock");
break;
} finally {
lock1.unlock();
}
} else {
System.out.println("thread2 get lock1 failed,trying.");
}
} finally {
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("thread2 get lock2 failed, trying.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
lockInterruptibly();
其实就是超时时间无限的tryLock
public class LockInterruptibility implements Runnable{
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
LockInterruptibility test = new LockInterruptibility();
Thread thread0 = new Thread(test);
Thread thread1 = new Thread(test);
thread0.start();
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread0.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"尝试获取锁");
try{
lock.lockInterruptibly();
try{
System.out.println(Thread.currentThread().getName()+"得到锁");
//拿到锁的保持锁,然后让另一个线程拿不到锁,有机会去打断它
Thread.sleep(5000);
}catch(InterruptedException e){
System.out.println(Thread.currentThread().getName()+"sleep时中断");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"释放锁");
}
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+"获得锁时被中断释放锁");
e.printStackTrace();
}
}
}
/*
Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0得到锁
Thread-0sleep时中断
Thread-0释放锁
Thread-1得到锁
Thread-1释放锁*/
/*这里如果中断的是在获取锁的线程,那么由于lockInterruptibly就会中断不再阻塞
Thread-0尝试获取锁
Thread-0得到锁
Thread-1尝试获取锁
Thread-1获得锁时被中断释放锁
Thread-0释放锁
Process finished with exit code 0*/
unlock()
提前finally防止死锁
可见性保证
happens-before
前一个操作的结果可以被后续操作获取
Lock的加解锁和synchronized有同样的内存语义,synchronized有内存可见性,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作
lock的可见性
在lock和unlock之间的操作,在后续获得lock的线程中都是可见的
锁的分类
这些分类,是从各种不同角度出发去看的
这些分类并不是互斥的,也就是多个类型可以并存∶有可能一个锁,同时属于两种类型
比如ReentrantLock既是互斥锁,又是可重入锁
思维导图
乐观锁和悲观锁
为什么会诞生非互斥同步锁
互斥同步锁的劣势
- 阻塞和唤醒带来的性能劣势:如用户态核心态的切换,上下文切换,检查是否有被阻塞线程需要被唤醒。
- 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行。
- 优先级反转:设置线程优先级时,如果优先级低的线程不释放锁或者释放较慢,就会导致高优先级的线程因为拿不到锁而阻塞。
什么是乐观锁和悲观锁
看是否真的锁住资源。
悲观锁
如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失。
Java中悲观锁的实现就是synchronized和Lock相关类,synchronized在后续JVM、hotspot优化之后有了偏向锁和CAS的部分,但是总体还是算悲观锁。
乐观锁
认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象。
在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据。
如果数据和一开始拿到的不一样了,说明其他线程在这段时间内改过数据,那就不能继续刚才的更新数据过程了,可以选择放弃、报错、重试等策略。
乐观锁的实现一般都是利用CAS算法来实现的,所以CAS是乐观锁的一种。
典型例子
悲观锁:synchronized和lock接口
乐观锁:原子类、并发容器
Git
Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败﹔如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库。
如果使用悲观锁,那么在一个人写代码的时候上锁了,别人就动不了了,大家别干活了,无法协作。
数据库
-
select for update就是悲观锁
-
version控制数据库就是乐观锁
添加一个字段lock_version
先查询这个更新语句的version : select * from table然后update set num = 2,version = version +1 whereversion = 1 and id = 5;
如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理。
开销对比
- 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
- 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
两种锁各自的使用场景
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况∶
- 临界区有IO操作
- 临界区代码复杂或者循环量大3临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。代码内容执行时间快预计等待时间少的场景。
可重入锁
ReentrantLock和synchronized都是可重入锁。
演示ReentrantLock的两个普通用法,和可重入无关
- 预定电影票。
- 打印字符串或者流,防止串流。
public class LockDemo {
public static void main(String[] args) {
new LockDemo().init();
}
private void init() {
//使用同一个输出器来演示多线程时的问题
final Outputer outputer = new Outputer();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
Thread.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
outputer.output("1234");
}
}
}).start();
new Thread(() -> {
while (true){
try {
Thread.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
outputer.output("ABCD");
}
}).start();
}
static class Outputer {
Lock lock = new ReentrantLock();
//字符串打印方法,一个个字符的打印
public void output(String name) {
int len = name.length();
lock.lock();
try {
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println("");
} finally {
lock.unlock();
}
}
}
}
什么是可重入
可重复可递归调用的锁就是可重入的,而不可重入就是在获取这个锁之后你还想再加一层同一把锁是不行的会发生死锁的 。
可重入的演示
使用getHoldCount()获取重入次数
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
/*
0
1
2
1
0*/
可重入的用途
在递归调用的方法中,不想放弃资源的锁就只能在递归中重复调用,而且还能根据重入次数来判断递归的深度。
可重入的性质
源码对比∶
可重入锁ReentrantLock以及非可重入锁ThreadPoolExecutor的Worker类
ReentrantLock的上锁的核心是AQS(一种框架工具,是很多并发工具的核心)
重入源码
//ReentrantLock的抽象静态内部类sync在继承了AQS之后
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//static变量state记录着上锁的次数通过getState赋值给c
int c = getState();
if (c == 0) {
//通过CAS,如果没人持有,将锁的拥有者赋值为当前线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//有人持有锁即c比0大,那么就会判断持有者是否是当前线程
else if (current == getExclusiveOwnerThread()) {
//新的锁次数,acquires通常是1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置次数
setState(nextc);
return true;
}
return false;
}
释放的源码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果c即重入次数减为0就证明锁是自由的了
if (c == 0) {
free = true;
//将锁持有者置空
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
非可重入锁
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{}
protected boolean tryAcquire(int unused) {
//之间获取锁,只有0,1,不是0就返回false
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//直接释放
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
ReentrantLock的其他方法
isHeldByCurrentThread
可以看出锁是否被当前线程持有getQueueLength
可以返回当前正在等待这把锁的队列有多长
一般这两个方法是开发和调试时候使用,上线后用到的不多
公平锁非公平锁
什么是公平和非公平
公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
非公平也同样不提倡“插队”行为,这里的非公平,指的是在合适的时机插队,而不是盲目插队。
什么是合适的时机?
线程从阻塞状态被唤醒需要一定时间切换,这时别的进行的线程需要一点点计算资源就可以,短时间就能使用完并释放锁并且没有唤醒成本。
这样避免唤醒带来的空档期。
公平的情况(以ReentrantLock为例)
如果在创建ReentrantLock对象时,参数填写为true ,那么这就是个公平锁。
假设线程1234是按顺序调用lock()的,那么就是正常排队。
不公平的情况(以ReentrantLock为例)
默认不公平锁
如果在线程1释放锁的时候,线程5执行过程中恰好去执行lock()。
由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没来得及获取到,因为获取需要时间)。
线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认的公平策略,也就是“不公平”。
代码案例︰演示公平和非公平的效果
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread[] thread = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Task(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Task implements Runnable{
PrintQueue printQueue;
public Task(PrintQueue printQueue){
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--start");
printQueue.printTask(new Object());
System.out.println(Thread.currentThread().getName()+"--over");
}
}
class PrintQueue{
//在这里调整公平性
private Lock queueLock = new ReentrantLock(true);
public void printTask(Object document){
duboPrint();
duboPrint();
}
private void duboPrint() {
queueLock.lock();
try {
int duration = new Random().nextInt(6)+1;
System.out.println(Thread.currentThread().getName()+"--需要"+duration);
Thread.sleep(duration*1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
公平特例
tryLock()方法
它不遵守设定的公平的规则。
例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了
对比公平和非公平的优缺点
公平锁
- 优点:各线程公平平等,每个线程在等待锁之后都有机会执行
- 缺点:更慢,吞吐量更小
非公平锁
- 优点:更快,吞吐量更大
- 缺点:有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行
源码分析
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//'多了一个与'就是加了判断,是否有排队线程排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//不管是否有排队线程,CAS通过就直接获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}