侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

多个线程交替执行(以打印输出为例)的多种实现方式

孔子说JAVA
2022-07-27 / 0 评论 / 0 点赞 / 96 阅读 / 13,100 字 / 正在检测是否收录...

如何实现2个线程交替循环打印,如2个线程交替打印1A2B3C4D5E6F7G,其中线程1输出数字,线程2输出字母。再加大点难度,如何实现多个线程交替循环打印?如线程1打印A,线程2打印B,线程3打印C,要求交替打印,并且可以循环打印,输出结果类似:ABCABCABC。如果是第一次见到这道题,很难在短时间内想出合理的解决方案。 如果只要求交替打印一次的话,实现比较简单,可以用Thread.join()方法,一个线程等待另一个线程执行完成。 现在要求循环打印,就涉及线程间通信,必须要用到锁。

1、两个线程交替执行

两个线程如何交替打印输出?这个问题可以有助于快速理解并发相关的API的使用,以及相关的原理。下面我们用多种方式实现这个问题。

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

1.1 通过加锁和notify()、wait()机制

1.1.1 方式一

public class TwoThreadPrint {
    public static void main(String[] args) {
        MutilThreadPrint m1 = new MutilThreadPrint();
        Thread thread1 = new Thread(m1);
        Thread thread2 = new Thread(m1);
        thread1.setName("thread1");
        thread2.setName("thread2");
        thread1.start();
        thread2.start();
    }
}

class MutilThreadPrint implements Runnable {
    char[] nums = "1234567".toCharArray();
    char[] chars = "ABCDEFG".toCharArray();

    int num = 0;

    @Override
    public void run() {
        synchronized (this) {
            if(num++ % 2 == 0) {
                for(char num: nums) {
                    print(num);
                }
            } else {
                for(char char1: chars) {
                    print(char1);
                }
            }
        }
    }

    public void print(char num) {
        // 唤醒wait()的一个或所有线程
        this.notify();
        System.out.println(Thread.currentThread().getName() + ":" + num);
        try {
            // wait会释放当前的锁,让另一个线程可以进入
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通过加锁和notify()、wait()机制可以有效的实现两个线程交替打印不同的数组,但互斥锁始终会影响性能,效率不高。

1.1.2 方式二

synchronized的原理,多个线程同时抢一把锁,该锁维护一个队列(同步队列),没有争抢到锁的所有线程在同一个同步队列里。

  • wait:该线程释放锁后阻塞,该线程进入另一个队列(等待队列)。
  • notify:随机叫醒等待队列中的任何一个线程。
  • notifyAll: 唤醒等待队列中的所有线程,让他们去争夺同一把锁,谁争抢到谁执行。
public class TwoThreadPrint {

    // 使用Object的notify和wait
    public static void main(String[] args) {
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        Object lock = new Object();

        /**
         * 线程t1和t2同时争抢同一把锁
         */

        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();
    }


}

该例中,线程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. 依次循环

这种方式有两个问题:

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

1.1.3 使用CountDownLatch改进方式二

import java.util.concurrent.CountDownLatch;

public class TwoThreadPrint {
    public static CountDownLatch countDownLatch = new CountDownLatch(1);

    // 使用Object的notify和wait, 改进方法二的实现
    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() 计算调用次数
                    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()  能够阻塞线程 直到调用N次countDownLatch.countDown() 方法才释放线程
                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();
    }
}

1.1.4 使用valatile标志位改进方式二

public class TwoThreadPrint {

    public static volatile boolean flag = true;

    // 使用Object的notify和wait, 改进方法二的实现
    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) {
                    if (flag) {
                        for (char temp : nums) {
                            System.out.println(temp);
                            flag = false;
                            lock.notify();
                            lock.wait();
                        }
                        lock.notify(); //清空等待队列
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                synchronized (lock) {
                    if (!flag) {
                        for (char temp : chars) {
                            System.out.println(temp);
                            flag = true;
                            lock.notify();
                            lock.wait();
                        }
                        lock.notify(); //清空等待队列
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2");

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

1.2 使用LockSupport的unpark和park

LockSupport是concurrent包中一个工具类,是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。本例中我们主要使用 park 和 unpark 方法。

LockSupport常用方法

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。

  • park不需要获取某个对象的锁。
  • 因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理。
  • park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
  • park和unpark的先后顺序并不是那么严格,park和unpark的使用不会出现死锁的情况。unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

LockSupport常用方法

import java.util.concurrent.locks.LockSupport;

/**
 * 两个线程交替打印
 */
public class TwoThreadPrint {
    // 定义为static
    static Thread t1 = null;
    static Thread t2 = null;

    // 使用LockSupport的unpark和park
    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();
    }
}

1.3 使用TransferQueue

这是一种取巧的方式,利用阻塞队列TransferQueue,队列里最多只能放一个元素,而且一个线程调用transfer放元素的时候,如果没有另一个线程调用take去取走元素,则放元素的线程必须阻塞在这个位置。

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

/**
 * 两个线程交替打印
 */
public class TwoThreadPrint4 {
    // 使用TransferQueue
    public static void main(String[] args) {
        // 阻塞队列,队列里最多只能放一个元素,
        // 而且一个线程调用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();
    }
}

1.4 使用ReentrantLock和CountDownLatch

本例中试用了公平锁,避免同一个线程多次获取锁的情况来实现顺序打印,同时使用countDownLatch让两个线程同时开始。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TwoThreadPrint {
    CountDownLatch countDownLatch = new CountDownLatch(1);
    Lock lock = new ReentrantLock(true);
    volatile int num  = 0;

    public void printA() {
        lock.lock();
        try {
            num++;
            System.out.println("current Thread A:" + Thread.currentThread()+num);
        }catch (Exception e){
        }finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            num++;
            System.out.println("current Thread B:" + Thread.currentThread()+num);
        }catch (Exception e){
        }finally {
            lock.unlock();
        }
    }

    // 使用Object的notify和wait, 改进方法二的实现
    public static void main(String[] args) {
        TwoThreadPrint print = new TwoThreadPrint();
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    print.countDownLatch.await();
                }catch (Exception e){
                    e.printStackTrace();
                }
                for (int i = 0; i < 50; i++) {
                    print.printA();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                print.countDownLatch.countDown();;
                for (int i = 0; i < 50; i++) {
                    print.printB();
                }
            }
        }).start();
    }
}

1.5 使用ReentrantLock和Condition

使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。也需要使用CountDownLantch精确的让哪一个线程先执行。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class TwoThreadPrint9 {
    // 使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。
    public static void main(String[] args) {
        char[] nums = "1234567".toCharArray();
        char[] chars = "ABCDEFG".toCharArray();

        /**
         * 每一个Condition都有一个队列,cT1的条件队列里有线程t1,cT2的条件队列里有线程t2。每个队列里的线程都是通过await释放锁,通过signal唤醒。
         *
         * 可以使用多个Condition精确的控制程序的打印.
         *
         * 也需要使用CountDownLantch精确的让哪一个线程先执行
         *
         */
        ReentrantLock lock = new ReentrantLock();
        Condition cT1 = lock.newCondition();
        Condition cT2 = 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);
                    // 通知第一个线程执行,让第二个线程等待
                    cT1.signal();
                    cT2.await();
                }
                cT1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

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

2、多个线程交替执行

本例中主要介绍3个线程交替执行的例子,再多线程的原理是一样的。

2.1 使用ReentrantLock和Condition

同样是使用ReentrantLock和Condition, 也需要使用CountDownLantch精确的让哪一个线程先执行。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeThreadPrint {
    // 使用ReentrantLock和Condition, 是一种比较好的实现方式。可以很方便的使用多个线程交替打印。
    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();
    }
}

