一个面试题:实现两个线程A,B交替输出偶数和奇数

问题:创建两个线程A和B,让他们交替打印0到100的所有整数,其中A线程打印偶数,B线程打印奇数

这个问题配合java的多线程,很多种实现方式

在具体实现之前,首先介绍一下java并发编程中共享变量的可见性问题。

可见性问题:

在java内存模型(JMM,java Memory Model)中定义了程序中各种共享变量的访问规则。

这里的共享变量指的是可以在线程之间共享的变量,包括实例字段,静态字段和构成数组对象的元素。

不包括局部变量和方法参数(这些都是在虚拟机栈中,是线程私有的)

在java内存模型中,规定所有的变量都保存在主内存中(JVM内存中的一个空间)。

此外,对于每个线程,都拥有自己的工作内存,在工作内存中存储了该线程使用的共享变量的主内存副本(从主内存中拷贝过来的)。

每个线程只能在工作内存中对共享变量的副本进行操作(读,赋值),不能直接读写主内存中的数据。

各个线程之间也无法访问对方工作内存中的变量副本,所有的线程只能通过主内存来完成变量的值传递。

在java内存模型中,定义了8中原子操作来完成工作内存与主内存之间的拷贝与同步。

这里重点关注一下,两个线程同时读取与修改同一个共享变量的问题。

当我们创建了一个静态变量之后,它就会被保存在主内存中。如果有两个线程A,B要访问这个静态变量并对其进行修改,线程会读取(read操作)这个变量的值并放到(load操作)线程的工作内存中的变量,线程在执行完修改指令后,将修改后的值赋值给(assign操作)工作内存中的变量,然后执行store操作将工作内存中变量的值传送到主内存中,然后使用write操作将传递过来的值放入到主内存变量中。

read操作和load操作必须按顺序执行,store操作和write操作也必须按顺序执行。

但是这里存在一个问题,即变量的可见性,read/load和store/write虽然是按顺序执行,但却不是连续执行的,也就是说工作内存中的变量值在修改完并复制给工作内存中的变量后,并不是立即执行store/write操作的,这就导致主内存中的变量值无法实时的得到更新。这时候如果另一个线程要读取主内存中该变量的值,仍然是旧值,无法读取到新值。只有在回写完成,才能在主内存中读取到新的值。

这里我们用一个例子来展示变量的可见性问题,使用错误的方法来实现两个线程交替输出偶数和奇数

方案1:使用自旋检查(循环检查)来实现线程交替输出

/*

定义两个线程A和B,让两个线程按顺序交替输出偶数和奇数(A输出偶数,B输出奇数)

*/

public class ThreadNum {

  public static int flag = 0; //定义一个静态全局变量,作为标志位

  public static void main(String[] args) {
Thread r1 = new Thread( //线程1用来输出偶数
()->{
while(flag<=100){
while(flag%2==1&&flag<=100); //循环判断,如果flag是偶数就跳出循环去flag
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++;//自增1,flag变成奇数
}
}
);
Thread r2 = new Thread(//线程B用来输出奇数
()->{
while(flag<100){
while(flag%2==0&&flag<100);//循环判断,如果flag为奇数就跳出循环去打印flag
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++; //自增1,flag变成偶数
}
}
);
r1.setName("线程A");
r2.setName("线程B");
r1.start();
r2.start();
}
} /*
程序运行结果:
线程A打印:0
线程B打印:1
线程A打印:2
线程B打印:3
线程A打印:4
线程B打印:5
在这里死循环,无法继续打印
*/

这个程序在运行时可能会死循环,两个线程会在while(flag%2==0&&flag<=100);while(flag%2==1&&flag<100);这里死循环。分析一下原因:

静态变量flag是一个普通变量,无法保证对所有的线程的可见性。

所以当线程B在打印出flag的值5之后,执行自增操作,将自己工作内存内的变量值更新为6,但是并没有立即更新到主内存中(应为工作内存中的值更新后并不会直接写入到主内存中),即便是更新到了主内存中,但是java内存模型没有规定主内存中变量值发生改变后会立即更新线程工作内存中对应的变量副本的值,此时线程A在执行循环,它读取的flag值始终是工作内存中的旧值5,导致无法跳出循环。

这样对于flag的值:

线程A工作内存中:flag=5,仍然为旧值,无法跳出循环while(flag%2==1&&flag<100);

线程B工作内存中:flag从5变成6,然后执行循环while(flag%2==0&&flag<=100);,同样无法跳出

主内存中:flag开始值为5,当从线程B中得到更新后的值,变成6.但是不会主动将更新后的值传递给线程B。

