两个线程如何交替打印输出?这个问题可以有助于快速理解并发相关的API的使用,以及相关的原理。

具体题目如下:

两个线程交替输出
                 第一个线程:1 2 3 4 5 6 7
                 第二个线程: A B C D E F G
        输出结果:1A2B3C4D5E6F7G

方法一:使用LockSupport的unpark和park

package com.wuxiaolong.concurrent;

import java.util.concurrent.locks.LockSupport;

/**
 * Description:
 *      两个线程交替输出
 *                 第一个线程:1 2 3 4 5 6 7
 *                 第二个线程: A B C D E F G
 *        输出结果:1A2B3C4D5E6F7G
 *
 * @author 诸葛小猿
 * @date 2021-03-01
 */
public class TestSample1 {

    // 定义为static
    static Thread t1 = null;
    static Thread t2 = null;

    public static void main(String[] args) {
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        t1 = new Thread(()->{

            for(char temp: nums){
                System.out.println(temp);
                LockSupport.unpark(t2); // 叫醒t2
                LockSupport.park(); // 阻塞当前线程t1
            }
        },"t1");

        t2 = new Thread(()->{

            for(char temp: chars){
                LockSupport.park(); // 阻塞当前线程t2
                System.out.println(temp);
                LockSupport.unpark(t1); // 叫醒t1
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

方法二:使用Object的notify和wait

package com.wuxiaolong.concurrent;

import java.util.concurrent.locks.LockSupport;

/**
 * Description:
 *      两个线程交替输出
 *                 第一个线程:1 2 3 4 5 6 7
 *                 第二个线程: A B C D E F G
 *        输出结果:1A2B3C4D5E6F7G
 *
 * @author 诸葛小猿
 * @date 2021-03-01
 */
public class TestSample2 {

    public static void main(String[] args){
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        Object lock = new Object();

        /**
         * synchronized的原理,多个线程同时抢一把锁,该锁维护一个队列(同步队列),没有争抢到锁的所有线程在同一个同步队列里。
         * wait:该线程释放锁后阻塞,该线程进入另一个队列(等待队列)
         * notify:随机叫醒等待队列中的任何一个线程
         * notifyAll: 唤醒等待队列中的所有线程,让他们去争夺同一把锁,谁争抢到谁执行。
         *
         * 下面t1和t2同时争抢同一把锁:
         *  1.假如t1争抢到锁,则t1执行而t2进入到该锁的同步队列中,
         *  2.t1执行时,执行到notify,这时等待队列中没有线程,继续执行到wait时,释放锁并让t1进入到等待队列,等待notify唤醒
         *  3.由于t1释放锁了,同步队列中的t2就可以获得锁执行,执行到notify时,会随机唤醒等待队列中的一个线程,因为队列中只有一个t1,所以t1去争抢锁;t2执行到wait,t2释放锁并让t2进入到等待队列,等待notify唤醒,t1会获得锁执行。
         *  4.依次循环
         */

        Thread t1 = new Thread(()->{
            try{
                synchronized (lock){
                    for(char temp: nums){
                        System.out.println(temp);
                        lock.notify();
                        lock.wait();
                    }
                }
            }catch (Exception e){

            }
        },"t1");

        Thread t2 = new Thread(()->{
            try{

                synchronized (lock){
                    for(char temp: chars){

                        System.out.println(temp);
                        lock.notify();
                        lock.wait();
                    }
                }

            }catch (Exception e){

            }
        },"t2");

        t1.start();
        t2.start();
    }
}

这种方式有两个问题:

  • 问题1:启动时不一定是t1先获得锁,可能会先打印字母。如何保证t1先获得锁呢? 使用CountDownLatch。
  • 问题2:这个线程会停止吗? 不会,最终等待队列中一定有一个线程存在。

方法三:改进方法二的实现

package com.wuxiaolong.concurrent;

import java.util.concurrent.CountDownLatch;

/**
 * Description:
 *      两个线程交替输出
 *                 第一个线程:1 2 3 4 5 6 7
 *                 第二个线程: A B C D E F G
 *        输出结果:1A2B3C4D5E6F7G
 *
 * @author 诸葛小猿
 * @date 2021-03-01
 */
public class TestSample2_1 {

    public static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args){
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        Object lock = new Object();

        Thread t1 = new Thread(()->{
            try{
                synchronized (lock){
                    countDownLatch.countDown(); //减去1
                    for(char temp: nums){
                        System.out.println(temp);
                        lock.notify();
                        lock.wait();
                    }
                    lock.notify(); //清空等待队列
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"t1");

        Thread t2 = new Thread(()->{
            try{

                countDownLatch.await(); //等待,下面的代码一定不会先运行。这里使用的是await,不是wait

                synchronized (lock){
                    for(char temp: chars){
                        System.out.println(temp);
                        lock.notify();
                        lock.wait();
                    }
                    lock.notify(); //清空等待队列
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"t2");

        t1.start();
        t2.start();
    }
}

方法四:使用ReentrantLock和Condition

这是一种比较好的实现方式。可以很方便的使用多个线程交替打印。

package com.wuxiaolong.concurrent;

import java.sql.SQLOutput;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Description:
 *      两个线程交替输出
 *                 第一个线程:1 2 3 4 5 6 7
 *                 第二个线程: A B C D E F G
 *                 第三个线程:  ~!@#$%^
 *        输出结果:1A!2B@3C#4D$5E%6F^7G&
 *
 * @author 诸葛小猿
 * @date 2021-03-01
 */
public class TestSample3 {

    public static void main(String[] args){
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();
        char[] signs = "~!@#$%^".toCharArray();

        /**
         * 每一个Condition都有一个队列,cT1的条件队列里有线程t1,cT2的条件队列里有线程t2。每个队列里的线程都是通过await释放锁,通过signal唤醒。
         *
         * 可以使用多个Condition精确的控制程序的打印.
         *
         * 也需要使用CountDownLantch精确的让哪一个线程先执行
         *
         */
        ReentrantLock lock = new ReentrantLock();
        Condition cT1 = lock.newCondition();
        Condition cT2 = lock.newCondition();
        Condition cT3 = lock.newCondition();

        Thread t1 = new Thread(()->{

            lock.lock();

            try{
                for (char temp : nums){
                    System.out.println(temp);
                    // 通知第二个线程执行,让第一个线程等待
                    cT2.signal();
                    cT1.await();
                }
                cT2.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread t2 = new Thread(()->{

            lock.lock();

            try{
                for (char temp : chars){
                    System.out.println(temp);
                    // 通知第三个线程执行,让第二个线程等待
                    cT3.signal();
                    cT2.await();
                }
                cT3.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread t3 = new Thread(()->{

            lock.lock();

            try{
                for (char temp : signs){
                    System.out.println(temp);
                    // 通知第一个线程执行,让第三个线程等待
                    cT1.signal();
                    cT3.await();
                }
                cT1.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        t1.start();
        t2.start();
        t3.start();
    }
}

方法五:使用TransferQueue

这是一种很取巧的做法。

package com.wuxiaolong.concurrent;

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

/**
 * Description:
 *      两个线程交替输出
 *                 第一个线程:1 2 3 4 5 6 7
 *                 第二个线程: A B C D E F G
 *        输出结果:1A2B3C4D5E6F7G
 *
 * @author 诸葛小猿
 * @date 2021-03-01
 */
public class TestSample4 {

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

        // 阻塞队列,队列里最多只能放一个元素,
        // 而且一个线程调用transfer放元素的时候,如果没有另一个线程调用take去取走元素,则放元素的线下必须阻塞在这个位置
        TransferQueue<Character> tq = new LinkedTransferQueue<>();

        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        Thread t1 = new Thread(()->{
            try{
                for(char temp: nums){
                    tq.transfer(temp); // 当前线程放一个元素进队列,并阻塞在这里,等待另一个线程拿走这个元素,才能往下执行
                    System.out.println(tq.take());// 从队列中拿走另一个线程放的元素,如果没有元素也会阻塞
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"t1");

        Thread t2 = new Thread(()->{

            try{
                for(char temp: chars){
                    System.out.println(tq.take());
                    tq.transfer(temp);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"t2");

        t2.start();
        t1.start();
    }
}