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)所有,若是转载请务必保留本段原创声明,违者必究。若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!】

  • 相关阅读:
    打印日志宏定义
    数据库读写操作
    SQL语句组成
    MySQL数据库的使用
    ubuntu下解决MySQL 1045 error
    css样式优先级
    redis
    dubbo
    maven
    Mybatis笔记
  • 原文地址:https://www.cnblogs.com/onblog/p/13043378.html
Copyright © 2011-2022 走看看