zoukankan      html  css  js  c++  java
  • Java多线程核心技术(四)Lock的使用

    【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

    本文主要介绍使用Java5中Lock对象也能实现同步的效果,而且在使用上更加方便。

    本文着重掌握如下2个知识点:

    1. ReentrantLock 类的使用。
    2. ReentrantReadWriteLock 类的使用。

    1. 使用ReentrantLock 类

    在Java多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在JDK1.5中新增加了 ReentrantLock 类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。

    1.1 使用ReentrantLock实现同步

    调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。

    下面是初步的程序示例:

    public class Demo {
        private Lock lock = new ReentrantLock();
    
        public void test(){
            lock.lock();
            for (int i= 0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+" - "+i);
            }
            lock.unlock();
        }
    
        public static void main(String[] args) {
            Demo demo =  new Demo();
            for (int i = 0;i<5;i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        demo.test();
                    }
                }).start();
            }
        }
    }
    

    运行结果:

    Thread-0 - 0
    Thread-0 - 1
    Thread-0 - 2
    Thread-0 - 3
    Thread-0 - 4
    Thread-1 - 0
    Thread-1 - 1
    Thread-1 - 2
    Thread-1 - 3
    Thread-1 - 4
    Thread-2 - 0
    Thread-2 - 1
    Thread-2 - 2
    Thread-2 - 3
    Thread-2 - 4
    Thread-3 - 0
    Thread-3 - 1
    Thread-3 - 2
    Thread-3 - 3
    Thread-3 - 4
    Thread-4 - 0
    Thread-4 - 1
    Thread-4 - 2
    Thread-4 - 3
    Thread-4 - 4
    

    从运行的结果来看,当前线程打印完毕后将锁进行释放,其他线程才可以继续打印。

    1.1.2 锁住类的所有实例对象

    上面的示例是所有线程调用一个ReentrantLock实例对象实现同步,如果每个线程都调用各自ReentrantLock实例对象的同一段代码呢?

    示例代码:

    public class MyService implements Runnable{
        private ReentrantLock lock = new ReentrantLock();
    
        public void method(){
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"锁定...");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+"解锁。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
    
        }
    
        public static void main(String[] args) {
            new Thread(new MyService()).start();
            new Thread(new MyService()).start();
            new Thread(new MyService()).start();
        }
    
        @Override
        public void run() {
            method();
        }
    }
    

    运行结果:

    Thread-0锁定...
    Thread-2锁定...
    Thread-1锁定...
    Thread-2解锁。
    Thread-0解锁。
    Thread-1解锁。
    

    从运行结果来看,并没有实现想要的方法同步的效果。如果我们想要实现类似synchronized(class),也就是给Class类上锁,可以把 ReentrantLock 声明为 static 静态变量。

    示例代码:

    public class MyService implements Runnable{
        private static ReentrantLock lock = new ReentrantLock();
    
        public void method(){
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"锁定...");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+"解锁。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
    
        }
    
        public static void main(String[] args) {
            new Thread(new MyService()).start();
            new Thread(new MyService()).start();
            new Thread(new MyService()).start();
        }
    
        @Override
        public void run() {
            method();
        }
    }
    

    运行结果:

    Thread-0锁定...
    Thread-0解锁。
    Thread-1锁定...
    Thread-1解锁。
    Thread-2锁定...
    Thread-2解锁。
    

    从运行结果来看,成功实现了预期的结果。

    1.2 使用Condition 实现等待 / 通知

    关键字 synchronized 与 wait() 和 notify() / notifyAll() 方法相结合可以实现等待 / 通知模式,类 ReentrantLock 也可以实现同样的功能,但需要借助于 Condition(即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

    在使用 notify() / notifyAll() 方法进行通知时,被通知的线程却是由JVM随机选择的。但使用 ReentrantLock 结合 Condition 类是可以实现前面介绍过的“选择性通知”,这个功能是非常重要的,而且在 Condition 类中是默认提供的。

    示例代码:

    public class Demo {
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public void await() {
            try {
                lock.lock();
                System.out.println("开始等待:" + System.currentTimeMillis());
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void signal() {
            try {
                lock.lock();
                System.out.println("结束等待:" + System.currentTimeMillis());
                condition.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Demo demo = new Demo();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.await();
                }
            }).start();
            Thread.sleep(3000);
            demo.signal();
        }
    }
    

    运行结果:

    开始等待:1537352883839
    结束等待:1537352886839
    

    成功实现等待 / 通知模式。

    在Object中,有wait() 、wait(long)、notify()、notifyAll()方法。

    在Condition类中,有 await()、await(long)、signal()、signalAll()方法。

    1.3使用多个Condition实现通知部分线程

    示例代码:

    public class Demo {
        private Lock lock = new ReentrantLock();
        private Condition conditionA = lock.newCondition();
        private Condition conditionB = lock.newCondition();
    
        public void awaitA() {
            try {
                lock.lock();
                System.out.println("A开始等待:" + System.currentTimeMillis());
                conditionA.await();
                System.out.println("A结束等待:" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void awaitB() {
            try {
                lock.lock();
                System.out.println("B开始等待:" + System.currentTimeMillis());
                conditionB.await();
                System.out.println("B结束等待:" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void signalAll_B() {
            try {
                lock.lock();
                conditionB.signalAll();
            } finally {
                lock.unlock();
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Demo demo = new Demo();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.awaitA();
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.awaitB();
                }
            }).start();
            Thread.sleep(3000);
            demo.signalAll_B();
        }
    }
    

    运行结果:

    A开始等待:1537354021740
    B开始等待:1537354021741
    B结束等待:1537354024738
    

    可以看到,只有B线程被唤醒了。

    通过此实验可知,使用 ReentrantLock 对象可以唤醒指定种类的线程,这是控制部分线程行为的方便行为。

    1.4 公平锁和非公平锁

    锁Lock分为”公平锁“和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加载的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

    设置公平锁:

    Lock lock = new ReentrantLock(true);
    

    使用ReentrantLock类设置公平锁只需要在构造时传入boolean参数即可。默认false。需要明白的是,即使设置为true也不能保证百分百公平。

    总结:

    公平锁:先去判断等待队列是否为空,也就是是否有线程在等待,没有就去获取锁,否则把自己加入等待队列。

    非公平锁:先去尝试获取锁,如果失败再加入到等待队列。

    1.5 方法getHoldCount()、getQueryLength()和getWaitQueryLength()

    1.方法getHoldCount() 的作用是查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数。

    示例代码:

    public class Service {
        private ReentrantLock lock = new ReentrantLock();
    
        public void method() {
            try {
                lock.lock();
                System.out.println("getHoldCount() " + lock.getHoldCount());
                method2();
            } finally {
                lock.unlock();
            }
        }
    
        public void method2() {
            try {
                lock.lock();
                System.out.println("getHoldCount() " + lock.getHoldCount());
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            Service service = new Service();
            service.method();
        }
    
    }
    

    运行结果:

    getHoldCount() 1
    getHoldCount() 2
    

    2.方法getQueryLength() 的作用是返回正等待获取此锁定的线程估计数。比如有5个方法,1个线程首先执行 await()方法,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待 lock 的释放。

    示例代码:

    public class Service {
        private ReentrantLock lock = new ReentrantLock();
    
        public void method() {
            try {
                lock.lock();
                System.out.println("Name: " + Thread.currentThread().getName());
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            Service service = new Service();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    service.method();
                }
            };
            for (int i = 0; i < 5; i++) {
                new Thread(runnable).start();
            }
            Thread.sleep(1000);
            ReentrantLock lock = service.getLock();
            System.out.println("有多少线程在等待:"+lock.getQueueLength());
        }
    
        private ReentrantLock getLock() {
            return lock;
        }
    
    }
    

    运行结果:

    Name: Thread-1
    有多少线程在等待:4
    

    3.方法getWaitQueryLength(condition) 的作用是返回等待与此锁定相关的给定条件Condition的线程估计数,比如有5个线程,每个线程都执行了同一个condition 对象的await() 方法,则调用 getWaitQueryLength(condition) 方法时返回的int值是5。

    示例代码:

    public class Service {
        private ReentrantLock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public void method() {
            try {
                lock.lock();
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void notifyMethod() {
            try {
                lock.lock();
                System.out.println("等待condition的线程数" + lock.getWaitQueueLength(condition));
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Service service = new Service();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    service.method();
                }
            };
            for (int i = 0; i < 5; i++) {
                new Thread(runnable).start();
            }
            Thread.sleep(1000);
            service.notifyMethod();
        }
        
    }
    

    运行结果:

    等待condition的线程数5
    

    1.6 方法hasQueuedThread()、hasQueuedThreads()和hasWaiters()

    【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

    1.方法 boolean hasQueuedThread(Thread thread) 的作用是查询指定的线程是否正在等待获取此锁定。

    2.方法 boolean hasQueuedThreads() 的作用是查询是否有线程正在等待获取此锁定。

    1、2示例代码:

    public class Service {
        private ReentrantLock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public void waitMethod(){
            try {
                lock.lock();
                Thread.sleep(Integer.MAX_VALUE);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    
        public ReentrantLock getLock(){
            return lock;
        }
    
        public static void main(String[] args) throws InterruptedException {
            final Service service = new Service();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    service.waitMethod();
                }
            };
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
            Thread.sleep(1000);
            ReentrantLock lock = service.getLock();
            System.out.println(lock.hasQueuedThreads());
            System.out.println(lock.hasQueuedThread(thread1));
            System.out.println(lock.hasQueuedThread(thread2));
        }
    
    }
    

    运行结果:

    true
    false
    true
    

    3.方法 boolean hasWaiters(Condition condition) 的作用是查询是否有线程正在等待与此锁定有关的 condition 条件。

    示例代码:

    public class Service {
        private ReentrantLock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();
    
        public void waitMethod(){
            try {
                lock.lock();
                condition.await();
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    
        public void notifyMethod(){
            try {
                lock.lock();
                System.out.println("有没有线程正在等待 condition ?" + lock.hasWaiters(condition) + " 线程数是多少?" + lock.getWaitQueueLength(condition));
                condition.signalAll();
            }finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Service service = new Service();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    service.waitMethod();
                }
            };
            for (int i = 0; i < 10; i++) {
                new Thread(runnable).start();
            }
            Thread.sleep(2000);
            service.notifyMethod();
        }
    
    }
    

    运行结果:

    有没有线程正在等待 condition ?true 线程数是多少?10
    

    1.7 方法isFair()、isHeldByCurrentThread()和isLocked()

    1. 方法boolean isFair() 的作用是判断是不是公平锁。
    2. 方法boolean isHeldByCurrentThread() 的作用是查询当前线程是否保持此锁定。
    3. 方法boolean isLocked() 的作用是查询此锁定是否由任意线程保持。

    更改上面的部分代码:

    System.out.println(lock.isHeldByCurrentThread());
    System.out.println(lock.isLocked());
    lock.lock();
    System.out.println(lock.isLocked());
    System.out.println(lock.isHeldByCurrentThread());
    

    运行结果:

    false
    false
    true
    true
    

    1.8 方法lockInterruptibly()、tryLock()和tryLock(long timeout, TimeUnit unit)

    下面的三个方法都是对lock.lock()方法的另一种变形:

    1. 方法void lockInterruptibly()的作用是:如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。

      而使用 lock() 方法,即使线程被中断(调用thread.interrupt()方法),也不会出现异常。

    2. 方法boolean tryLock() 的作用是,仅在未被另一个线程保持的情况下,才获取该锁定。

      假设有两个线程同时调用同一个lock对象的tryLock()方法,那么除了第一个获得锁(返回true),其它都获取不到锁(返回false)。

    3. 方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

    1.9 方法 condition.awaitUninterruptibly()的使用

    前面讲到,执行condition.await()方法后,线程进入等待状态,如果这时线程被中断(调用thread.interrupt()方法)则会抛出异常。而使用 condition.awaitUninterruptibly() 方法代替 condition.await() 方法则不会抛出异常。

    1.10 方法 condition.awaitUntil(Date deadline)的使用

    使用方法 condition.awaitUntil(Date deadline) 可以代替 await(long time, TimeUnit unit) 方法进行线程等待,该方法在等待时间到达前是可以被提前唤醒的。

    1.11 使用Condition实现顺序执行

    使用Condition对象可以对线程执行的业务进行排序规划。

    示例代码:

    public class DThread{
        volatile private static int nextPrintWho = 1;
        private static ReentrantLock lock = new ReentrantLock();
        final private static Condition conditionA = lock.newCondition();
        final private static Condition conditionB = lock.newCondition();
        final private static Condition conditionC = lock.newCondition();
        public static void main(String[] args) {
            Thread threadA = new Thread(){
                @Override
                public void run() {
                    try {
                        lock.lock();
                        while (nextPrintWho != 1){
                            conditionA.await();
                        }
                        for (int i = 0;i<3;i++){
                            System.out.println("ThreadA "+(i+1));
                        }
                        nextPrintWho = 2;
                        conditionB.signalAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            };
            Thread threadB = new Thread(){
                @Override
                public void run() {
                    try {
                        lock.lock();
                        while (nextPrintWho != 2){
                            conditionA.await();
                        }
                        for (int i = 0;i<3;i++){
                            System.out.println("ThreadB "+(i+1));
                        }
                        nextPrintWho = 3;
                        conditionB.signalAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            };
            Thread threadC = new Thread(){
                @Override
                public void run() {
                    try {
                        lock.lock();
                        while (nextPrintWho != 3){
                            conditionA.await();
                        }
                        for (int i = 0;i<3;i++){
                            System.out.println("ThreadC "+(i+1));
                        }
                        nextPrintWho = 1;
                        conditionB.signalAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }
                }
            };
            for (int i= 0;i<5;i++){
                new Thread(threadA).start();
                new Thread(threadB).start();
                new Thread(threadC).start();
            }
        }
    
    }
    

    打印结果:

    ThreadA 1
    ThreadA 2
    ThreadA 3
    ThreadB 1
    ThreadB 2
    ThreadB 3
    ThreadC 1
    ThreadC 2
    ThreadC 3
    ....
    

    2.使用ReentrantReadWriteLock类

    类 ReentrantLock 具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock() 方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写 ReentrantReadWriteLock 来提升该方法的代码运行速度。
    读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有线程 Thread进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread可以同时进行读取操作但是同一时刻只允许一个 Thread 进行写入操作。

    总结起来就是:读读共享,写写互斥,读写互斥,写读互斥。

    声明读写锁:

    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    

    获取读锁:

    lock.readLock().lock();
    

    获取写锁:

    lock.writeLock().lock();
    

    3.文末总结

    学习完本文完全可以使用Lock对象将 synchronized关键字替换掉,而且其具有的独特功能也是 synchronized 所不具有的。在学习并发时,Lock是synchronized关键字的进阶,掌握Lock有助于学习并发包中源代码的实现原理,在并发包中大量的类使用了Lock 接口作为同步的处理方式。

    参考

    《Java多线程编程核心技术》高洪岩著

    扩展

    Java多线程编程核心技术(一)Java多线程技能

    Java多线程编程核心技术(二)对象及变量的并发访问

    Java多线程编程核心技术(三)多线程通信

    Java多线程核心技术(五)单例模式与多线程

    Java多线程核心技术(六)线程组与线程异常

    版权声明

    【本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

  • 相关阅读:
    Java实现 蓝桥杯 算法提高 特等奖学金(暴力)
    Java实现 蓝桥杯 算法提高 特等奖学金(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 套正方形(暴力)
    Java实现 蓝桥杯 算法提高 套正方形(暴力)
    第一届云原生应用大赛火热报名中! helm install “一键安装”应用触手可及!
    云原生时代,2个方案轻松加速百万级镜像
    Knative 基本功能深入剖析:Knative Serving 自动扩缩容 Autoscaler
  • 原文地址:https://www.cnblogs.com/onblog/p/13043378.html
Copyright © 2011-2022 走看看