Java多线程连续打印abc实现方法详解

一道编程题如下:

实例化三个线程,一个线程打印a,一个线程打印b,一个线程打印c,三个线程同时执行,要求打印出10个连着的abc。

题目分析:

通过题意我们可以得出,本题需要我们使用三个线程,三个线程分别会打印6次字符,关键是如何保证顺序一定是abc...呢。所以此题需要同步机制来解决问题!

令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC。问题为三线程间的同步唤醒操作,主要的目的就是使程序按ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程,因此本人整理出了三种方式来解决此问题。

一、通过两个锁(不推荐,可读性和安全性比较差)

package com.demo.test;

/**
 * 基于两个lock实现连续打印abcabc....
 * @author lixiaoxi
 *
 */
public class TwoLockPrinter implements Runnable {

  // 打印次数
  private static final int PRINT_COUNT = 10;
  // 前一个线程的打印锁
  private final Object fontLock;
  // 本线程的打印锁
  private final Object thisLock;
  // 打印字符
  private final char printChar;

  public TwoLockPrinter(Object fontLock, Object thisLock, char printChar) {
    this.fontLock = fontLock;
    this.thisLock = thisLock;
    this.printChar = printChar;
  }

  @Override
  public void run() {
    // 连续打印PRINT_COUNT次
    for (int i = 0; i < PRINT_COUNT; i++) {
      // 获取前一个线程的打印锁
      synchronized (fontLock) {
        // 获取本线程的打印锁
        synchronized (thisLock) {
          //打印字符
          System.out.print(printChar);
          // 通过本线程的打印锁唤醒后面的线程
          // notify和notifyall均可,因为同一时刻只有一个线程在等待
          thisLock.notify();
        }
        // 不是最后一次则通过fontLock等待被唤醒
        // 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
        if(i < PRINT_COUNT - 1){
          try {
            // 通过fontLock等待被唤醒
            fontLock.wait();

          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }

      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    // 打印A线程的锁
    Object lockA = new Object();
    // 打印B线程的锁
    Object lockB = new Object();
    // 打印C线程的锁
    Object lockC = new Object();

    // 打印a的线程
    Thread threadA = new Thread(new TwoLockPrinter(lockC, lockA, 'A'));
    // 打印b的线程
    Thread threadB = new Thread(new TwoLockPrinter(lockA, lockB, 'B'));
    // 打印c的线程
    Thread threadC = new Thread(new TwoLockPrinter(lockB, lockC, 'C'));

    // 依次开启a b c线程
    threadA.start();
    Thread.sleep(100); // 确保按顺序A、B、C执行
    threadB.start();
    Thread.sleep(100);
    threadC.start();
    Thread.sleep(100);
  }

}

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

此解法为了为了确定唤醒、等待的顺序,每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是fontLock,就是前一个线程所持有的对象锁,还有一个就是自身对象锁thisLock。主要的思想就是,为了控制执行的顺序,必须要先持有fontLock锁,也就是前一个线程要释放掉前一个线程自身的对象锁,当前线程再去申请自身对象锁,两者兼备时打印,之后首先调用thisLock.notify()释放自身对象锁,唤醒下一个等待线程,再调用fontLock.wait()释放prev对象锁,暂停当前线程,等待再次被唤醒后进入循环。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序,所以需要手动控制他们三个的启动顺序,即Thread.Sleep(100)。

二、通过一个ReentrantLock和三个conditon实现(推荐,安全性,性能和可读性较高)

package com.demo.test;

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

/**
 * 基于一个ReentrantLock和三个conditon实现连续打印abcabc...
 * @author lixiaoxi
 *
 */
public class RcSyncPrinter implements Runnable{

  // 打印次数
  private static final int PRINT_COUNT = 10;
  // 打印锁
  private final ReentrantLock reentrantLock;
  // 本线程打印所需的condition
  private final Condition thisCondtion;
  // 下一个线程打印所需要的condition
  private final Condition nextCondtion;
  // 打印字符
  private final char printChar;

  public RcSyncPrinter(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition,
      char printChar) {
    this.reentrantLock = reentrantLock;
    this.nextCondtion = nextCondition;
    this.thisCondtion = thisCondtion;
    this.printChar = printChar;
  }

  @Override
  public void run() {
    // 获取打印锁 进入临界区
    reentrantLock.lock();
    try {
      // 连续打印PRINT_COUNT次
      for (int i = 0; i < PRINT_COUNT; i++) {
        //打印字符
        System.out.print(printChar);
        // 使用nextCondition唤醒下一个线程
        // 因为只有一个线程在等待,所以signal或者signalAll都可以
        nextCondtion.signal();
        // 不是最后一次则通过thisCondtion等待被唤醒
        // 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
        if (i < PRINT_COUNT - 1) {
          try {
            // 本线程让出锁并等待唤醒
            thisCondtion.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }

      }
    } finally {
      // 释放打印锁
      reentrantLock.unlock();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    // 写锁
    ReentrantLock lock = new ReentrantLock();
    // 打印a线程的condition
    Condition conditionA = lock.newCondition();
    // 打印b线程的condition
    Condition conditionB = lock.newCondition();
    // 打印c线程的condition
    Condition conditionC = lock.newCondition();
    // 实例化A线程
    Thread printerA = new Thread(new RcSyncPrinter(lock, conditionA, conditionB, 'A'));
    // 实例化B线程
    Thread printerB = new Thread(new RcSyncPrinter(lock, conditionB, conditionC, 'B'));
    // 实例化C线程
    Thread printerC = new Thread(new RcSyncPrinter(lock, conditionC, conditionA, 'C'));
    // 依次开始A B C线程
    printerA.start();
    Thread.sleep(100);
    printerB.start();
    Thread.sleep(100);
    printerC.start();
  }
}

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

仔细想想本问题,既然同一时刻只能有一个线程打印字符,那我们为什么不使用一个同步锁ReentrantLock?线程之间的唤醒操作可以通过Condition实现,且Condition可以有多个,每个condition.await阻塞只能通过该condition的signal/signalall来唤醒!这是synchronized关键字所达不到的,那我们就可以给每个打印线程一个自身的condition和下一个线程的condition,每次打印字符后,调用下一个线程的condition.signal来唤醒下一个线程,然后自身再通过自己的condition.await来释放锁并等待唤醒。

三、通过一个锁和一个状态变量来实现(推荐)

package com.demo.test;

/**
 * 基于一个锁和一个状态变量实现连续打印abcabc...
 * @author lixiaoxi
 *
 */
public class StateLockPrinter {
  //状态变量
  private volatile int state=0;

  // 打印线程
  private class Printer implements Runnable {
    //打印次数
    private static final int PRINT_COUNT=10;
    //打印锁
    private final Object printLock;
    //打印标志位 和state变量相关
    private final int printFlag;
    //后继线程的线程的打印标志位,state变量相关
    private final int nextPrintFlag;
    //该线程的打印字符
    private final char printChar;
    public Printer(Object printLock, int printFlag,int nextPrintFlag, char printChar) {
      super();
      this.printLock = printLock;
      this.printFlag=printFlag;
      this.nextPrintFlag=nextPrintFlag;
      this.printChar = printChar;
    }

    @Override
    public void run() {
      //获取打印锁 进入临界区
      synchronized (printLock) {
        //连续打印PRINT_COUNT次
        for(int i=0;i<PRINT_COUNT;i++){
          //循环检验标志位 每次都阻塞然后等待唤醒
          while (state!=printFlag) {
            try {
              printLock.wait();
            } catch (InterruptedException e) {
              return;
            }
          }
          //打印字符
          System.out.print(printChar);
          //设置状态变量为下一个线程的标志位
          state=nextPrintFlag;
          //注意要notifyall,不然会死锁,因为notify只通知一个,
          //但是同时等待的是两个,如果唤醒的不是正确那个就会没人唤醒,死锁了
          printLock.notifyAll();
        }
      }
    }

  }

  public void test() throws InterruptedException{
    //锁
    Object lock=new Object();
    //打印A的线程
    Thread threadA=new Thread(new Printer(lock, 0,1, 'A'));
    //打印B的线程
    Thread threadB=new Thread(new Printer(lock, 1,2, 'B'));
    //打印C的线程
    Thread threadC=new Thread(new Printer(lock, 2,0, 'C'));
    //一次启动A B C线程
    threadA.start();
    Thread.sleep(1000);
    threadB.start();
    Thread.sleep(1000);
    threadC.start();
  }

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

    StateLockPrinter print = new StateLockPrinter();
    print.test();
  }

}

打印结果:

ABCABCABCABCABCABCABCABCABCABC

分析:

状态变量是一个volatile的整型变量,0代表打印a,1代表打印b,2代表打印c,三个线程都循环检验标志位,通过阻塞前和阻塞后两次判断可以确保当前打印的正确顺序,随后线程打印字符,然后设置下一个状态字符,唤醒其它线程,然后重新进入循环。

补充题

三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75,同样的解法:

package com.demo.test;

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

/**
 * 数字打印,三个线程同时打印数字,第一个线程打印12345,第二个线程打印678910 .........
 * @author lixiaoxi
 *
 */
public class NumberPrinter {

  //打印计数器
  private final AtomicInteger counter=new AtomicInteger(0);

  private class Printer implements Runnable{
    //总共需要打印TOTAL_PRINT_COUNT次
    private static final int TOTAL_PRINT_COUNT = 5;
    //每次打印PER_PRINT_COUNT次
    private static final int PER_PRINT_COUNT = 5;
    //打印锁
    private final ReentrantLock reentrantLock;
    //前一个线程的condition
    private final Condition afterCondition;
    //本线程的condition
    private final Condition thisCondtion;

    public Printer(ReentrantLock reentrantLock, Condition thisCondtion,Condition afterCondition) {
      super();
      this.reentrantLock = reentrantLock;
      this.afterCondition = afterCondition;
      this.thisCondtion = thisCondtion;
    }

    @Override
    public void run() {
      //进入临界区
      reentrantLock.lock();
      try {
        //循环打印TOTAL_PRINT_COUNT次
        for(int i=0;i<TOTAL_PRINT_COUNT;i++){
          //打印操作
          for(int j=0;j<PER_PRINT_COUNT;j++){
            //以原子方式将当前值加 1。
            //incrementAndGet返回的是新值(即加1后的值)
            System.out.println(counter.incrementAndGet());
          }
          //通过afterCondition通知后面线程
          afterCondition.signalAll();
          if(i < TOTAL_PRINT_COUNT - 1){
            try {
              //本线程释放锁并等待唤醒
              thisCondtion.await();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
      } finally {
        reentrantLock.unlock();
      }
    }
  }

  public void test() throws InterruptedException {
    //打印锁
    ReentrantLock reentrantLock=new ReentrantLock();
    //打印A线程的Condition
    Condition conditionA=reentrantLock.newCondition();
    //打印B线程的Condition
    Condition conditionB=reentrantLock.newCondition();
    //打印C线程的Condition
    Condition conditionC=reentrantLock.newCondition();

    //打印线程A
    Thread threadA=new Thread(new Printer(reentrantLock,conditionA, conditionB));
    //打印线程B
    Thread threadB=new Thread(new Printer(reentrantLock, conditionB, conditionC));
    //打印线程C
    Thread threadC=new Thread(new Printer(reentrantLock, conditionC, conditionA));
    // 依次开启a b c线程
    threadA.start();
    Thread.sleep(100);
    threadB.start();
    Thread.sleep(100);
    threadC.start();
  }

  public static void main(String[] args) throws InterruptedException {
    NumberPrinter print = new NumberPrinter();
    print.test();
  }
}

运行结果:

2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38
40
42
44
46
48
50
52
54
56
58
60
62
64
66
68
70
72
74

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java打印流原理及实例详解

    这篇文章主要介绍了Java打印流原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输岀方式. PrintStream类 PrintStream类,为其他输出流添加了功能,使他们能够方便的打印各种数据值表示格式. PrintStream类的特点: 只负责数

  • java打印出菱形图案实例详解

    第一步:首先对图像进行解析 想要打印该图形必须要进行多层循环嵌套,分两个部分进行打印. 第一部分为上半部分前四行,他们是递增的关系,后半部分后三行为递减关系,由此可以得出我们需要写两个打的循环.并且由于"*"位置的关系,我们必须带入空格同时打印.所以每个部分需要两个循环控制,即两个大循环每个里面嵌套两个小循环总计四个循环. 第二部:对数字进行分析 在分析之前,我们必须明白外层循环控制行数,内层循环控制列数,因此我们需要分析他的行和列. 示例代码如下: class ForForTest

  • JAVA实现连接本地打印机并打印文件的实现代码

    实现代码一 import javax.print.*; import javax.print.attribute.DocAttributeSet; import javax.print.attribute.HashDocAttributeSet; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; import java.

  • java实现两个线程交替打印的实例代码

    使用ReentrantLock实现两个线程交替打印 实现字母在前数字在后 package com.study.pattern; import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public c

  • java打印指定年月份的日历

    本文实例为大家分享了java打印指定年月份日历的具体代码,供大家参考,具体内容如下 做了一个小的日历打印,没有使用数组,所以不能对日期进行操作,用了单个for循环打印,对初学者来说应该好理解点 工具类: import java.util.Calendar; public class DaysTool { private int year; private int month; public DaysTool(){} public DaysTool(int year ,int month){ th

  • 如何通过Java打印Word文档

    这篇文章主要介绍了如何通过Java打印Word文档,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java 打印Word文档 本文介绍如何在Java程序中通过物理打印机和虚拟打印机来打印Word文档的方法.文中使用了类库Spire.Doc for Java,可通过 官网下载jar文件并导入程序或者直接通过maven仓库安装导入. [示例1]通过物理打印机打印 import com.spire.doc.Document; import com.s

  • Java打印斐波那契前N项的实现示例

    题外 由于idea原因 用注解test无法在控制台上输入所以写死到程序里了,版本都30.9102了为什么还是这样啊,intelJ你们该反思了!!! 用intelJ IDEA的小伙伴有遇到这种测试情况吗,如果项目上有测试用例需要自己单元测试,怎么解决控制台输入问题(@test情况下),直接改main方法的那个就算了.~~ 斐波那契的认识 斐波那契数列前2项为1,从第3项开始为该项的前2项和. eg:1,1,2,3,5,8- f(n)=f(n-1)+f(n-2) 代码参考 import org.ju

  • java自定义异常打印内容详解

    本文实例为大家分享了java自定义异常打印内容的具体代码,供大家参考,具体内容如下 背景:在开发中,我们可能会使用到自定义异常,但是,这个自定义异常在打印日志时,往往打印的内容比较多. 1.自定义异常打印内容 可以看到,虽然我们使用的是自定义异常,但是当抛出时,还是会打印出堆栈的全部信息. 2.查看源码 通过查看源码,我们可以得知,当抛出异常时,程序会调用此异常的fillInStackTrace方法,但是,大部分异常都没有对该方法做处理.基本是调用super的方法. 可以看到,此方法的父类实现,

  • Java多线程连续打印abc实现方法详解

    一道编程题如下: 实例化三个线程,一个线程打印a,一个线程打印b,一个线程打印c,三个线程同时执行,要求打印出10个连着的abc. 题目分析: 通过题意我们可以得出,本题需要我们使用三个线程,三个线程分别会打印6次字符,关键是如何保证顺序一定是abc...呢.所以此题需要同步机制来解决问题! 令打印字符A的线程为ThreadA,打印B的ThreadB,打印C的为ThreadC.问题为三线程间的同步唤醒操作,主要的目的就是使程序按ThreadA->ThreadB->ThreadC->Thr

  • Java多线程案例之阻塞队列详解

    目录 一.阻塞队列介绍 1.1阻塞队列特性 1.2阻塞队列的优点 二.生产者消费者模型 2.1阻塞队列对生产者的优化 三.标准库中的阻塞队列 3.1Java提供阻塞队列实现的标准类 3.2Blockingqueue基本使用 四.阻塞队列实现 4.1阻塞队列的代码实现 4.2阻塞队列搭配生产者与消费者的代码实现 一.阻塞队列介绍 1.1阻塞队列特性 阻塞队列特性: 一.安全性 二.产生阻塞效果 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有

  • Java AtomicInteger类的使用方法详解

    首先看两段代码,一段是Integer的,一段是AtomicInteger的,为以下: public class Sample1 { private static Integer count = 0; synchronized public static void increment() { count++; } } 以下是AtomicInteger的: public class Sample2 { private static AtomicInteger count = new AtomicIn

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • Java多线程之哲学家就餐问题详解

    一.题目 教材提供一个哲学家就餐问题的解决方案的框架.本问题要求通过pthreads 互斥锁来实现这个解决方案. 哲学家 首先创建 5 个哲学家,每个用数字 0~4 来标识.每个哲学家作为一个单独的 线程运行. 可使用 Pthreads 创建线程.哲学家在思考和吃饭之间交替.为了模拟这两种活动,请让线程休眠 1 到 3 秒钟.当哲学家想要吃饭时,他调用函数: pickup_forks(int philosopher _number) 其中,philosopher _number 为想吃饭哲学家的

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java多线程同步工具类CountDownLatch详解

    目录 简介 核心方法 CountDownLatch如何使用 CountDownLatch运行流程 运用场景 总结 简介 CountDownLatch是一个多线程同步工具类,在多线程环境中它允许多个线程处于等待状态,直到前面的线程执行结束.从类名上看CountDown既是数量递减的意思,我们可以把它理解为计数器. 核心方法 countDown():计数器递减方法. await():使调用此方法的线程进入等待状态,直到计数器计数为0时主线程才会被唤醒. await(long, TimeUnit):在

  • Java多线程模拟银行系统存钱问题详解

    目录 一.题目描述 二.解题思路 三.代码详解 多学一个知识点 一.题目描述 题目:模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱. 实现:使用特殊域变量volatile实现同步. 二.解题思路 创建一个类:SynchronizedBankFrame,继承JFrame类 写一个内部类Bank 定义一个account变量,来表示账户. deposit():一个存钱的方法 getAccount():显示账户余额的方法. 写一个内部类Transfer,实现Runnable接口 在run方法

随机推荐