今天在敲书里的一段代码时发现一个地方出了问题public class Test {
public static void main(String[] args) {
MyThreads t=new MyThreads();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class MyThreads implements Runnable{
int tk=10;
public void run(){
for(;tk>0;tk--){
System.out.println(Thread.currentThread().getName()+" "+tk);
}
}
}
这里出现了一个问题,就是这个程序跑起来会有一些重复的地方。但是如果将run中的内容改一下 while(tk>0){
System.out.println(Thread.currentThread().getName()+" "+tk--);//重点是在这里让tk自减
}
这样就不会出现重复的现象了
求大神帮忙解释一下这是为什么,谢谢
public static void main(String[] args) {
MyThreads t=new MyThreads();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class MyThreads implements Runnable{
int tk=10;
public void run(){
for(;tk>0;tk--){
System.out.println(Thread.currentThread().getName()+" "+tk);
}
}
}
这里出现了一个问题,就是这个程序跑起来会有一些重复的地方。但是如果将run中的内容改一下 while(tk>0){
System.out.println(Thread.currentThread().getName()+" "+tk--);//重点是在这里让tk自减
}
这样就不会出现重复的现象了
求大神帮忙解释一下这是为什么,谢谢
第一步:int old = tk;
第二步:int temp = tk-1;
第三步:tk = temp;
再然后输入语句时:System.out.println(old);假如说tk为10,线程1执行到第二步,然后这时候线程2进来了,这时候tk是不是还是10,然后线程2一直执行下去到输出10,然后线程1被执行了,这时候输入的是old,线程1的old是几?不还是10吗,所以输出10
如楼上的伙伴们说的一样,这个原因就是因为多线程下的线程安全问题。
首先,先看下你的第一种情况:class MyThreads implements Runnable{
int tk=10;
public void run(){
for(;tk>0;tk--){
System.out.println(Thread.currentThread().getName()+" "+tk);
}
}
}这段代码等价于:while (tk > 0) {
System.out.println(Thread.currentThread().getName() + " " + tk);
tk--;
}然后,你的这段代码:while(tk>0){
System.out.println(Thread.currentThread().getName()+" "+tk--);//重点是在这里让tk自减
}等价于while(tk>0){
int curVal = tk;
tk--;
System.out.println(Thread.currentThread().getName()+" "+ curVal);//重点是在这里让tk自减
}可以看到,这两段代码对于tk的读写操作都没有任何happens-before保证。所以在多线程环境下会都存在线程安全问题。
如果深入讨论的话,因为tk这个变量属于基本数据类型,所以在构建run()这个方法的时候,会将tk的值压栈。这就导致实际操作的tk其实是方法栈上的tk的副本,只有在对该变量发生写操作后,才会将方法栈上的值写入到原始的tk上。
注意,上面说的是“写操作后”,而不是“写操作时”。这就使得别的线程“有机可乘”,可以在新值写入tk之前,将旧的tk值读入栈中,导致lz所说的“重复值”的出现。
下面是对这个结论的论证:
为了减少反编译后的字节码的干扰项,所以对第一种情况的代码做了一个精简: static class MyThreads implements Runnable {
int tk = 10; public void run() {
for (; tk > 0; tk--) {
a(tk);
}
}
public void a(int val){
}
}下面我们来看看run()方法的字节码:public void run();
Code:
0: goto 21
3: aload_0
4: aload_0
5: getfield #14; //Field tk:I
8: invokevirtual #21; //Method a:(I)V
11: aload_0
12: dup
13: getfield #14; //Field tk:I
16: iconst_1
17: isub
18: putfield #14; //Field tk:I
21: aload_0
22: getfield #14; //Field tk:I
25: ifgt 3
28: return注意里面的getfield与putfield的时机。可以看到,对于tk++,分三步,第一步getfield获取原始值,压栈。第二步isub对栈上的值减一,第三步putfield将栈上的值写回到tk中。这三个步骤不满足happens-before,线程间执行可能会发生指令重排序,导致结果出现问题。
那么如何解决呢?最简单的办法就是加入同步块了,因为,jvm可以保证
1.同步块的进入是happens-before的
2.因为1的保证,所以同步块内的代码执行是happens-before的。
也就是说,加了同步块以后,getfield isub putfield这三个指令会转化成一个原子操作,就不会让别的线程“有机可乘”啦。多线程同步问题各种奇怪现象,其本质上就是一个指令对于数据的读写操作顺序得不到保障(happens-before)的问题,搞懂happens-before后,就好理解多了。以下是参考资料:
http://ifeve.com/easy-happens-before/
1.所有的写操作对后续的读操作要可见
2.写操作必须在所有的读操作完成后才会被执行
但是这么做会付出比较昂贵的代价,所以现在的CPU都默认不提供这种保障。这也就是happend-before问题产生的原因。好,既然知道happens-before的原因,也知道cpu默认不会遵循happens-before,那么我们就可以综合原因来谈谈多核CPU执行多个线程会出现什么问题:
首先,多核环境下的线程调度,会出现以下两种情况:
1.一个程序的多个线程被调度到一个CPU核心上
2.一个程序的多个线程被调度到多个CPU核心上
好,如果这多个线程之间不存在共享数据的话,怎么调度都是没问题的,但是,一旦这些线程之间存在共享数据,而存在共享数据的线程恰好处于情况2下,那么,因为happens-before得不到保障,假设tA与tB线程共享d资源,那么会存在以下情况:
1.理想情况下:tA读取d。tA操作d,tA写回d。tB读取d,tB操作d,tB写回d。这种情况下,happens-before得到保障,程序正常执行。
2.非理想情况下:tA读取d,tB读取d(脏数据),tA操作d,tB操作d,tA写回d,tA读取d(脏数据),tB写回d,tB读取d(脏数据)
可以看到,由于tB读取d的操作没有在tA写入d之后执行,所以后续对于d的读取几乎都是错误的数据,多线程问题产生。结合到lz的例子,因为对于数据修改的操作在tk--上,而在tk--之前,有一个数据的读取操作(println),如果将这两个操作套入上面所说的非理想情况,就会出现重复值的问题。以下是几个例子: while(tk>0){ //no happens-before
int curVal = tk; //no happens-before
synchronized (this) { //happens-before bolck
tk--;
System.out.println(Thread.currentThread().getName()+" "+ curVal);//重点是在这里让tk自减
}
}可以解决while(tk>0)不生效问题但不能解决重复的问题。 while(tk>0){ //no happens-before
synchronized (this) { //happens-before bolck
int curVal = tk;
tk--;
System.out.println(Thread.currentThread().getName()+" "+ curVal);//重点是在这里让tk自减
}
}可以解决重复问题,但不能解决while(tk>0)的问题 while(true){
synchronized (this) { //happens-before block
int curVal = tk;
if(curVal == 0){
break;
}
tk--;
System.out.println(Thread.currentThread().getName()+" "+ curVal);//重点是在这里让tk自减
}
}可以解决所有问题
又或者说,CPU重排序与JAVA线程栈回写的问题都会导致这个问题的产生。而回写问题,又可以看做是JVM级别的指令重排序。
如果按照这种说法,JVM有可能重排序。如果JVM没发生重排序,CPU又有可能重排序。那么,这个原因就是指令重排序了= =