Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信

前几篇:

Java多线程编程-(1)-线程安全和锁Synchronized概念

Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性

Java多线程编程-(3)-线程本地ThreadLocal的介绍与使用

Java多线程编程-(4)-线程间通信机制的介绍与使用

在《Java多线程编程-(4)-线程间通信机制的介绍与使用》已经学习了,可以使用方法wait/notify 结合同步关键字synchronized实现同步和线程间通信,下边介绍一种更为方便的方式实现同步和线程间通信的效果,那就是Lock对象。

Lock对象简介

这里为什么说Lock对象哪?Lock其实是一个接口,在JDK1.5以后开始提供,其实现类常用的有ReentrantLock,这里所说的Lock对象即是只Lock接口的实现类,为了方便记忆或理解,都简称为Lock对象。

我们知道synchronized关键字可以实现线程间的同步互斥,从JDK1.5开始新增的ReentrantLock类能够达到同样的效果,并且在此基础上还扩展了很多实用的功能,比使用synchronized更佳的灵活。

ReentrantLock的另一个称呼就是“重入锁”,Reentrant的英文释义为:重入。

何为重入锁,前几篇在学习synchronized的时候,也谈到了重入锁,“一个对象一把锁,多个对象多把锁”,可重入锁的概念就是:自己可以获取自己的内部锁。

ReentrantLock实现了Lock中的接口,继承关系和方法属性如下:

Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信_第1张图片

下边,就开始一起学习一下ReentrantLock对象。

使用ReentrantLock实现线程同步

public class Run {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        //lambda写法
        new Thread(() -> runMethod(lock), "thread1").start();
        new Thread(() -> runMethod(lock), "thread2").start();
        new Thread(() -> runMethod(lock), "thread3").start();
        new Thread(() -> runMethod(lock), "thread4").start();
        //常规写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                runMethod(lock);
            }
        }, "thread5").start();
    }

    private static void runMethod(Lock lock) {
        lock.lock();
        for (int i = 1; i <= 5; i++) {
            System.out.println("ThreadName:" + Thread.currentThread().getName() + (" i=" + i));
        }
        System.out.println();
        lock.unlock();
    }
}

运行结果:

ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread1 i=4
ThreadName:thread1 i=5

ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread2 i=4
ThreadName:thread2 i=5

ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
ThreadName:thread3 i=4
ThreadName:thread3 i=5

ThreadName:thread4 i=1
ThreadName:thread4 i=2
ThreadName:thread4 i=3
ThreadName:thread4 i=4
ThreadName:thread4 i=5

ThreadName:thread5 i=1
ThreadName:thread5 i=2
ThreadName:thread5 i=3
ThreadName:thread5 i=4
ThreadName:thread5 i=5

可以看出,当前线程打印完毕之后释放锁,其他线程才可以获取锁然后进行打印。线程打印的数据是分组打印的,这是因为当前线程已经持有锁,在当前线程打印完之后才会释放锁,但线程之间打印的顺序是随机的。

为了进一步说明使用ReentrantLock可以实现线程之间同步,测试代码如下:

