java中的锁(悲观锁、乐观锁、可重入锁、不可重入锁、公平锁、非公平锁、自旋锁、阻塞锁...)
Lock接口
1.简介、地位、作用
① 锁是一种工具,用于控制对共享资源的访问
② Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用和功能上又有较大的不同
③ Lock并不是用来替代synchronized,而是当使用synchronized不合适或不满足要求的时候,来提供高级功能的
④ Lock接口最常见的实现类是ReentrantLock
⑤ 通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWriteLock里面的ReadLock
2.为什么synchronized不够用?
① 效率低:锁的释放情况少,视图获得锁时不能设定超时,不能中断一个试图获得锁的线程
② 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
③ 无法知道是否成功获取到锁
3.方法介绍
lock()方法:
① lock()就是最普通的获取锁,如果锁已经被其他线程获取,则进行等待
② lock不会像synchronized一样在异常时自动释放锁,因此最佳实践是,在finally中释放锁,以保证在发生异常时锁一定被释放。
public class MustUnlock {private static Lock lock = new ReentrantLock();public static void main(String[] args) {lock.lock();try {System.out.println(Thread.currentThread().getName() + "开始执行任务");} finally {lock.unlock();}}
}
③ lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待
tryLock():
① tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,并返回true,否则返回false,代表获取锁失败
② 相比于lock,这个的方法显然更功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
③ 该方法会立刻返回,即便在拿不到锁时不会一直在那等
/*** 描述: 用tryLock来避免死锁*/
public class TryLockDeadlock implements Runnable {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;r1.flag=0;new Thread(r1).start();new Thread(r2).start();}@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (flag == 1) {try {if (lock1.tryLock(800, TimeUnit.SECONDS)) {try {System.out.println("线程1获取到了锁1");Thread.sleep(new Random().nextInt(1000));if (lock2.tryLock(800, TimeUnit.SECONDS)) {try {System.out.println("线程1获取到了锁2");System.out.println("线程1成功获取到了两把锁");break;} finally {lock2.unlock();}} else {System.out.println("线程1获取锁2失败,已重试");}} finally {lock1.unlock();Thread.sleep(new Random().nextInt(1000));}} else {System.out.println("线程1获取锁1失败,已重试");}} catch (InterruptedException e) {e.printStackTrace();}}if (flag == 0) {try {if (lock2.tryLock(3000, TimeUnit.SECONDS)) {try {System.out.println("线程2获取到了锁2");Thread.sleep(new Random().nextInt(1000));if (lock1.tryLock(800, TimeUnit.SECONDS)) {try {System.out.println("线程2获取到了锁1");System.out.println("线程2成功获取到了两把锁");break;} finally {lock1.unlock();}} else {System.out.println("线程1获取锁2失败,已重试");}} finally {lock2.unlock();Thread.sleep(new Random().nextInt(1000));}} else {System.out.println("线程2获取锁2失败,已重试");}} catch (InterruptedException e) {e.printStackTrace();}}}}
}
tryLock(long time,TimeUnit unit):
① 超时就放弃
② lockInterruptibly():相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断
public class LockInterruptibly implements Runnable {private Lock lock = new ReentrantLock();public static void main(String[] args) {LockInterruptibly lockInterruptibly = new LockInterruptibly();Thread thread0 = new Thread(lockInterruptibly);Thread thread1 = new Thread(lockInterruptibly);thread0.start();thread1.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}thread0.interrupt();}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "尝试获取锁");try {lock.lockInterruptibly();System.out.println(Thread.currentThread().getName() + "尝试获取锁");try {System.out.println(Thread.currentThread().getName() + "获取到了锁");Thread.sleep(5000);} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");} finally {lock.unlock();System.out.println(Thread.currentThread().getName() + "释放了锁");}} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");}}
}
③ unlock():解锁
4.可见性保证
① happens-before
② Lock的加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生所有操作
锁的分类
锁分类是从不同角度出发的,这些分类并不是互斥的,也就是多个类型并存,有可能一个锁,同时属于两种类型,比如ReentrantLock既是互斥锁,又是可重入锁
乐观锁和悲观锁
1.为什么会诞生非互斥同步锁,互斥同步锁的劣势
① 互斥同步锁的劣势
阻塞和唤醒带来的性能劣势
永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的哪几个线程,将永远得不到执行
优先级反转
② 悲观锁
如果不锁住这个资源,别人就会来争抢,就会造成数据结果丢失,把数据锁住,让别人无法访问该数据,可以确保数据内容万无一失。
Java中悲观锁的实现是synchronized和Lock相关类
③ 乐观锁
认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象
在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的只有我自己在操作,那我就正常去修改数据
如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略
乐观锁的实现一般都是利用CAS算法来实现的
2.典型的例子
悲观锁:悲观锁的实现是synchronized和lock接口
乐观锁:原子类、并发容器等
Git:Git就是乐观锁的典型的例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败,如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库
数据库:
select for update就是悲观锁
用version控制数据库就是乐观锁
#添加一个字段lock_version,先查询这个更新语句的version:
select*from table
#然后
update set num = 2,version=version+1 where version=1 and id= 5;
#如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理
3.开销对比
悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
4.各种锁的使用场景
悲观锁:适合并发写入多的情况,适用于临界区持锁比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
临界区有IO操作
临界区代码复杂或者循环量大
临界区竞争非常激烈
乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅度提高
可重入锁和非可重入锁,以ReentrantLock为例(重点)
1.预定电影院座位
/*** 演示多线程预定电影院座位*/
public class CinemaBookSeat {private static ReentrantLock lock = new ReentrantLock();private static void bookSeat(){lock.lock();try {System.out.println(Thread.currentThread().getName()+"开始预定座位");Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+"完成预定座位");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public static void main(String[] args) {new Thread(()->bookSeat()).start();new Thread(()->bookSeat()).start();new Thread(()->bookSeat()).start();new Thread(()->bookSeat()).start();}
}
2.打印字符串
/*** 演示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() {@Overridepublic void run() {while (true) {try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}outputer.output("悟空");}}}).start();new Thread(new Runnable() {@Overridepublic void run() {while (true) {try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}outputer.output("大师兄");}}}).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();}}}
}
3.什么是可重入锁
可重入锁,简单来说就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。
在多线程并发编程里面,绝大部分锁都是可重入的,比如Synchronized、ReentrantLock等,但是也有不支持重入的锁,比如JDK8里面提供的读写锁StampedLock
好处:
- 避免死锁
- 提升封装性
public class RecursionDemo {private static ReentrantLock lock = new ReentrantLock();public static void accessResource() {lock.lock();try {System.out.println("已经对资源进行了处理");if (lock.getHoldCount() < 5) {System.out.println(lock.getHoldCount());accessResource();System.out.println(lock.getHoldCount());}} finally {lock.unlock();}}public static void main(String[] args) {accessResource();}
}
公平锁和非公平锁
1.什么是公平和非公平
公平指的是按照线程请求的顺序,来分配锁,非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
注意:非公平也同样不提倡"插队"行为,这里的非公平,指的是"在合适的时机"插队,而不是盲目的插队。
2.为什么要有公平锁
避免唤醒带来的空档期
3.公平的情况(以ReentrantLock为例)
如果在创建ReentrantLock对象时,参数填写为true,那么这个就是公平锁
假设线程1234是按顺调用lock()的
后续等待的线程会到wait queue里面,按照顺序依次执行
在线程1执行unlock()释放锁之后,由于此线程2的等待时间最久,所以线程2先得到执行,然后是线程3和线程4
4.不公平的情况(以ReentrantLock为例)
如果在线程1释放锁得时候,线程5恰好去执行lock()
由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没来得及获取到,因为获取需要时间)
线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认得公平策略,也就是"不公平"
5.演示公平和非公平的效果
/*** 演示公平和不公平两种情况*/
@SuppressWarnings("all")
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 Job(printQueue));}for (int i = 0; i < 10; i++) {thread[i].start();try {thread[i].sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}static class Job implements Runnable {PrintQueue printQueue;public Job(PrintQueue printQueue) {this.printQueue = printQueue;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始打印");printQueue.printJob(new Object());System.out.println(Thread.currentThread().getName() + "打印完毕");}}static class PrintQueue {private Lock queueLock = new ReentrantLock(true);//测试非共平时,参数为falsepublic void printJob(Object document) {queueLock.lock();try {int duration = new Random().nextInt(10) + 1;System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration + "秒");Thread.sleep(duration*1000);} catch (InterruptedException e) {e.printStackTrace();} finally {queueLock.unlock();}queueLock.lock();try {int duration = new Random().nextInt(10) + 1;System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration + "秒");Thread.sleep(duration*1000);} catch (InterruptedException e) {e.printStackTrace();} finally {queueLock.unlock();}}}
}
6.特例
针对treLock()方法,它不遵守设定的公平的规则
例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程在等待队列里了
7.对比公平和非公平的优缺点
共享锁和排他锁:以ReentrantReadWriteLock读写锁为例(重点)
1.什么是共享锁和排他锁
排他锁:又称为独占锁、独享锁
共享锁:又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
2.读写锁的作用
共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
在没有读写锁之前,假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
3.读写锁的规则
① 多个线程只申请读锁,都可以申请到
② 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
③ 如果有一个线程已经占用了写锁,则此时其他线程如果要申请写锁或读锁,则申请的线程会一直等待释放写锁
④ 简单总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)
4.ReentrantReadWriteLock具体用法
public class CinemaReadWrite {private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();public static void read() {readLock.lock();try {System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + "释放读锁");readLock.unlock();}}public static void write() {writeLock.lock();try {System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + "释放写锁");writeLock.unlock();}}public static void main(String[] args) {new Thread(() -> read(), "Thread1").start();new Thread(() -> read(), "Thread2").start();new Thread(() -> write(), "Thread3").start();new Thread(() -> write(), "Thread4").start();}
}
5.读锁和写锁的交互方式
非公平:假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取
两种策略:
策略1:
读可以插队,效率高,容易造成饥饿
策略2:
避免饥饿
策略的选择取决于具体锁的实现,ReentrantReadWriteLock的实现是选择了策略2,是很明智的
读锁插队策略:
- 公平锁:不允许插队
- 非公平锁
- 写锁可以随时插队
- 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队
自旋锁和阻塞锁
阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
如果同步代码块中的内容过于简单,状态转换的时间有可能比用户代码执行的时间还要长
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
如果无机器有多个处理器,能够让两个或者以上的线程同时并行执行,我们就可以让后面哪个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
而为了让当前线程"稍等一下",我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁
阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒
1.自旋锁缺点
如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
在自旋的过程中,一直消耗CPU,所以自旋锁的起始开销低于悲观锁,但是随着自旋的时间增长,开销也是线性增长的
2.原理
在java1.5版本及以上的并发框架java.util.concurrent的atmic包下的类基本都是自旋锁的实现
AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,甚至修改成功
自己写一个简单的自旋锁:
public class SpinLock {private AtomicReference<Thread> sign = new AtomicReference<>();public void lock() {Thread current = Thread.currentThread();while (!sign.compareAndSet(null, current)) {System.out.println("自旋获取失败,再次尝试");}}public void unlock() {Thread current = Thread.currentThread();sign.compareAndSet(current, null);}public static void main(String[] args) {SpinLock spinLock = new SpinLock();Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");spinLock.lock();System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了自旋锁");}}};Thread thread1 = new Thread(runnable);Thread thread2 = new Thread(runnable);thread1.start();thread2.start();}
}
3.自旋锁适用场景
① 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞的效率高
② 自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的
可中断锁:顾名思义,就是可以响应中断的锁
在java中,synchronized就不是可中断锁,而lock是可中断锁,因为tryLock(time)和lockInterruptibily都能响应中断
如果某一个线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁
锁优化
1.自旋锁和自适应
2.锁消除
3.锁粗化
4.写代码时如何优化锁和提高并发性能
① 缩小同步代码块
② 尽量不要锁住方法
③ 减少请求锁的次数
④ 避免人为制造"热点(某些数据是共享的使用它就需要加锁,故意的让加锁解锁增多)"
⑤ 锁中尽量不要再包含锁
⑥ 选择合适的锁类型或合适的工具类
java中的锁(悲观锁、乐观锁、可重入锁、不可重入锁、公平锁、非公平锁、自旋锁、阻塞锁...)相关推荐
- Java锁之公平和非公平锁
Java锁之公平和非公平锁 目录 公平锁和非公平锁概念 公平锁和非公平锁区别 ReentrantLock和synchronized是公平锁还是非公平锁? 1. 公平锁和非公平锁概念 公平锁:是指多个线 ...
- java并发编程(三十五)——公平与非公平锁实战
前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...
- 云阶月地,关锁千重(一.公平和非公平)
看到文章的标题是不是很诧异,一个搞技术的为什么要搞这么文艺的话题呢?标题说关锁千重,是不是很形象,我们在开发中的锁不也是多种多样么? Lock 既然之前说了锁千重,那锁到底有多少种,他们的分类又是怎么 ...
- 公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的
转载:https://www.jianshu.com/p/5104cd94dbe0 1.什么是公平锁与非公平锁 公平锁:公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁. 非公平锁:非 ...
- 乐观锁、悲观锁和公平、非公平
今天心情:我是一个程序员,现在已经走向了逼不得已通过写文章赚取流量来谋生的道路.可是现在流量惨淡,可是我并不惊慌.奥里给. 详细内容链接地址:https://zhuanlan.zhihu.com/p/ ...
- java公平索非公平锁_Java 并发编程中使用 ReentrantLock 替代 synchronized
Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功能.使用 ...
- java公平索非公平锁_java中的非公平锁不怕有的线程一直得不到执行吗
首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点, ...
- java -锁(公平、非公平锁、可重入锁【递归锁】、自旋锁)
1.公平锁.非公平锁 2.可重入锁(递归锁) 3.自旋锁 AtomicReference atomicReference = new AtomicReference();//原子引用线程 下面代码5秒 ...
- Java解决空引用_在java中检查空引用的乐观方法
本问题已经有最佳答案,请猛点这里访问. 任何Java精通/专家都能告诉我,在使用null来避免EDCOX1×0时,检查空引用或对象的乐观方式是什么? 在我的代码中,我有100多个字段,其中大部分是为了 ...
- java中的Attribute类_java培训技术ModelAttribute注解修饰POJO类型的入参
@RequestMapping("/testModelAttribute") //public String testModelAttribute(User user){ publ ...
最新文章
- ffmpeg转码速度控制方法
- 一键部署区块链环境 阿里云发布企业级BaaS服务
- DCMTK:转换DICOM彩色图像调色板的颜色
- 考研生物和计算机结合的专业,2020考研:生物医学工程,考研是考原专业还是跨专业考计算机好?...
- 怎么查看我的php版本,怎样查看php版本
- 2017.10.22 最多的方案 失败总结
- java 重定向到某个页面并弹出消息_前端面试100问之浏览器从输入URL到页面展示发生了什么...
- SLAM系统原理推导
- sqlmap命令详解(最全版本)
- linux环境安装的odac,net不安装Oracle11g客户端直接使用ODAC
- 干货|手把手教你写一个串口调试助手
- 【笔记】《iOS开发进阶-唐巧》
- 低配电脑不能运行大型游戏?试试AWS EC2
- 2022-03-清华管理学-清华大学-宁向东
- android 分享文件功能实现
- linux+unlock+10,unlock解锁工具 - 亚风软件站
- 传统网吧风光不再 玩家上座率不足50%
- 【数学】Baby Step,Giant Step
- 分析system_call中断处理过程
- Docker-镜像的优化
热门文章
- 『姑苏 · 踏青』:诗情未尽在苏杭
- DMIScope 1.00.005 注册版
- 软件架构师培训 深圳 北京
- 分布式与集群的区别究竟是什么?
- 安装VMware虚拟机
- 切片和切块 钻取 旋转(转)
- 十代服务器芯片组,【十代处理器主板芯片组个人整理汇总一览】包含总结和分析...
- 2017全国计算机ps版本,Adobe Photoshop v18.0.0 (PS CC 2017) 中文多语言版本 不断更新
- web项目电商网站开发实战(1)
- 在windows应用商店安装ubuntu子系统错误总结