文章目录

  • 一、题目要求
  • 二、用wait-notify优化前后的两版代码
  • 1.优化前
  • 2.优化后
  • 三、使用park和unpark实现
  • 四、Condition接口


一、题目要求

给定两个List(或者数组),一个List存储"ABCDEFG",一个List存储"1234567",两个线程分别打印两个List,打印的结果是 “A1B2C3D4E5F6G7”

面试官当时说,这是一道学并发编程必写的一道题,我心里想:嗯?我就没写过。然后他接着说:打开你的IDEA共享屏幕,开始写吧(视频面试)。行我就开始写了。

这道题考察的是线程通信的方式,就是线程A打印A阻塞,线程B打印1阻塞,以此类推。实现的方式有很多种,比如wait-notify,park-unpark,Contidition等等,上面说的三种是比较常见的方式了。

我选用的是wait-notify,后面两种也会写到。

二、用wait-notify优化前后的两版代码

1.优化前

public static void main(String[] args){

     //定义对象锁和两个List
     final Object o = new Object();
     final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
     final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");

     //打印数字
     new Thread(() -> {
         integers.forEach(item -> {
             synchronized (o) {
                 try {
                     o.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.print(item);
                 o.notify();
             }
         });
     }, "t1").start();

     //打印字母
     new Thread(() -> {
         strings.forEach(item -> {
             synchronized (o) {
                 System.out.print(item);
                 o.notify();
                 try {
                     o.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         });
     }, "t2").start();
 }

//输出结果
A1B2C3D4E5F6G7

解释下上面实现的过程

两个线程使用同一把对象锁,Object类型的对象o,synchronized大家比较熟悉了,实现了锁机制,线程间的同步,原理就是多个线程同时修改对象头中的ThreadId,修改成功了就是抢到锁了,进入同步代码块,修改失败会进入等待队列中(后续也会写相关的文章)。

假如线程t1抢到锁了,调用o.wait()时,线程t1会释放锁资源,jvm会将线程t1暂时放到对象锁o的等待队列中。当线程t2调用o.notify()时会从对象锁o的等待队列中随机唤醒一个,因为只有一个t1,所以唤醒t1继续打印输出。换成t2亦然。

我写完之后,面试官接着说:给你1min分钟的时间,想一下有没有可以优化的地方。大家也可以想下,我当时说的是代码重复。

2.优化后

面试官:你这个过程中每循环一次就会加一次锁,会有额外的性能损耗。听完豁然开朗,开始优化第二版

优化过程比较简单就是将synchronized同步代码块放到循环的外面,这样只有线程启动时加锁,只有一次加锁的过程。代码就不放了,可以自己实现下

三、使用park和unpark实现

park和unpark是LockSupport类中的两个方法,LockSupport.park()使当前线程处于等待状态,而LockSupport.unpark()表示唤醒一个线程,unpark方法需要一个参数,指定唤醒哪个线程。理解起来比较简单。

直接上代码吧

static Thread t1=null;
static Thread t2=null;

public static void main(String[] args) {
    final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
    final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");
    //打印字母
    t1=new Thread(()->{
        strings.forEach(item->{
            System.out.print(item);
            LockSupport.unpark(t2);
            LockSupport.park();
        });
    },"t1");
    //打印数字
    t2=new Thread(()->{
        integers.forEach(item->{
            LockSupport.park();
            System.out.print(item);
            LockSupport.unpark(t1);
        });
    },"t2");
    t1.start();
    t2.start();
}

四、Condition接口

先上代码,再解释

public static void main(String[] args) {

    final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
    final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G");

    ReentrantLock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    
    //打印数字
    new Thread(()->{
        lock.lock();
        try {
            integers.forEach(item->{
                try {
                    condition1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.print(item);
                condition2.signal();
            });
        }finally {
            lock.unlock();
        }
    },"t1").start();

    //打印字母
    new Thread(()->{
        lock.lock();
        try {
            strings.forEach(item->{
                System.out.print(item);
                condition1.signal();
                try {
                    condition2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }finally {
            lock.unlock();
        }
    },"t2").start();
}

看的出来condition.await()和signal()实现了o.wait和notify的作用。await使当前线程阻塞,signal唤醒其他线程。大致原理是AQS除了维护一个同步队列外,还维护了一个等待队列,该等待队列实现了condition,具体原理后续博客也回写。

不过要注意一个点,await和signal在阻塞和唤醒时如果顺序不当,可能导致当前程序无法执行完,wait和notify也有一样的问题。当然上面的程序是没有问题。

因为当主程序运行时t1先获取锁,然后立刻阻塞。此时t2获取锁先打印字母A,再唤醒t1打印1,阻塞t2。整好可以打印一组A1,循环7次。最后t2打印完G处于等待状态,t1打印完成后唤醒t2,程序结束。两个线程都不会处于等待状态。