什么是线程安全问题?

线程安全是多线程编程是的计算机程序代码中的一个概念

我们知道,同一进程下的线程之间是共享资源的,多线程的引入,导致了一个问题,那就是多条线程操作同一数据,而每个线程处理的结果不一致,**导致共享资源出现错乱,这就是线程安全问题。**

如下:
两个线程 A 和 B 同时操作同一个数组 (共享资源) , 判断如果数组第一个元素为 null 的时候, 将其设置当前线程名.

然而 A 线程在判断条件成立之后, 由于某些原因多耗费了 20 毫秒的时间

当耗时过后, 此时的数组第一个元素已经有值了, A 线程已经判断过就不可能重新判断, 所以又给第一个元素赋了值, 这就导致共享资源的最终状态并不准确

  1. public class ThreadSecurity {
  2. public static void main(String[] args) throws InterruptedException {
  3. // 定义一个数组
  4. final String[] array = new String[10];
  5. // 线程A向array[0]设置当前线程名
  6. Thread threadA = new Thread("A"){
  7. @Override
  8. public void run() {
  9. // 判断此时的array[0]是否已经存在值
  10. if(null == array[0]){
  11. System.out.println("线程A休眠前的array[0] = "+ array[0]);
  12. // 假如此时有比较耗时的操作需要20毫秒(比如查询数据库之类的)
  13. try {
  14. sleep(20);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. System.out.println("经过休眠后的array[0] = "+array[0]);
  19. array[0] = this.getName();
  20. }
  21. }
  22. };
  23. // 线程B向array[0]设置当前线程名
  24. Thread threadB = new Thread("B"){
  25. @Override
  26. public void run() {
  27. // 如果array[0]没有值
  28. if(null == array[0]){
  29. array[0] = this.getName();
  30. System.out.println("线程B:没人要我就先设置了,array[0] = " + array[0]);
  31. }
  32. }
  33. };
  34. threadA.start();
  35. threadB.start();
  36. Thread.sleep(200);
  37. System.out.println("最终的array[0] = "+array[0]);
  38. }
  39. }

最终输出如下:

  1. 线程A休眠前的array[0] = B
  2. 线程B:没人要我就先设置了,array[0] = B
  3. 经过休眠后的array[0] = B
  4. 最终的array[0] = A

为什么会出现线程安全问题?

多线程下,CPU执行时先将主内存中的数据拷贝一份作缓存,之后的操作不在从请求主内存中拿数据而是从缓存中获取数据,如果执行完后有更改再同步回主内存,这时候如果存在多条线程操作,就会出现主内存和缓存中数据的不一致,而导致了最终结果的错误,而单线程情况下则完全不会出现这一情况
**
线程安全问题都是由全局变量及静态变量引起的. 若每个线程中对全局变量, 静态变量只有读操作, 而无写操作, 一般来说, 这个全局变量是线程安全的; 若有多个线程同时执行写操作, 一般都需要考虑线程同步, 否则的话就可能影响线程安全.

也就是说, 出现线程安全问题的条件是:
1、在多线程环境下
2、必须有共享资源
3、对共享资源进行非原子性操作【结果不一样的写操作】。

怎么解决线程安全问题?

解决线程安全问题实际上就是实现线程安全, 线程安全就是: 在多线程环境中, 能永远保证程序的正确性.

线程安全在三个方面体现
1. 原子性: 提供互斥访问, 同一时刻只能有一个线程对数据进行操作 (atomic, synchronized);
2. 可见性: 一个线程对主内存的修改可以及时地被其他线程看到 (synchronized, volatile);
3. 有序性: 一个线程观察其他线程中的指令执行顺序, 由于指令重排序, 该观察结果一般杂乱无序, (happens-before 原则) .

我们知道, 线程安全问题都是因为全局变量以及成员变量, 那我们避免使用不就行了? 那也不可能不用对吧, 我们看看在 Java 并发编程里面常用的几种解决方案:

synchronized关键字

synchronized 是 Java 的一个关键字, 实现同步锁 (也叫互斥锁或内置锁) , 它可以保证在同一时刻, 只有一个线程可以执行某个方法或某个代码块

  1. // 1.修饰代码块,括号内的是临界对象,也就是要获取锁的资源,花括号内是持有锁期间执行的内容
  2. public void method()
  3. {
  4. synchronized(this) {
  5. // todo
  6. }
  7. }
  8. // 2.修饰方法
  9. public synchronized void method()
  10. {
  11. // todo
  12. }

原理: 当线程进入方法时, 会自动获得一个针对临界对象的锁, 一旦锁被获得, 其他线程必须等待获得锁的线程执行完代码或代码发生异常 释放锁.
**

所谓的临界对象就是共享资源对象,如上述案例的array就是临界对象,当synchronized关键字被用于修饰方法的时候,临界对象是该方法所属对象

缺点: 同步等待, 效率降低

示例:
如下我们在两个线程中操作array之前都加了一个synchronized关键字,并包裹了一段代码块

  1. public class ThreadSecurity {
  2. public static void main(final String[] args) throws InterruptedException {
  3. // 定义一个数组
  4. final String[] array = new String[10];
  5. // 线程A向array[0]设置当前线程名
  6. Thread threadA = new Thread("A"){
  7. @Override
  8. public void run() {
  9. // 获取array的同步锁
  10. synchronized (array){
  11. // 判断此时的array[0]是否已经存在值
  12. if(null == array[0]){
  13. System.out.println("线程A休眠前的array[0] = "+ array[0]);
  14. // 假如此时有比较耗时的操作需要20毫秒(比如查询数据库之类的)
  15. try {
  16. sleep(100); // sleep操作并不会释放锁,也不会让出CPU资源
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println("经过休眠后的array[0] = "+array[0]);
  21. array[0] = this.getName();
  22. System.out.println("线程A设置后的array[0] = "+array[0]+" \n");
  23. }
  24. }
  25. }
  26. };
  27. // 线程B向array[0]设置当前线程名
  28. Thread threadB = new Thread("B"){
  29. @Override
  30. public void run() {
  31. System.out.println("线程B开始咯");
  32. // 获取array的同步锁
  33. synchronized (array){
  34. System.out.println("线程B获取到的array[0] = " + array[0]);
  35. // 如果array[0]没有值
  36. if(null == array[0]){
  37. array[0] = this.getName();
  38. System.out.println("线程B:没人要我就先设置了,array[0] = " + array[0]);
  39. }
  40. }
  41. }
  42. };
  43. threadA.start();
  44. threadB.start();
  45. Thread.sleep(200);
  46. System.out.println("最终的array[0] = "+array[0]);
  47. }
  48. }

结果:可以看到,在线程A获取到array的同步锁后,只有当线程A的synchronized包裹的代码执行完毕后,线程B才能继续,因为线程B无法获取到array的同步锁,处于阻塞状态。

线程A休眠前的array[0] = null
线程B开始咯
经过休眠后的array[0] = null
线程A设置后的array[0] = A 

线程B获取到的array[0] = A
最终的array[0] = A

扩展:synchronized实现等待/通知机制

**
线程之间的协作:
为了完成某个任务, 线程之间需要进行协作, 采取的方式: 中断, 互斥, 以及互斥上面的线程的挂起, 唤醒; 如: 生成者—消费者模式, 或者某个动作完成, 可以唤醒下一个线程, 管道流已准备等等;

等待/通知机制:
等待/通知机制 是线程之间的协作一种常用的方式之一

等待/通知机制 经典的使用方式, 便是在生产者与消费者的模式中使用:

1, 生产者负责生产商品, 并送到仓库中存储;
2, 消费者则从仓库中获取商品, 享受商品;
3, 仓库的容量有限, 当仓库满了后, 生产者就会停止生产, 进入等待状态. 直到消费者消耗了一部分商品, 通知生产者, 仓库有空位了, 生产者才会继续生产商品;
4, 当消费者的消耗速度过快, 消耗光仓库的商品时, 也会停止消耗, 进入等待状态, 直到生产者生产了商品, 并通知唤醒消费者时, 消费者才继续消耗商品.

等待/通知机制 在 synchronized 下的实现:
在内置锁下, 等待/通知机制 是依赖于 wait( ) 和 notify, notifyAll 来实现的, 这三个方法都是继承自根类 Object 中.
void wait():
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前, 当前线程持续等待.
**
void wait(long timeout):
计时等待, 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法, 或者超过指定的时间量前, 当前线程等待. 如果时间当时间到来了, 线程还是没有被唤醒或中断, 那么就会自动唤醒;

void wait(long timeout, int nanos):
参数 timeout 的单位是毫秒, 参数 nanos 的单位是纳秒, 即等待时间精确到纳秒. 其他特性与 wait(long timeout) 一样.

void notify():
唤醒在此对象监视器 (也就是临界对象/共享对象) 上等待的单个线程. 如果所有线程都在此对象上等待, 则会选择唤醒其中一个线程. 选择是任意性的, 并在对实现做出决定时发生. 线程通过调用其中一个 wait 方法, 在对象的监视器上等待. 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;

void notifyAll():
唤醒在此对象监视器上等待的所有线程.
**
使用案例:

/**
 * synchronized关键字实现等待通知机制
 * @throws InterruptedException
 */
public static void synchronizedNotifyTest() throws InterruptedException {
    // 定义一个数组(相当于仓库)
    final String[] array = new String[10];

    // 线程A向array持续存放数据,相当于进货到仓库
    Thread threadA  = new Thread("A"){
        @Override
        public void run() {

            // 获取array的同步锁
            synchronized (array){
                int i = 0;
                // 只要第十个元素还没有内容都可以循环存放内容
                while(null == array[9]){
                    array[i] = "第"+(i+1)+"个商品";
                    System.out.println(this.getName() + ":生产者进了"+array[i]);
                    i++;
                    if(i>9) {
                        System.out.println(this.getName() + ":我进完货了,先叫醒消费者线程B");

                        array.notify(); // 商品填充满了,唤醒消费者

                        System.out.println(this.getName() + ":我自己先离开了,不然它拿不到商品");

                        // 但是实际上还没有释放锁,还得等自己执行完这段代码块才能释放,所以消费者还是暂时不能消费
                        break; // 退出循环,也就是结束当前线程
                    }

                }
            }
        }
    };

    // 线程B持续将array的元素置为null,相当于消费商品
    Thread threadB  = new Thread("B"){
        @Override
        public void run() {

            // 获取array的同步锁
            synchronized (array){
                if(array[9] == null){ // 假如现在进货还没到10个
                    try {
                        System.out.println(this.getName() + ":我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货");
                        array.wait(); // 让当前线程先进入等待状态,释放锁给线程A去执行进货,当前线程等待唤醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                for(int i = 0 ;i<10;i++){
                    System.out.println(this.getName() + ":消费者消费 "+array[i]);
                    array[i] = null;
                }
            }
        }
    };
    threadA.setPriority(Thread.MIN_PRIORITY);
    threadB.setPriority(Thread.MAX_PRIORITY);
    threadA.start();
    threadB.start();
}

输出结果如下:
image.png

要注意的几点内容:
1、调用 wait、notify、notifyAll 前,必须先获取锁
2、**wait()是释放锁,notify、notifyAll 不释放锁(调用wait()方法是线程马上释放锁,进入等待队列,而调用notify、notifyAll 后,线程不会释放锁,而仅仅唤醒线程而已,要等待当前运行完后,锁才是真的被释放,被唤醒的线程才可以竞争获取锁。)**

volatile关键字

一旦一个共享变量 (类的成员变量, 类的静态成员变量) 被 volatile 修饰之后, 那么就具备了两层语义:
  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。【不会上锁,但是会保证修改的值会立即被更新到主存(而不是缓存),当有其他线程需要读取时,它会去内存中读取新值。
2) 禁止进行指令重排序. 『让线程顺序执行同步代码』

但是,因为volatile不能保证变量操作的原子性,所以试图通过volatile来保证线程安全性是不靠谱的。

案例:

public static void volatileTest() throws InterruptedException {
    // 定义一个数组
    String[] array = new String[10];

    // 线程A向array[0]设置当前线程名
    Thread threadA  = new Thread(new MyRunnableA(array),"A");

    // 线程B向array[0]设置当前线程名
    Thread threadB  = new Thread(new MyRunnableB(array),"B");

    threadA.start();
    threadB.start();
    Thread.sleep(200);
    System.out.println("最终的array[0] = "+array[0]);
}

static class MyRunnableA implements Runnable{
    volatile String array[];

    MyRunnableA(String array[]){
        this.array = array;
    }

    @Override
    public void run() {
        // 判断此时的array[0]是否已经存在值
        if(null == array[0]){

            System.out.println("线程A休眠前的array[0] = "+ array[0]);

            // 假如此时有比较耗时的操作需要20毫秒(比如查询数据库之类的)
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("经过休眠后的array[0] = "+array[0]);

            array[0] = Thread.currentThread().getName();

            System.out.println("线程A设置后的array[0] = "+array[0]+" \n");
        }
    }
}

static class MyRunnableB implements Runnable{
    volatile String array[];

    MyRunnableB(String array[]){
        this.array = array;
    }

    @Override
    public void run() {
        System.out.println("线程B开始咯");

        System.out.println("线程B获取到的array[0] = " + array[0]);
        // 如果array[0]没有值
        if (null == array[0]) {
            array[0] = Thread.currentThread().getName();
            System.out.println("线程B:没人要我就先设置了,array[0] = " + array[0]);
        }
    }
}

输出如下:

线程B开始咯
线程A休眠前的array[0] = null
线程B获取到的array[0] = null
线程B:没人要我就先设置了,array[0] = B
经过休眠后的array[0] = B
线程A设置后的array[0] = A 

最终的array[0] = A

volatile 一般用于标志量修饰:

volatile boolean asleep;
……
while( !asleep){
 countSomeSheep();
}

volatile 的读性能消耗与普通变量几乎相同, 但是写操作稍慢, 因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行.

JDK提供的原子类

jdk5之后,在**java.util.concurrent.atomic**包下面,有许多的原子类,这里面的操作类型大多数与JAVA中基本类型的包装类对应。目的是为了防止高并发的情况下,各个线程操作产生错误数据。这些类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁

CAS 算法: Compare and Swap, 即比较再交换. 它包含三个参数 CAS(V, E, N): V 表示要更新的变量, E 表示预期值, N 表示新值. 仅当 V 值等于 E 值时, 才会将 V 的值设为 N, 如果 V 值和 E 值不同, 则说明已经有其他线程做了更新, 则当前线程什么都不做. 最后, CAS 返回当前 V 的真实值. 类似于版本控制

而这种方式的缺点就是: 只能保证一个共享变量的原子性

Java 中的原子操作类大致可以分为 4 类: 原子更新基本类型, 原子更新数组类型, 原子更新引用类型, 原子更新属性类型

原子更新基本类型

基本类型封装: AtomicBoolean, AtomicInteger, AtomicLong

原子更新数组类型

AtomicIntegerArray: 原子更新整型数组里的元素.
AtomicLongArray: 原子更新长整型数组里的元素.
AtomicReferenceArray: 原子更新引用类型数组里的元素.

如下述案例, 主要原理就是依靠 compareAndSet 方法, 进行插入操作之前的版本校对.

public static void atomicTest() throws InterruptedException {
    String[] array = new String[10];
    final AtomicReferenceArray atomicReferenceArray = new AtomicReferenceArray(array);

    // 线程A向array[0]设置当前线程名
    Thread threadA  = new Thread("A"){
        @Override
        public void run() {

            // 判断此时的array[0]是否已经存在值
            Object current = atomicReferenceArray.get(0); // 获取目前的首元素
            if(null == current){

                System.out.println("线程A休眠前的array[0] = "+ current);

                // 假如此时有比较耗时的操作需要20毫秒(比如查询数据库之类的)
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("经过休眠后的array[0] = "+atomicReferenceArray.get(0));

                // 比较后交换,相当于又if了一次,如果值current依旧等于首元素,证明没有被别的元素修改过,就可以修改
                atomicReferenceArray.compareAndSet(0,current,this.getName());

                System.out.println("线程A设置后的array[0] = "+atomicReferenceArray.get(0)+" \n");
            }
        }

    };

    // 线程B向array[0]设置当前线程名
    Thread threadB  = new Thread("B"){
        @Override
        public void run() {
            System.out.println("线程B开始咯");

            System.out.println("线程B获取到的array[0] = " + atomicReferenceArray.get(0));
            // 如果array[0]没有值
            Object current = atomicReferenceArray.get(0);
            if(null == current){
                atomicReferenceArray.compareAndSet(0,current,this.getName());
                System.out.println("线程B:没人要我就先设置了,array[0] = " + atomicReferenceArray.get(0));
            }
        }
    };

    threadA.start();
    threadB.start();
    Thread.sleep(200);
    System.out.println("最终的array[0] = "+array[0]);
}

原子更新引用类型

AtomicReference

原子更新属性类型

AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器.
AtomicLongFieldUpdater: 原子更新长整型字段的更新器.
AtomicStampedReference: 原子更新带有版本号的引用类型

使用Lock

Lock 类是 java 提供的类, 丰富的 api 使得 Lock 类的同步功能比 synchronized 的同步更强大, 它可以确保当一个线程位于代码的临界区时, 另一个线程不会进入该临界区. 如果其他线程尝试进入锁定的代码, 则它将一直等待 (即被阻止) , 直到该对象被释放.

Lock 类实际上是一个接口, 我们在实例化的时候实际上是实例化实现了该接口的类 Lock lock = new ReentrantLock();

使用方式:通过Lock对象lock,**用lock.lock来加锁,用lock.unlock来释放锁。在两者中间放置需要同步处理的代码。**

为什么需要Lock?使用synchronized不是更方便吗?

需要使用 Lock 肯定是因为他比 synchronized 更加完善.

1, 在使用 synchronized 关键字的情形下, 假如占有锁的线程由于要等待 IO 或者其他原因 (比如调用 sleep 方法) 被阻塞了, 但是又没有释放锁, 那么其他线程就只能一直等待, 这会极大影响程序执行效率. 因此, 就需要有一种机制可以不让等待的线程一直无期限地等待下去 (比如只等待一定的时间 (解决方案: tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案: lockInterruptibly())) , 这种情况可以通过 Lock 解决.

2, synchronized 关键字实现同步的话, 就会导致一个问题, 即当多个线程都只是进行读操作时, 也只有一个线程在可以进行读操作, 其他线程只能等待锁的释放而无法进行读操作. 因此, 需要一种机制来使得当多个线程都只是进行读操作时, 线程之间不会发生冲突. 同样地, Lock 也可以解决这种情况 (解决方案: ReentrantReadWriteLock)

3, 我们可以通过 Lock 得知线程有没有成功获取到锁 (解决方案: ReentrantLock) , 但这个是 synchronized 无法办到的.

先看看 lock 接口定义了哪些方法:

// 获取锁,如果锁已被其他线程获取,则进行等待
void lock();

// 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;

//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock();

//和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间
// 在时间期限之内如果还拿不到锁,就返回false。
// 如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 释放锁
void unlock();

// 返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

简单案例

public static void lockTest() {
    final Lock lock = new ReentrantLock();
    final String[] array = new String[10];

    // 线程A向array[0]设置当前线程名
    Thread threadA  = new Thread("A"){
        @Override
        public void run() {

            // 得到对象锁
            lock.lock();
            try {
                // 判断此时的array[0]是否已经存在值
                if(null == array[0]){

                    System.out.println("线程A休眠前的array[0] = "+ array[0]);

                    // 假如此时有比较耗时的操作需要20毫秒(比如查询数据库之类的)
                    sleep(100);

                    System.out.println("经过休眠后的array[0] = "+array[0]);

                    array[0] = this.getName();

                    System.out.println("线程A设置后的array[0] = "+array[0]+" \n");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally { // 释放锁放在finally防止发生异常时无法释放锁,导致持续性的线程阻塞

                // 释放对象锁
                lock.unlock();
            }
        }
    };

    // 线程B向array[0]设置当前线程名
    Thread threadB  = new Thread("B"){
        @Override
        public void run() {
            System.out.println("线程B开始咯");

            // 得到对象锁
            lock.lock();
            try {
                System.out.println("线程B获取到的array[0] = " + array[0]);
                // 如果array[0]没有值
                if(null == array[0]){
                    array[0] = this.getName();
                    System.out.println("线程B:没人要我就先设置了,array[0] = " + array[0]);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally { // 释放锁放在finally防止发生异常时无法释放锁,导致持续性的线程阻塞
                lock.unlock(); // 释放对象锁
            }
        }
    };

    threadA.start();
    threadB.start();
    try {
        Thread.sleep(200);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("最终的array[0] = "+array[0]);
}

输出如下:在线程A获取到对象锁之后,线程B再去获取只能进行等待,直至线程A释放了锁

线程A休眠前的array[0] = null
线程B开始咯
经过休眠后的array[0] = null
线程A设置后的array[0] = A 

线程B获取到的array[0] = A
最终的array[0] = A

公平锁与非公平锁

ReentrantLock 实现了 Lock 接口, 同时提供了一些比较实用的功能, 如公平锁与非公平锁.

公平锁: 按照线程加锁的顺序来获取锁 (先来先得, 默认, 从这个角度来讲, synchronized 算是一个公平锁)
非公平锁: 随机竞争来得到锁
**
创建公平与非公平锁由 ReentrantLock 的构造方法来决定:

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

上述简单案例创建的实际上就是一个公平锁
**
此外, JAVA 还提供 isFair() 来判断一个锁是不是公平锁.

/**
 * Returns {@code true} if this lock has fairness set true.
 *
 * @return {@code true} if this lock has fairness set true
 */
public final boolean isFair() {
    return sync instanceof FairSync;
}

那如果只是这样的话, 貌似和 synchronized 没啥两样对吧, 只是多了个手动调节, 还得看看 Condition 类

Condition类

synchronized 关键字与 wait() 和 notify/notifyAll() 方法相结合可以实现等待/通知机制, Lock 类也可以实现, 但是需要借助于 Condition 接口与 newCondition() 方法.

Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以**有选择性的进行线程通知**,在调度线程上更加灵活

主要依靠的方法如下:

// 对应Object的wait方法
void await() //造成当前线程在接到信号或被中断之前一直处于等待状态。 
boolean await(long time, TimeUnit unit) //造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 
long awaitNanos(long nanosTimeout) //造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 
void awaitUninterruptibly() //造成当前线程在接到信号之前一直处于等待状态。 
boolean awaitUntil(Date deadline) //造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。 

// 对应Object的notify和notifyAll
void signal() //唤醒一个等待线程
void signalAll() //唤醒所有等待线程。

Condition 类实现等待/通知机制

/**
 * Lock的Condition实现等待通知机制
 * @throws InterruptedException
 */
public static void conditionNotifyTest() throws InterruptedException {
    final Lock lock = new ReentrantLock();
    final String[] array = new String[10];
    final List<Condition> conditions = new ArrayList<>(); // 等待队列
    // 创建10个等待对象
    for(int i=0;i<10;i++){
        conditions.add(lock.newCondition());
    }


    // 线程A向array持续存放数据,相当于进货到仓库
    Thread threadA  = new Thread("生产者"){
        @Override
        public void run() {

            // 得到对象锁
            lock.lock();
            try {
                int i = 0;
                // 只要第十个元素还没有内容都可以循环存放内容
                while(null == array[9]){
                    array[i] = "第"+(i+1)+"个商品";
                    System.out.println(this.getName() + ":生产者进了"+array[i]);
                    i++;
                    if(i>9) {
                        System.out.println(this.getName() + ":我进完货了,先叫醒消费者1号");

                        conditions.get(1).signal(); // 商品填充满了,我只想唤醒1号消费者,别的我都不管

                        System.out.println(this.getName() + ":我自己先离开了,不然它拿不到商品");

                        // 但是实际上还没有释放锁,还得等自己执行完这段代码块才能释放,所以消费者还是暂时不能消费
                        break; // 退出循环,也就是结束当前线程
                    }

                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                // 释放对象锁
                lock.unlock();
            }
        }
    };

    // 创建多个消费者线程
    for(int i = 0;i<10;i++){

        final int finalI = i;
        Thread consumer  = new Thread("消费者"+ finalI){
            @Override
            public void run() {

                // 得到对象锁
                lock.lock();
                try {
                    if (array[9] == null) { // 假如现在进货还没到10个
                        try {
                            System.out.println(this.getName() + ":我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货");
                            conditions.get(finalI).await(); // 让当前线程先进入等待状态,释放锁给生产者线程去执行进货,当前线程等待唤醒
                            System.out.println(this.getName()+"准备开始消费咯");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    for (int i = 0; i < 10; i++) {
                        System.out.println(this.getName() + ":消费者消费 " + array[i]);
                        array[i] = null;
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    // 释放对象锁
                    lock.unlock();
                }
            }
        };
        // 直接启动消费者线程
        consumer.start();
    }

    threadA.setPriority(Thread.MIN_PRIORITY);
    threadA.start();
}

输出结果如下:
消费者线程队列先行启动, 而此时 array 并不是满的, 每个消费者线程对应的 condition 对象执行 await () 方法, 进入等待状态, 让出锁.
当生产者线程拿到锁之后开始进货, 精准唤醒消费者 1 号, 仅让它一个线程进行消费『精准投喂』

消费者0:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者5:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者1:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者2:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者3:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者6:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者4:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者7:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者8:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
消费者9:我先拿到了资源锁,但是array里面不是满的,我先等会儿,让生产者先进货
生产者:生产者进了第1个商品
生产者:生产者进了第2个商品
生产者:生产者进了第3个商品
生产者:生产者进了第4个商品
生产者:生产者进了第5个商品
生产者:生产者进了第6个商品
生产者:生产者进了第7个商品
生产者:生产者进了第8个商品
生产者:生产者进了第9个商品
生产者:生产者进了第10个商品
生产者:我进完货了,先叫醒消费者1号
生产者:我自己先离开了,不然它拿不到商品
消费者1准备开始消费咯
消费者1:消费者消费 第1个商品
消费者1:消费者消费 第2个商品
消费者1:消费者消费 第3个商品
消费者1:消费者消费 第4个商品
消费者1:消费者消费 第5个商品
消费者1:消费者消费 第6个商品
消费者1:消费者消费 第7个商品
消费者1:消费者消费 第8个商品
消费者1:消费者消费 第9个商品
消费者1:消费者消费 第10个商品