Java的重排序影响线程调用顺序

  你所认为的程序运行顺序是什么样的呢?是JVM照着程序编写的顺序运行吗?
  正确答案是:不是的。
  今天在学习多线程的时候遇到了一个问题。代码本身的功能是为了验证synchronized关键字对于线程锁的作用。用synchronized锁住Example类的非静态方法,可以让两个线程不同步执行两个方法。代码如下:

/**
 * Created by K Lin on 2017/7/10.
 */
public class Demo1 {


    public static void main(String[] args) {
        Example example = new Example();

        Thread t1 = new TheThread(example);

        Thread t2 = new TheThread2(example);

        t1.start();

        t2.start();
    }
}

class Example {

    public synchronized void execute() {

        for (int i = 0; i < 5; i++) {

            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("lin: " + i);
        }
    }

    public synchronized void execute2() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("kai: " + i);
        }
    }
}

class TheThread extends Thread {

    private Example example;

    public TheThread(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute();
    }
}

class TheThread2 extends Thread {

    private Example example;

    public TheThread2(Example example) {
        this.example = example;
    }

    @Override
    public void run() {
        this.example.execute2();
    }
}

  预期的输出是这个样子的:
  Java的重排序影响线程调用顺序_第1张图片
  
  按程序编写的先后顺序进行执行,这是意想得到的。但是,其中一次运行出现的结果却令我百思不得其解:
  Java的重排序影响线程调用顺序_第2张图片
  
  为什么会出现这个情况呢?Google了半天的我知道了有指令重排序这种东西。也知道了happen-before原则。
  

  重排序在多线程环境下出现的概率是很高的。在关键字上有volatile和synchronized可以禁用重排序,除此之外还有一些规则,也正是这些规则,使得我们在平时的编程工作中没有感受到重排序的坏处。

  • 程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。

  • 监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock操作。

  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。

  • 线程启动规则(Thread Start Rule):Thread独享的start()方法先行于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule):线程中的每个操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值检测到线程已经终止执行。

  • 线程中断规则(Thread Interruption Rule):对线程interrupte()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否已中断。

  • 对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

      正是以上这些规则保障了happen-before的顺序,如果不符合以上规则,那么在多线程环境下就不能保证执行顺序等同于代码顺序,也就是“如果在本线程中观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,则不符合以上规则的都是无序的”

  为了遵守上面的规范,我在t1.start();语句后添加了如下代码:

try
{
    t1.join();
 }
  catch (InterruptedException e)
  {
    e.printStackTrace();
  }

  即符合了线程终止规则。(其实还有多种方法)在之后的不下50次的程序运行中都没有再出现错误结果。
  


  但是,在继续学习之后,我又发现,其实指令重排序并不是影响这个程序运行结果的关键,关键原因竟是:线程启动过程不具备原子性,且进程的执行是随机的!。这会导致一个问题,当t1线程处于启动阶段,它有可能在线程启动准备完后,被t2线程挤掉,停止执行,而t2线程启动完成之后,才来继续完成t1的线程执行操作。这才是结果输出顺序不同的原因!
  而前面所做的修改方法依旧是正确的,因为它确保了t2的执行必须处于t1完成之后。所以不会再发生顺序颠倒的情况!


  因此,遵守好happen-before原则以及运用好synchronized、Lock以及volatile修饰符在多线程编程中是十分之重要的!

你可能感兴趣的:(Java进阶)