Java 线程和 volatile 解释

最近开始学习 Java,因此记录一些 Java 的知识点。这篇是一些关于 Java 线程的文章。java

Java 支持多线程,Java 中建立线程的方式有两种:面试

  • 继承 Thread 类,重写 run 方法。
  • 实现 Runnable 接口,实现 run 方法。
// 继承 Thread 类
class ThreadDemo extends Thread {

    @Override
    public void run() {
        System.out.println("一个简单的例子就须要这么多代码...");
    }
}



// 实现 Runnable 接口
class RunnableDemo implements Runnable {
    public void run() {
        System.out.println("一个简单的例子就须要这么多代码...");
    }
}


public class Main {
    public static void main(String[] strings) {

        // 继承 Thread 类
        Thread thread = new ThreadDemo();
        thread.start();

        // 实现 Runnable 接口
        Thread again = new Thread(new RunnableDemo());
        again.start();
    }
}复制代码

经过调用 start 函数能够启动有一个新的线程,而且执行 run 方法中的逻辑。这里能够引出一个很容易被问道的面试题:编程

Thread 类中 start 函数和 run 函数有什么区别。多线程

最明显的区别在于,直接调用 run 方法并不会启动一个新的线程来执行,而是调用 run 方法的线程直接执行。只有调用 start 方法才会启动一个新的线程来执行。ide

引入线程的目的是为了使得多个线程能够在多个 CPU 上同时运行,提升多核 CPU 的利用率。函数

多线程编程很常见的状况下是但愿多个线程共享资源,经过多个线程同时消费资源来提升效率,可是新手一不当心很容易陷入一个编码误区。性能

class ThreadDemo extends Thread {
    private int i = 3;
    @Override
    public void run() {
        i--;
        System.out.println(i);
    }
}

public class Main {
    public static void main(String[] strings) {
        Thread thread = new ThreadDemo();
        thread.start();
        Thread thread1 = new ThreadDemo();
        thread1.start();
        Thread thread2 = new ThreadDemo();
        thread2.start();
    }
}复制代码

上面的实例代码,但愿经过 3 个线程同时执行 i--; 操做,使得最终 i 的值为 0,可是结果不如人意,3 次输出的结果都为 2。这是由于在 main 方法中建立的三个线程都独自持有一个 i ,咱们的目的一应该是 3 个线程共享一个 i。学习

public class Main {
    public static void main(String[] strings) {
        DemoRunnable demoRunnable = new DemoRunnable();
        new Thread(demoRunnable).start();
        new Thread(demoRunnable).start();
        new Thread(demoRunnable).start();
    }
}

class DemoRunnable implements Runnable {

    private int i= 3;

    @Override
    public void run() {
        i--;
        System.out.println(i);
    }
}复制代码

使用上面的代码才有可能使得 i 最终的结果为0。因此,在进行多线程编程的时候必定要留心多个线程是否共享资源。优化

Volatile

若是你运气好,执行上面的代码发现,有时候三次 i--; 的结果也不必定是 0。这种怪异的现象须要从 JVM 的内存模型提及。编码

当 Java 启动了多个线程分布在不一样的 CPU 上执行逻辑,JVM 为了提升性能,会把在内存中的数据拷贝一份到 CPU 的寄存器中,使得 CPU 读取数据更快。很明显,这种提升性能的作法会使得 Thread1 中对 i 的修改不能立刻反应到 Thread2 中。

下面例子能够明显的体现出这个问题。

public class Main {
    static int NEXT_IN_LINE = 0;

    public static void main(String[] args) throws Exception {
        new ThreadA().start();
        new ThreadB().start();
    }

    static class ThreadA extends Thread {
        @Override
        public void run() {
            while (true) {
                if (NEXT_IN_LINE >= 4) {
                    break;
                }
            }
            System.out.println("in CustomerInLine...." + NEXT_IN_LINE);
        }
    }

    static class ThreadB extends Thread {
        @Override
        public void run() {
            while (NEXT_IN_LINE < 10) {
                System.out.println("in Queue ..." + NEXT_IN_LINE++);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}复制代码

上面的代码中,ThreadA 线程进入死循环一直到 NEXT_IN_LINE 的值为 4 才退出,ThreadB 线程不停的对 NEXT_IN_LINE++ 操做。然而执行代码发现 ThreadA 没有输出 in CustomerInLine...." + NEXT_IN_LINE,而是一直处于死循环状态。这个例子能够很明显的验证:"JVM 会把线程共享的变量拷贝到寄存器中以提升效率" 的说法。

那么,怎么才能避免这种优化给编程带来的困扰?这里要引出一个内存可见性 的概念。

内存可见性指的是一个线程对共享变量值的修改,可以及时地被其余线程看到。

为了实现内存可见性,Java 引入了 volatile 的关键字。这个关键字的做用在于,当使用 volatile 修改了某个变量,那么 JVM 就不会对该变量进行优化,即意味着,不会把该变量拷贝到 CPU 寄存器中,每一个变量对该变量的修改,都会实时的反应在内存中。

针对上面的例子,把 static int NEXT_IN_LINE = 0; 改为 static volatile int NEXT_IN_LINE = 0; 那么执行的结果就如咱们所预料的,在 ThraedB 自增到 NEXT_IN_LINE = 4 的时候 ThreadA 会跳出死循环。

指令重排

volatile 还有一个很好玩的特性:防止指令重排。

首先要明白什么是指令重排?

假设在 ThreadA 中有

context = loadContext();
inited = true;复制代码

ThreadB 中

while(!inited) {
    sleep(100);
}
doSomething(context);复制代码

那么,ThreadB 中会在 inited 置位 true 以后执行 doSomething 方法,inited 变量的做用就是用来标志 context 是否被初始化了。可是实际上在执行 ThreadA 代码的时候 JVM 会根据上下行代码是否互相关联而决定是否对代码执行顺序进行重排。这就意味着 CPU 认为 ThreadA 中的两行代码没有顺序关联,因而先执行 inited=true 再执行 context=loadContext()。如此一来,就会致使 ThreadB 中引用了一个值为 null 的 context 对象。

使用 volatile 能够避免指令重排。在定义 inited 变量的时候使用 volatile修饰:volatile boolean inited = false;。 使用 volatile 修饰 inited 以后,JVM 就不会对 inited 相关的变量进行指令重排。

原子性

回到最初的例子。在 volatile 部分咱们说过最终的结果不是输出 i = 0 的缘由是 JVM 拷贝内存变量到 CPU 寄存器中致使线程之间没办法实时更新 i 变量的值致使的,只要使用 volatile 修饰 i 就能够实现内存可见性,可使得结果输出 i = 0。可是实际上,即便使用了 volatile 以后,仍是有可能的致使 i != 0 的结果。

输出 i != 0 的结果是因为 i++; 操做并不是为原子性操做。

什么是原子性操做?简单来讲就是一个操做不能再分解。i++ 操做实际上分为 3 步:

  • 读取 i 变量的值。
  • 增长 i 变量的值。
  • 把新的值写到内存中。

那么,假设 ThraedA 在执行第 2 步以后,ThreadB 读取了 i 变量的值,这时候还未被 ThreadA 更新,读取的还是旧的值,以后 ThreadA 写入了新的值。这种状况下就会致使 i 在某个时刻被修改屡次。

解决这种问题须要用到 synchronized。可是这里不打算对 synchronized 进行讨论。这里指出一个很容易被误解的概念:volatile 可以实现内存可见性和避免指令重排,可是不能实现原子性。

相关文章
相关标签/搜索