Java 线程和 volatile 解释

最近开始学习 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 方法才会启动一个新的线程来执行。

引入线程的目的是为了使得多个线程可以在多个 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 变量的时候使用 olatile修饰: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 能够实现内存可见性和避免指令重排,但是不能实现原子性。

本文首发与 https://jaychen.cc
作者 JayChen

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容