首页 资讯 社群 我的社区 搜索

Java 多线程核心技术 (四):Lock 的使用

yimo~
2019-09-06 16:43:48

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

  • ReentrantLock 类的使用。
  • 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()

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() 方法),也不会出现异常。

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

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

  1. 方法 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 接口作为同步的处理方式。

 

用户评论