关于java中Runable的多线程控制一个变量的问题

群居的山羊 2017-03-24 11:22:28
今天在敲书里的一段代码时发现一个地方出了问题

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自减
}

这样就不会出现重复的现象了
求大神帮忙解释一下这是为什么,谢谢
...全文
804 22 打赏 收藏 转发到动态 举报
写回复
用AI写文章
22 条回复
切换为时间正序
请发表友善的回复…
发表回复
群居的山羊 2017-08-03
  • 打赏
  • 举报
回复
<a onclick="alert(1)">dd</a>
夜半钟声_ 2017-03-27
  • 打赏
  • 举报
回复
引用 4楼我是你的主体 的回复:
[quote=引用 3 楼 夜半钟声_的回复:]一共开启了4个线程,每个线程都会去执行run方法,输出10到1递减。每个线程都会去抢夺执行权,谁抢到谁就去执行。
那为什么会出现重复数字呢[/quote]这是多线程并发的安全问题,线程1 抢到执行权,执行输出语句,输出10,在还没有执行--时,线程3抢到执行权,此时变量值还是10.所以也输出10
China小嘿 2017-03-27
  • 打赏
  • 举报
回复
上同步锁 就行
hhh丶hhhao 2017-03-27
  • 打赏
  • 举报
回复
多线程导致的数据不安全!两个解决方案一个就是将要执行的方法用 synchronize修饰二是lock锁锁住要要执行的代码
群居的山羊 2017-03-27
  • 打赏
  • 举报
回复
没想到能看到大神级评论,喜不自禁
dracularking 2017-03-27
  • 打赏
  • 举报
回复
引用 18 楼 zs808 的回复:
是的,这个问题可以引申到数据库的事务上,其实数据库的事务,也可以理解为为了避免“数据库对数据读写的操作进行乱序执行而产生的不一致问题”而实现的一套方案。而数据库读写锁,其实就类似于内存屏障。 细思恐极
哈哈,没错,不同尺度之上存在着偶然却貌似又必然的重复。好像某种本质快要呼之欲出了。
zs808 2017-03-27
  • 打赏
  • 举报
回复
引用 17 楼 dracularking 的回复:
[quote=引用 15 楼 zs808 的回复:] 如果你在JVM基础上再写一个“字节码(指令)解释引擎”,那么在多线程环境下,如果你的VM不保证指令的happens-before话,就会在你的VM里发生一个指令重排序现象。
出于优化的目的,指令重排序是很普遍了,多线程环境下如果不额外加以控制(memory barrier),就会产生违背业务需求预期的结果 对于业务需求上实际是要求原子化的操作(tk--),而程序默认又不提供这个保证的情况下,势必就会造成问题[/quote] 是的,这个问题可以引申到数据库的事务上,其实数据库的事务,也可以理解为为了避免“数据库对数据读写的操作进行乱序执行而产生的不一致问题”而实现的一套方案。而数据库读写锁,其实就类似于内存屏障。 细思恐极
dracularking 2017-03-27
  • 打赏
  • 举报
回复
引用 15 楼 zs808 的回复:
如果你在JVM基础上再写一个“字节码(指令)解释引擎”,那么在多线程环境下,如果你的VM不保证指令的happens-before话,就会在你的VM里发生一个指令重排序现象。
出于优化的目的,指令重排序是很普遍了,多线程环境下如果不额外加以控制(memory barrier),就会产生违背业务需求预期的结果 对于业务需求上实际是要求原子化的操作(tk--),而程序默认又不提供这个保证的情况下,势必就会造成问题
early的牙膏兄 2017-03-27
  • 打赏
  • 举报
回复
难得遇到大神级回复
zs808 2017-03-27
  • 打赏
  • 举报
回复
如果你在JVM基础上再写一个“字节码(指令)解释引擎”,那么在多线程环境下,如果你的VM不保证指令的happens-before话,就会在你的VM里发生一个指令重排序现象。
zs808 2017-03-27
  • 打赏
  • 举报
回复
引用 13 楼 dracularking 的回复:
[quote=引用 12 楼 zs808 的回复:]
非常感谢,写得这么详细认真 我想了解的问题重点是 这里面的指令重排序实际上是没有发生吧?但是重复值照样可以出现,因为第二个线程在第一个线程写回修改后的值之前就读取了变量值[/quote] 不能准确地说有没有发生CPU重排序,因为发生CPU重排序后的结果,与现在这个结果的表现是一致的。。。 又或者说,CPU重排序与JAVA线程栈回写的问题都会导致这个问题的产生。而回写问题,又可以看做是JVM级别的指令重排序。 如果按照这种说法,JVM有可能重排序。如果JVM没发生重排序,CPU又有可能重排序。那么,这个原因就是指令重排序了= =
dracularking 2017-03-27
  • 打赏
  • 举报
回复
引用 12 楼 zs808 的回复:
非常感谢,写得这么详细认真 我想了解的问题重点是 这里面的指令重排序实际上是没有发生吧?但是重复值照样可以出现,因为第二个线程在第一个线程写回修改后的值之前就读取了变量值
zs808 2017-03-27
  • 打赏
  • 举报
回复
引用 11 楼 dracularking 的回复:
[quote=引用 10 楼 zs808 的回复:]
说得太好了,请教一下不满足happens before,就会指令重排序,这和打印出重复值有什么关联关系?[/quote] 首先,对于多核环境下CPU对于数据的操作,安全的情况下应该是: 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自减
			         
				}
		    }
可以解决所有问题
dracularking 2017-03-27
  • 打赏
  • 举报
回复
引用 10 楼 zs808 的回复:
说得太好了,请教一下不满足happens before,就会指令重排序,这和打印出重复值有什么关联关系?
zs808 2017-03-27
  • 打赏
  • 举报
回复
首先,纠正一个问题,就算你把tk--放在println里,仍然会出现数字重复的现象。
如楼上的伙伴们说的一样,这个原因就是因为多线程下的线程安全问题。
首先,先看下你的第一种情况:

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/
mayiaihuangluo 2017-03-26
  • 打赏
  • 举报
回复
这个要加锁才行,并且要注意共用的资源和锁对象要一致
qq_29242127 2017-03-26
  • 打赏
  • 举报
回复
线程并发问题.加锁就可以了
艾恩iron 2017-03-26
  • 打赏
  • 举报
回复
对于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
群居的山羊 2017-03-26
  • 打赏
  • 举报
回复
引用 3 楼 夜半钟声_的回复:
一共开启了4个线程,每个线程都会去执行run方法,输出10到1递减。每个线程都会去抢夺执行权,谁抢到谁就去执行。
那为什么会出现重复数字呢
夜半钟声_ 2017-03-26
  • 打赏
  • 举报
回复
一共开启了4个线程,每个线程都会去执行run方法,输出10到1递减。每个线程都会去抢夺执行权,谁抢到谁就去执行。
加载更多回复(2)

62,628

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