为了解决这个变量的可见性问题,java引入了volatile型变量,来保证共享变量的改变对所有线程的可见性。

当一个线程修改了这个变量的值,新的值对其他线程来说是立即可见的。当其他线程读取自己被volatile修饰的该变量时,会直接从主存中读取数据从而刷新自己工作内存中的数据,保证读取到最新的值。

修改后的实现方法:

方案2:使用volatile型变量和自旋检查来实现交替输出:

/*
定义两个线程A和B,让两个线程按顺序交替输出偶数和奇数(A输出偶数,B输出奇数)
*/
public class ThreadNum {
public static volatile int flag = 0; //使用volatile 来保证flag对两个线程的可见性
private static final int N = 200;
public static void main(String[] args) {
//AtomicInteger flag = new AtomicInteger(0); Thread r1 = new Thread( //线程A用来输出偶数
()->{
while(flag<=N){
while(flag%2==1&&flag<=N); //循环判断当前flag是否是偶数
//lock.lock(); //先获取锁
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++;//自增1
//lock.unlock();//释放锁
}
}
); Thread r2 = new Thread(//线程B用来输出奇数
()->{
while(flag<N){
while(flag%2==0&&flag<N);
//lock.lock();
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++;
// lock.unlock();
}
}
);
r1.setName("线程A");
r2.setName("线程B");
r1.start();
r2.start();
}
} /**
可以成功实现输出
线程A打印:0
线程B打印:1
线程A打印:2
线程B打印:3
线程A打印:4
线程B打印:5
线程A打印:6
线程B打印:7
线程A打印:8
线程B打印:9
线程A打印:10
...
线程A打印:194
线程B打印:195
线程A打印:196
线程B打印:197
线程A打印:198
线程B打印:199
线程A打印:200
**/

在上面的方案中,线程之间通过自旋检查来保证并发性,也就是当过某个线程发现当前自己无法进行输出时,他会循环检查对应的条件,知道条件满足,线程执行输出操作。

在这种方案中,线程没有被阻塞,时钟在占用CPU执行循环。

另一种实现方案是利用互斥锁来保证线程之间的并发性(有序执行),同一时刻,只有获取到锁的线程才能对变量进行操作(主要是修改)。而无法获得锁的线程会堵塞,知道锁被释放,他们才有机会获取锁。

同时,采用条件变量(Condition)的await()和signal()方法来实现实现两个线程的交替输出

对于获取到锁lock的线程,如果当前无法满足输出要求(比如flag不是奇数),该线程会被挂起(await()),同时将锁释放并等待。

而其他可以进行输出的线程,在操作完之后,会调用signal()方法或者signalAll()方法,来唤醒被挂起的线程,同时自己释放锁,使得被唤醒的线程可以再次尝试获取锁,并错上次被挂起的位置继续执行。