2.2 使用ReentrantLock和Condition

线程1打印A,线程2打印B,线程3打印C,要求交替打印,并且可以循环打印。输出结果类似:ABCABCABC

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeThreadPrint1 {
    private static Lock lock = new ReentrantLock();
    private static Condition A = lock.newCondition();
    private static Condition B = lock.newCondition();
    private static Condition C = lock.newCondition();

    private volatile static int count = 0;
    private  static int sum=1;

    static class ThreadA extends Thread {
        @Override
        public void run() {
            try {
                lock.lock();

                for (int i = 0; i < 10; i++) {
                    if (count % 3 != 0){//注意这里是不等于0,也就是说没轮到该线程执行,之前一直等待状态
                        A.await(); //该线程A将会释放lock锁,构造成节点加入等待队列并进入等待状态
                    }
                    System.out.println("-------第"+sum+"次--------");
                    System.out.println("A");
                    count++;
                    B.signal(); // A执行完唤醒B线程
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class ThreadB extends Thread {
        @Override
        public void run() {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    if (count % 3 != 1)
                        B.await();// B释放lock锁,当前面A线程执行后会通过B.signal()唤醒该线程
                    System.out.println("B");
                    count++;
                    C.signal();// B执行完唤醒C线程
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class ThreadC extends Thread {
        @Override
        public void run() {
            try {
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    if (count % 3 != 2)
                        C.await();// C释放lock锁
                    System.out.println("C");
                    count++;
                    sum++;
                    A.signal();// C执行完唤醒A线程
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }
}

2.3 使用Semaphore信号量

前2个多线程循环打印使用Synchronized或者ReentrantLock实现,它们不能控制线程启动后的执行顺序。因为三个线程启动后,都等待CPU调度执行,而CPU调度的顺序又是随机的,所以不能保证线程1先执行。如果想要控制打印顺序,需要使用CountDownLantch。

有个可以控制线程启动后执行顺序,又简单的实现方式,就是用Semaphore(信号量),它可以控制共享资源的访问个数。本例使用Semaphore信号量来精确控制线程的打印顺序。

使用方式:

初始化的时候,指定共享资源的个数

// 初始化一个资源
Semaphore semaphore = new Semaphore(1);

获取资源,获取资源后,semaphore资源个数减1,变成0,其他线程再获取资源的时候就会阻塞等待

semaphore.acquire();

释放资源,semaphore资源个数加1,其他阻塞的线程就可以获取到资源了

semaphore.release();

实现代码

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeThreadPrint2 {
    static class ThreadDemo extends Thread {

        private Semaphore current;
        private Semaphore next;
        private String name;

        /**
         * 构造方法
         *
         * @param current 要获取的当前锁
         * @param next    要释放的下一把锁
         * @param name    打印内容
         */
        public ThreadDemo(Semaphore current, Semaphore next, String name) {
            this.current = current;
            this.next = next;
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try {
                    // 获取当前锁,然后打印
                    current.acquire();
                    System.out.print(name);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 释放下一把锁
                next.release();
            }
        }
    }

    public static void main(String[] args) {
        // 初始化三把锁,只有A锁是可用的
        Semaphore A = new Semaphore(1);
        Semaphore B = new Semaphore(0);
        Semaphore C = new Semaphore(0);

        // 创建并启动三个线程,线程1获取A锁,释放B锁
        new ThreadDemo(A, B, "A").start();

        // 线程2获取B锁,释放C锁
        new ThreadDemo(B, C, "B").start();

        // 线程3获取C锁,释放A锁
        new ThreadDemo(C, A, "C").start();
    }
}
0

评论区