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中的锁(悲观锁、乐观锁、可重入锁、不可重入锁、公平锁、非公平锁、自旋锁、阻塞锁...)相关推荐

  1. Java锁之公平和非公平锁

    Java锁之公平和非公平锁 目录 公平锁和非公平锁概念 公平锁和非公平锁区别 ReentrantLock和synchronized是公平锁还是非公平锁? 1. 公平锁和非公平锁概念 公平锁:是指多个线 ...

  2. java并发编程(三十五)——公平与非公平锁实战

    前言 在 java并发编程(十六)--锁的七大分类及特点 一文中我们对锁有各个维度的分类,其中有一个维度是公平/非公平,本文我们来探讨下公平与非公平锁. 公平|非公平 首先,我们来看下什么是公平锁和非 ...

  3. 云阶月地,关锁千重(一.公平和非公平)

    看到文章的标题是不是很诧异,一个搞技术的为什么要搞这么文艺的话题呢?标题说关锁千重,是不是很形象,我们在开发中的锁不也是多种多样么? Lock 既然之前说了锁千重,那锁到底有多少种,他们的分类又是怎么 ...

  4. 公平锁和非公平锁-ReentrantLock是如何实现公平、非公平的

    转载:https://www.jianshu.com/p/5104cd94dbe0 1.什么是公平锁与非公平锁 公平锁:公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁. 非公平锁:非 ...

  5. 乐观锁、悲观锁和公平、非公平

    今天心情:我是一个程序员,现在已经走向了逼不得已通过写文章赚取流量来谋生的道路.可是现在流量惨淡,可是我并不惊慌.奥里给. 详细内容链接地址:https://zhuanlan.zhihu.com/p/ ...

  6. java公平索非公平锁_Java 并发编程中使用 ReentrantLock 替代 synchronized

    Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功能.使用 ...

  7. java公平索非公平锁_java中的非公平锁不怕有的线程一直得不到执行吗

    首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点, ...

  8. java -锁(公平、非公平锁、可重入锁【递归锁】、自旋锁)

    1.公平锁.非公平锁 2.可重入锁(递归锁) 3.自旋锁 AtomicReference atomicReference = new AtomicReference();//原子引用线程 下面代码5秒 ...

  9. Java解决空引用_在java中检查空引用的乐观方法

    本问题已经有最佳答案,请猛点这里访问. 任何Java精通/专家都能告诉我,在使用null来避免EDCOX1×0时,检查空引用或对象的乐观方式是什么? 在我的代码中,我有100多个字段,其中大部分是为了 ...

  10. java中的Attribute类_java培训技术ModelAttribute注解修饰POJO类型的入参

    @RequestMapping("/testModelAttribute") //public String testModelAttribute(User user){ publ ...

最新文章

  1. ffmpeg转码速度控制方法
  2. 一键部署区块链环境 阿里云发布企业级BaaS服务
  3. DCMTK:转换DICOM彩色图像调色板的颜色
  4. 考研生物和计算机结合的专业,2020考研:生物医学工程,考研是考原专业还是跨专业考计算机好?...
  5. 怎么查看我的php版本,怎样查看php版本
  6. 2017.10.22 最多的方案 失败总结
  7. java 重定向到某个页面并弹出消息_前端面试100问之浏览器从输入URL到页面展示发生了什么...
  8. SLAM系统原理推导
  9. sqlmap命令详解(最全版本)
  10. linux环境安装的odac,net不安装Oracle11g客户端直接使用ODAC
  11. 干货|手把手教你写一个串口调试助手
  12. 【笔记】《iOS开发进阶-唐巧》
  13. 低配电脑不能运行大型游戏?试试AWS EC2
  14. 2022-03-清华管理学-清华大学-宁向东
  15. android 分享文件功能实现
  16. linux+unlock+10,unlock解锁工具 - 亚风软件站
  17. 传统网吧风光不再 玩家上座率不足50%
  18. 【数学】Baby Step,Giant Step
  19. 分析system_call中断处理过程
  20. Docker-镜像的优化

热门文章

  1. 『姑苏 · 踏青』:诗情未尽在苏杭
  2. DMIScope 1.00.005 注册版
  3. 软件架构师培训  深圳  北京
  4. 分布式与集群的区别究竟是什么?
  5. 安装VMware虚拟机
  6. 切片和切块 钻取 旋转(转)
  7. 十代服务器芯片组,【十代处理器主板芯片组个人整理汇总一览】包含总结和分析...
  8. 2017全国计算机ps版本,Adobe Photoshop v18.0.0 (PS CC 2017) 中文多语言版本 不断更新
  9. web项目电商网站开发实战(1)
  10. 在windows应用商店安装ubuntu子系统错误总结