一个面试题:实现两个线程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型变量,来保证共享变量的改变对所有线程的可见性。