采用volatile 型变量和ReentrantLock锁以及Condition条件变量的实现方案

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; /*
定义两个线程A和B,让两个线程按顺序交替输出偶数和奇数(A输出偶数,B输出奇数)
*/
public class ThreadPrint2 {
public static volatile int flag = 0; //volatile修饰变量保证对线程的可见性
private static final int N = 50;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();//声明一个锁对象,
Condition c = lock.newCondition();//创建这个锁对应的一个条件变量 Thread r1 = new Thread( //线程A用来输出偶数
()->{
while(flag<=N){
try{
lock.lock();//首先获取锁
if(flag%2==1){//如果当前值为奇数,就将线程阻塞挂起
c.await();//将当前线程挂起
}
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++;//自增1
c.signal(); //唤醒其他因为这个条件而被被挂起的线程
}catch(InterruptedException e){
e.printStackTrace();
}finally{
//这里必须在finally代码块中来释放锁,防止应其他异常导致线程中断,但是锁 //却没有释放,导致出现死锁
lock.unlock();
}
}
}
); Thread r2 = new Thread(//线程B用来输出奇数
()->{
while(flag<N){
try{
lock.lock();//首先获取锁
if(flag%2==0){//如果当前值为偶数,就将线程阻塞挂起
c.await();
}
System.out.println(Thread.currentThread().getName()+"打印:"+flag);
flag++;//自增1
c.signal();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
);
r1.setName("线程A");
r2.setName("线程B");
r1.start();
r2.start();
}
}

参考书籍:java并发编程的艺术,深入理解Java虚拟机

java面试题:多线程交替输出偶数和奇数的更多相关文章

  1. Java面试题-多线程

    1. java中有几种方法可以实现一个线程? 多线程有两种实现方法,分别是继承Thread类与实现Runnable接口. 这两种方法的区别是,如果你的类已经继承了其它的类,那么你只能选择实现Runna ...

  2. [ Java面试题 ]多线程篇

    1.什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速.比如,如果一个线程完成一 ...

  3. Java基础之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  4. Java面试题阶段汇总

    初级面试题   Java面试题-基础篇一 Java面试题-基础篇二 Java面试题-集合框架篇三 Java面试题-基础篇四 Java面试题-基础篇五 Java面试题-javaweb篇六 Java面试题 ...

  5. 最全最新java面试题系列全家桶(带答案)

    最全最新java面试题系列全家桶(带答案) 置顶 2019年04月06日 22:40:28 青春季风暴 阅读数 14082 文章标签: java面试题技术栈 更多 分类专栏: 面试   版权声明:本文 ...

  6. java面试题:已知一个数组[2,4,6,2,1,5],将该数组进行排序(降序,不能用工具类进行排序),创建两条线程交替输出排序后的数组,线程名自定义

    package com.swift; import java.util.Arrays; import java.util.Comparator; public class ArrayThread_Te ...

  7. Java面试题:Java中怎么样实现多线程

    方法一:继承 Thread 类,覆盖方法 run(),我们在创建的 Thread 类的子类中重写 run() ,加入线程所要执行的代码即可. 下面是一个例子: public class MyThrea ...

  8. java并发与多线程面试题与问题集合

    http://www.importnew.com/12773.html     https://blog.csdn.net/u011163372/article/details/73995897    ...

  9. [原]Java面试题-将字符串中数字提取出来排序后输出

    [Title][原]Java面试题-将字符串中数字提取出来排序后输出 [Date]2013-09-15 [Abstract]很简单的面试题,要求现场在纸上写出来. [Keywords]面试.Java. ...

随机推荐

  1. 突发!美商务部宣布封禁微信,TikTok——面对科技封锁,如何应对

    刚刚美国商务部忽然发布了这则新闻,为了回应特朗普2020年8月6号的行政令,称这些应用程序存在安全威胁. 禁令中称,自2020年9月20日起,美国政府将: 1 禁止通过美国在线移动应用程序商店分发或维 ...

  2. C#开发PACS医学影像三维重建(一):使用VTK重建3D影像

    VTK简介: VTK是一个开源的免费软件系统,主要用于三维计算机图形学.图像处理和可视化.Vtk是在面向对象原理的基础上设计和实现的,它的内核是用C++构建的. 因为使用C#语言开发,而VTK是C++ ...

  3. 想要搭建个论坛?Guide哥调研了100来个 Java 开源论坛系统,发现这 5 个最好用!

    大家好!我是 Guide 哥,Java 后端开发.一个会一点前端,喜欢烹饪的自由少年. 最近有点小忙.但是,由于前几天答应了一位读者自己会推荐一些开源的论坛系统,所以,昨晚就简单地熬了个夜,对比了很多 ...

  4. 趣图:调试bug进行时

      扩展阅读 趣图:大神写实,左脚程序继续运行,右脚程序调试 趣图:Bug 多了,总有一个会把你坑了 趣图:领导在旁,只求代码无Bug

  5. kmt字符串匹配

    # -*- coding:utf-8 -*-class StringPattern: def findAppearance(self, A, lena, B, lenb): pos=0 tmp = 0 ...

  6. Mysql安装(解压版)

    文章首推 刷网课请点击这里 刷二级请点击这里 论文查重请点击这里 WIFI破解详细教程 今日主题:Mysql安装(解压版) 环境 系统:windows10 版本:mysql5.7.29 安装过程 1. ...

  7. 开源两个spring api项目

    开源两个spring api项目 转载请注明出处: https://www.cnblogs.com/funnyzpc/p/13762616.html 工作也有五年有余了,中间一直迫于时间或能力没从零开 ...

  8. Typore的简单用法

    1 无序列表使用方法 +号和空格一起按就可以写出这个点 2 有序列表使用方法 .先写1.然后打个空格就再回车 3 使用#和空格表示一级标题 一级标题 4 使用##和空格表示二级标题 5 二级标题 6 ...

  9. centos 7 安装docker 常用指令

    什么是docker l  使用最广泛的开源容器引擎 l  一种操作系统级的虚拟化技术 l  依赖于Linux内核特性:Namespace和Cgroups l  一个简单的应用程序打包工具 docker ...

  10. VMware ESXi 客户端连接控制台时,提示“VMRC 控制台连接已断开...正在尝试重新连接”的解决方法

    故障描述: 通过 VMware vSphere Client 连接到安装 VMware ESXi 虚拟环境的主机时,当启动其中的虚拟机后,无法连接到控制台. 选择"控制台"时,控制 ...