摘要:支持多线程,中创建线程的方式有两种继承类,重写方法。多线程编程很常见的情况下是希望多个线程共享资源,通过多个线程同时消费资源来提高效率,但是新手一不小心很容易陷入一个编码误区。所以,在进行多线程编程的时候一定要留心多个线程是否共享资源。
文章首发于 http://jaychen.cc
作者 JayChen
最近开始学习 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://www.ucloud.cn/yun/70308.html
摘要:变量可见性问题的关键字保证了多个线程对变量值变化的可见性。只要一个线程需要首先读取一个变量的值,基于这个值生成一个新值,则一个关键字不足以保证正确的可见性。 Java的volatile关键字用于标记一个Java变量为在主存中存储。更确切的说,对volatile变量的读取会从计算机的主存中读取,而不是从CPU缓存中读取,对volatile变量的写入会写入到主存中,而不只是写入到CPU缓存...
摘要:并发编程关键字解析解析概览内存模型的相关概念并发编程中的三个概念内存模型深入剖析关键字使用关键字的场景内存模型的相关概念缓存一致性问题。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。 Java并发编程:volatile关键字解析 1、解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析volatile关键字 ...
摘要:最近在看多线程相关,看到这篇来自大神关于关键字的讲解感觉非常详细易懂,特此转载一下。如果对增加声明则所有线程对的写都会立即刷新到主存中,而且所有对的读也都直接从主存中去读。 最近在看java多线程相关,看到这篇来自大神Jakob Jenkov关于Volatile关键字的讲解感觉非常详细易懂,特此转载一下。原文链接:http://tutorials.jenkov.com/j... 内存可...
摘要:每个会缓存主存的共享变量,从而提高处理效率。为当前缓存行加入缓存一致性协议。任何修改,其他线程是可见的。修饰的变量还是会缓存的,只是通过一系列处理保证了所有线程看到这个变量的值是一致的 java并发编程实战对volatile的解释就是:当一个域声明为valatile类型后,编译器与运行时会监视这个变量:它是共享的,而且对它的操作不会与其他的内存操作一起被重排序。volatile变量不会...
摘要:今天开始整理学习多线程的知识,谈谈最重要的两个关键字和。但是这样一个过程比较慢,在使用多线程的时候就会出现问题。有序性有序性是指多线程执行结果的正确性。这种机制在多线程中会出现问题,因此可以通过来禁止重排。 今天开始整理学习多线程的知识,谈谈最重要的两个关键字:volatile和synchronized。 一、三个特性 1、原子性 所谓原子性操作就是指这些操作是不可中断的,要么执行过程...
阅读 3836·2021-09-27 13:35
阅读 1008·2021-09-24 09:48
阅读 2877·2021-09-22 15:42
阅读 2320·2021-09-22 15:28
阅读 3120·2019-08-30 15:43
阅读 2581·2019-08-30 13:52
阅读 2949·2019-08-29 12:48
阅读 1417·2019-08-26 13:55