public class Run {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> runMethod(lock, 0), "thread1").start();
        new Thread(() -> runMethod(lock, 5000), "thread2").start();
        new Thread(() -> runMethod(lock, 1000), "thread3").start();
        new Thread(() -> runMethod(lock, 5000), "thread4").start();
        new Thread(() -> runMethod(lock, 1000), "thread5").start();
    }

    private static void runMethod(Lock lock, long sleepTime) {
        lock.lock();
        try {
            Thread.sleep(sleepTime);
            System.out.println("ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

ThreadName:thread1
ThreadName:thread2
ThreadName:thread3
ThreadName:thread4
ThreadName:thread5

可以看出,在sleep指定的时间内,当调用了lock.lock()方法线程就持有了”对象监视器”,其他线程只能等待锁被释放后再次争抢,效果和使用synchronized关键字是一样的。

使用Lock对象实现线程间通信

上述,已经大致看了一下如何使用ReentrantLock实现线程之间的同步,下边再看一下ReentrantLock是如何实现线程间通信的。

在前文中我们已经知道可以使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition对象进行实现的。

Condition的创建方式如下:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

Condition按字面意思理解就是条件,当然,我们也可以将其认为是条件进行使用,这样的话我们可以通过上述的代码创建多个Condition条件,我们就可以根据不同的条件来控制现成的等待和通知。而我们还知道,在使用关键字synchronized与wait()方法和notify()方式结合实现线程间通信的时候,notify/notifyAll的通知等待的线程时是随机的,显然使用Condition相对灵活很多,可以实现”选择性通知”。

这是因为,synchronized关键字相当于整个Lock对象只有一个单一的Condition对象,所有的线程都注册到这个对象上。线程开始notifAll的时候,需要通知所有等待的线程,让他们开始竞争获得锁对象,没有选择权,这种方式相对于Condition条件的方式在效率上肯定Condition较高一些。

下边,我们首先看一个实例。

使用Lock对象和Condition实现等待/通知实例

主要方法对比如下:

(1)Object的wait()方法相当于Condition类中的await()方法;
(2)Object的notify()方法相当于Condition类中的signal()方法;
(3)Object的notifyAll()方法相当于Condition类中的signalAll()方法;

首先,使用Lock的时候,和《Java多线程编程-(4)-线程间通信机制的介绍与使用》介绍的一样,都需要先获取锁。

示例代码如下:

public class LockConditionDemo {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        //使用同一个LockConditionDemo对象,使得lock、condition一样
        LockConditionDemo demo = new LockConditionDemo();
        new Thread(() -> demo.await(), "thread1").start();
        Thread.sleep(3000);
        new Thread(() -> demo.signal(), "thread2").start();
    }

    private void await() {
        try {
            lock.lock();
            System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
            condition.await();
            System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void signal() {
        lock.lock();
        System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
        condition.signal();
        lock.unlock();
    }
}

运行结果:

开始等待await! ThreadName:thread1
发送通知signal! ThreadName:thread2
等待await结束! ThreadName:thread1

可以看出结果正确执行!

使用Lock对象和多个Condition实现等待/通知实例

示例代码如下:

public class LockConditionDemo {

    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        LockConditionDemo demo = new LockConditionDemo();

        new Thread(() -> demo.await(demo.conditionA), "thread1_conditionA").start();
        new Thread(() -> demo.await(demo.conditionB), "thread2_conditionB").start();
        new Thread(() -> demo.signal(demo.conditionA), "thread3_conditionA").start();
        System.out.println("稍等5秒再通知其他的线程!");
        Thread.sleep(5000);
        new Thread(() -> demo.signal(demo.conditionB), "thread4_conditionB").start();

    }

    private void await(Condition condition) {
        try {
            lock.lock();
            System.out.println("开始等待await! ThreadName:" + Thread.currentThread().getName());
            condition.await();
            System.out.println("等待await结束! ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void signal(Condition condition) {
        lock.lock();
        System.out.println("发送通知signal! ThreadName:" + Thread.currentThread().getName());
        condition.signal();
        lock.unlock();
    }
}

运行结果:

开始等待await! ThreadName:thread1_conditionA
开始等待await! ThreadName:thread2_conditionB
发送通知signal! ThreadName:thread3_conditionA
等待await结束! ThreadName:thread1_conditionA
稍等5秒再通知其他的线程!
发送通知signal! ThreadName:thread4_conditionB
等待await结束! ThreadName:thread2_conditionB

可以看出实现了分别通知。因此,我们可以使用Condition进行分组,可以单独的通知某一个分组,另外还可以使用signalAll()方法实现通知某一个分组的所有等待的线程。

公平锁和非公平锁

概念很好理解,公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,即先进先出,那么他就是公平的;非公平是一种抢占机制,是随机获得锁,并不是先来的一定能先得到锁,结果就是不公平的。

ReentrantLock提供了一个构造方法,可以很简单的实现公平锁或非公平锁,源代码构造函数如下:

public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

参数:fair为true表示是公平锁,反之为非公平锁,这里不再写代码测试。

ReentrantLock的其他方法

ReentrantLock源代码结构如下:

Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信_第2张图片

方法很简单,看到名称就可以想到作用是什么,挑一些简单介绍一下:

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

(2)getQueueLength()方法:返回正等待获取此锁定的线程估计数目;

(3)isFair()方法:判断是不是公平锁;

使用ReentrantReadWriteLock实现并发

上述的类ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock()之后的任务。

类似于我们集合中有同步类容器并发类容器,HashTable(HashTable几乎可以等价于HashMap,并且是线程安全的)也是完全排他的,即使是读也只能同步执行,而ConcurrentHashMap就可以实现同一时刻多个线程之间并发。为了提高效率,ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。

ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

在没有线程进行写操作时,进行读操作的多个线程都可以获取到读锁,而写操作的线程只有获取写锁后才能进行写入操作。即:多个线程可以同时进行读操作,但是同一时刻只允许一个线程进行写操作。

ReentrantReadWriteLock锁的特性:

(1)读读共享;
(2)写写互斥;
(3)读写互斥;
(4)写读互斥;

ReentrantReadWriteLock实例代码

(1)读读共享

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {

        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.read(), "ThreadA").start();
        new Thread(() -> demo.read(), "ThreadB").start();
    }

    private void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName()
                        + " 时间:" + System.currentTimeMillis());
                //模拟读操作时间为5秒
                Thread.sleep(5000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

获得读锁ThreadA 时间:1507720692022
获得读锁ThreadB 时间:1507720692022

可以看出两个线程之间,获取锁的时间几乎同时,说明lock.readLock().lock(); 允许多个线程同时执行lock()方法后面的代码。

(2)写写互斥

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {

        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.write(), "ThreadA").start();
        new Thread(() -> demo.write(), "ThreadB").start();
    }

    private void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获得写锁" + Thread.currentThread().getName()
                        + " 时间:" + System.currentTimeMillis());
                //模拟写操作时间为5秒
                Thread.sleep(5000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

获得写锁ThreadA 时间:1507720931662
获得写锁ThreadB 时间:1507720936662

可以看出执行结果大致差了5秒的时间,可以说明多个写线程是互斥的。

(3)读写互斥或写读互斥

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.read(), "ThreadA").start();
        Thread.sleep(1000);
        new Thread(() -> demo.write(), "ThreadB").start();
    }

    private void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("获得读锁" + Thread.currentThread().getName()
                        + " 时间:" + System.currentTimeMillis());
                Thread.sleep(3000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("获得写锁" + Thread.currentThread().getName()
                        + " 时间:" + System.currentTimeMillis());
                Thread.sleep(3000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

执行结果:

获得读锁ThreadA 时间:1507721135908
获得写锁ThreadB 时间:1507721138908

可以看出执行结果大致差了3秒的时间,可以说明读写线程是互斥的。

你可能感兴趣的:(Java技术提高,Java多线程编程核心技术)