并发编程(六)之显示锁Lock

    技术2022-07-10  160

    概述

    Lock是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。

    有了 synchronized 为什么还要 Lock?

    Java 程序是靠 synchronized 关键字实现锁功能的,使用 synchronized 关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。

    特性描述尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取,则成功获取并持有锁能被中断地获取锁与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放超时获取锁在指定的截至时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

    Lock的标准用法

    lock.lock(); try { ... } finally { lock.unlock(); } /** *类说明:使用Lock的范例 */ public class LockCase { private Lock lock = new ReentrantLock(); private int age = 100000;//初始100000 private static class TestThread extends Thread{ private LockCase lockCase; public TestThread(LockCase lockCase, String name) { super(name); this.lockCase = lockCase; } @Override public void run() { for(int i=0;i<100000;i++) {//递增100000 lockCase.test(); } System.out.println(Thread.currentThread().getName() +" age = "+lockCase.getAge()); } } public void test() { lock.lock(); try{ age++; }finally { lock.unlock(); } } public void test2() { lock.lock(); try { age--; } finally { lock.unlock(); } } public int getAge() { return age; } public static void main(String[] args) throws InterruptedException { LockCase lockCase = new LockCase(); Thread endThread = new TestThread(lockCase,"endThread"); endThread.start(); for(int i=0;i<100000;i++) {//递减100000 lockCase.test2(); } System.out.println(Thread.currentThread().getName() +" age = "+lockCase.getAge()); } }

    在 finally 块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在 try 块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

    Lock的常用API

    方法描述void lock()获取锁,调用该方法当前线程将会获取锁,当获取锁后,从该方法返回void lockInterruptibly()可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程boolean tryLock()尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取返回true,反之falseboolean tryLock(long time, TimeUnit unit)超时的获取锁,当前线程在一下3种情况下会返回: ①当前线程在超时时间内获得了锁 ②当前线程在超时时间内被中断 ③超时时间结束,返回falsevoid unlock()释放锁

    ReentrantLock

    锁的可重入

    简单地讲就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权”。而 synchronized 关键字隐式的支持重进入,比如一个 synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得 该锁。ReentrantLock 在调用 lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

    公平和非公平锁

    如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。 ReentrantLock 提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A持有一个锁,并且线程 B 请求这个锁。由于这个锁已被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样 的情况是一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早地获得了锁,并且吞吐量也获得了提高。

    ReentrantReadWriteLock

    读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

    在没有读写锁支持的(Java 5 之前)时候,如果需要完成上述工作就要使用Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠 synchronized 关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。

    改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

    一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量,ReentrantReadWriteLock 其实实现的是 ReadWriteLock 接口。

    演示商品读写

    商品实体类

    public class GoodsInfo { private final String name; //总销售额 private double totalMoney; //库存数 private int storeNumber; public GoodsInfo(String name, int totalMoney, int storeNumber) { this.name = name; this.totalMoney = totalMoney; this.storeNumber = storeNumber; } public double getTotalMoney() { return totalMoney; } public int getStoreNumber() { return storeNumber; } //修改数量 public void changeNumber(int sellNumber){ this.totalMoney += sellNumber*25; this.storeNumber -= sellNumber; } }

    商品服务接口

    public interface GoodsService { //获得商品的信息 public GoodsInfo getNum(); //设置商品的数量 public void setNum(int number); }

    使用synchronized

    /** *类说明:用内置锁来实现商品服务接口 */ public class UseSyn implements GoodsService { private GoodsInfo goodsInfo; public UseSyn(GoodsInfo goodsInfo) { this.goodsInfo = goodsInfo; } @Override public synchronized GoodsInfo getNum() { SleepTools.ms(5); return this.goodsInfo; } @Override public synchronized void setNum(int number) { SleepTools.ms(5); goodsInfo.changeNumber(number); } }

    测试

    /** *类说明:对商品进行业务的应用 */ public class BusiApp { //读写线程的比例 static final int readWriteRatio = 10; //最少线程数 static final int minthreadCount = 3; //读操作 private static class GetThread implements Runnable{ private GoodsService goodsService; public GetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { long start = System.currentTimeMillis(); for(int i=0;i<100;i++){//操作100次 goodsService.getNum(); } System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:" +(System.currentTimeMillis()-start)+"ms"); } } //写操做 private static class SetThread implements Runnable{ private GoodsService goodsService; public SetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { long start = System.currentTimeMillis(); Random r = new Random(); for(int i=0;i<10;i++){//操作10次 SleepTools.ms(50); goodsService.setNum(r.nextInt(10)); } System.out.println(Thread.currentThread().getName() +"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------"); } } public static void main(String[] args) throws InterruptedException { GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000); GoodsService goodsService = new UseSyn(goodsInfo); for(int i = 0;i<minthreadCount;i++){ Thread setT = new Thread(new SetThread(goodsService)); for(int j=0;j<readWriteRatio;j++) { Thread getT = new Thread(new GetThread(goodsService)); getT.start(); } SleepTools.ms(100); setT.start(); } } }

    其中一次打印如下

    Thread-13读取商品数据耗时:5505ms Thread-31读取商品数据耗时:8677ms Thread-32读取商品数据耗时:8848ms Thread-11写商品数据耗时:10721ms--------- Thread-0写商品数据耗时:11768ms--------- Thread-7读取商品数据耗时:12282ms ......

    使用ReentrantReadWriteLock

    /** *类说明:对商品进行业务的应用 */ public class BusiApp { //读写线程的比例 static final int readWriteRatio = 10; //最少线程数 static final int minthreadCount = 3; //读操作 private static class GetThread implements Runnable{ private GoodsService goodsService; public GetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { long start = System.currentTimeMillis(); for(int i=0;i<100;i++){//操作100次 goodsService.getNum(); } System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:" +(System.currentTimeMillis()-start)+"ms"); } } //写操做 private static class SetThread implements Runnable{ private GoodsService goodsService; public SetThread(GoodsService goodsService) { this.goodsService = goodsService; } @Override public void run() { long start = System.currentTimeMillis(); Random r = new Random(); for(int i=0;i<10;i++){//操作10次 SleepTools.ms(50); goodsService.setNum(r.nextInt(10)); } System.out.println(Thread.currentThread().getName() +"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------"); } } public static void main(String[] args) throws InterruptedException { GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000); GoodsService goodsService = new UseRwLock(goodsInfo); for(int i = 0;i<minthreadCount;i++){ Thread setT = new Thread(new SetThread(goodsService)); for(int j=0;j<readWriteRatio;j++) { Thread getT = new Thread(new GetThread(goodsService)); getT.start(); } SleepTools.ms(100); setT.start(); } } }

    其中一次打印如下

    Thread-0写商品数据耗时:617ms--------- Thread-10读取商品数据耗时:780ms Thread-9读取商品数据耗时:780ms Thread-2读取商品数据耗时:781ms Thread-1读取商品数据耗时:781ms Thread-8读取商品数据耗时:780ms Thread-3读取商品数据耗时:780ms Thread-6读取商品数据耗时:780ms Thread-5读取商品数据耗时:780ms ......

    写独占,读共享,读写互斥

    Condition接口

    任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、wait(long timeout)、notify()以及 notifyAll()方法,这些方法与synchronized 同步关键字配合,可以实现等待/通知模式。Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。

    通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比如下

    对比项Object Monitor MethodsCondition前置条件获取对象的锁1.调用Lock.lock()获取 2.调用Lock.newCondition()获取Condition对象调用方式直接调用,如:object.wait()直接调用,如:condition.await()等待队列个数一个多个当前线程释放锁并进入等待状态支持支持当前线程释放锁并进入等待状态,在等待状态中不响应终端不支持支持当前线程释放锁并进入超时等待状态支持支持当前线程释放锁并进入等待状态到将来的某个时间不支持支持唤醒等待队列中的一个线程支持支持唤醒等待队列中的全部线程支持支持

    Condition常用方法

    方法描述void await() throws InterruptedException当线程进入等待状态直到被通知(signal)或中断,当前线程将进入运行状态且从await()方法返回的情况,包括: 其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒 1. 其他线程(调用interrupt()方法)中断当前线程 2. 如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁void awaitUninterruptibly()当前线程进入等待状态直到被通知,从方法名称上可以看出该方法对中断不敏感long awaitNanos(long nanosTimeout) throws InterruptedException当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余的时间,如果在nanosTimeout纳秒之前被唤醒,那么返回值就是(nanosTimeout - 实际耗时)。如果返回值是0或者负数,那么可以认定已经超时了boolean awaitUntil(Date deadline) throws InterruptedException当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回falsevoid signal()唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁void signalAll()唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁

    Condition使用范式

    Lock lock = new ReentrantLock(); //获取一个Condition必须通过Lock的newCondition()方法 Condition condition = lock.newCondition(); public void conditionWait() throws InterruptedException { lock.lock(); try { condition.await(); } finally { lock.unlock(); } } public void conditionSignal() throws InterruptedException { lock.lock(); try { condition.signal(); } finally { lock.unlock(); } }

    如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

    了解Condition的实现

    Processed: 0.015, SQL: 9