今天在敲书里的一段代码时发现一个地方出了问题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自减
}
这样就不会出现重复的现象了
求大神帮忙解释一下这是为什么,谢谢

解决方案 »

  1.   

    一共开启了4个线程,每个线程都会去执行run方法,输出10到1递减。每个线程都会去抢夺执行权,谁抢到谁就去执行。
      

  2.   

    对于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
      

  3.   

    多线程导致的数据不安全!两个解决方案一个就是将要执行的方法用  synchronize修饰二是lock锁锁住要要执行的代码
      

  4.   

    首先,纠正一个问题,就算你把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/
      

  5.   

    说得太好了,请教一下不满足happens before,就会指令重排序,这和打印出重复值有什么关联关系?
      

  6.   

    说得太好了,请教一下不满足happens before,就会指令重排序,这和打印出重复值有什么关联关系?首先,对于多核环境下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自减
             
    }
        }可以解决所有问题
      

  7.   

    非常感谢,写得这么详细认真我想了解的问题重点是 这里面的指令重排序实际上是没有发生吧?但是重复值照样可以出现,因为第二个线程在第一个线程写回修改后的值之前就读取了变量值不能准确地说有没有发生CPU重排序,因为发生CPU重排序后的结果,与现在这个结果的表现是一致的
    又或者说,CPU重排序与JAVA线程栈回写的问题都会导致这个问题的产生。而回写问题,又可以看做是JVM级别的指令重排序。
    如果按照这种说法,JVM有可能重排序。如果JVM没发生重排序,CPU又有可能重排序。那么,这个原因就是指令重排序了= =
      

  8.   

    如果你在JVM基础上再写一个“字节码(指令)解释引擎”,那么在多线程环境下,如果你的VM不保证指令的happens-before话,就会在你的VM里发生一个指令重排序现象。
      

  9.   

    出于优化的目的,指令重排序是很普遍了,多线程环境下如果不额外加以控制(memory barrier),就会产生违背业务需求预期的结果对于业务需求上实际是要求原子化的操作(tk--),而程序默认又不提供这个保证的情况下,势必就会造成问题
      

  10.   

    出于优化的目的,指令重排序是很普遍了,多线程环境下如果不额外加以控制(memory barrier),就会产生违背业务需求预期的结果对于业务需求上实际是要求原子化的操作(tk--),而程序默认又不提供这个保证的情况下,势必就会造成问题是的,这个问题可以引申到数据库的事务上,其实数据库的事务,也可以理解为为了避免“数据库对数据读写的操作进行乱序执行而产生的不一致问题”而实现的一套方案。而数据库读写锁,其实就类似于内存屏障。细思恐极
      

  11.   

    <a onclick="alert(1)">dd</a>