”你永远都不知道一个线程何时在运行!“
在上一篇博客JAVA并发编程1_多线程的实现方式中后面看到多线程中程序运行结果往往不确定,和我们预期结果不一致。这就是线程的不安全。线程的安全性是非常复杂的,没有任何同步的情况下,多线程的执行顺序是不可预测的。当多个线程访问同一个资源时就会出现线程安全问题。例如有一个银行账户,一个线程往里面打钱,一个线程取钱,要是得到不确定的结果那是多么可怕的事情。
引入:
例如下面的程序,在单线程下,会依次顺序打印0-9,但是在多线程环境下则不能得确定的结果。
public class Test implements Runnable { private int i = 0; private int getNext() { return i++; } @Override public void run() { while (i< 10) { System.out.println(getNext()); } } public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); Thread.yield(); } }
运行了2次,得到的结果都不正确,运行结果根本不确定。
仅仅是执行一个i++的简单操作,在多线程环境下都会出现莫名其妙的结果。
下面将从线程的机制/JAVA线程内存模型的角度分析线程安全。
线程的机制:
通过使用多线程机制,这些独立任务(子任务)中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此单个进程可以拥有多个并发的任务。程序中每个任务好像都有自己的CPU,底层机制是切分CPU时间。
在使用线程时,CPU轮流给每个任务分配其占用的时间。每个任务都觉得自己一直在占用CPU,但事实上CPU时间是划分成片段分配给了所有任务。(多个CPU或者多核例外)。
JAVA线程内存模型:
JAVA虚拟机定义的一种JAVA内存模型屏蔽掉了各个硬件和操作系统的内存访问差异,以实现在不同平台上达成一样的内存访问效果。(这也是JAVA跨平台的原因)
线程的内存模型规定了所有的变量都存储在主内存中(就是我们所说的堆内存),每个线程有自己的工作内存。工作内存保存了被该线程使用到的变量的住内存的副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程无法直接访问对方工作内存中的变量。线程间变量值的传递需要通过主内存来完成。
变量如何从住内存拷贝到工作内存/如何从工作内存同步到主内存?JAVA内存模型定义了8中操作:lock/unlock/read/load/use/assign/save/write。
l A use action (bya thread) transfers the contents of the thread's working copy of a variable tothe thread's execution engine.
l An assign action(by a thread) transfers a value from the thread's execution engine into thethread's working copy of a variable.
l A read action (bythe main memory) transmits the contents of the master copy of a variable to athread's working memory for use by a later load operation.
l A load action (bya thread) puts a value transmitted from main memory by a read action into thethread's working copy of a variable.
l A save action(by a thread) transmits the contents of the thread's working copy of a variableto main memory for use by a later write operation.
l A write action(by the main memory) puts a value transmitted from the thread's working memoryby a store action into the master copy of a variable in main memory.
java线程和内存交互
这8中操作都是原子的/不可再分的(double/long在某些平台有例外),并且JAVA虚拟机还规定了一系列操作规则。
(1) read和load、store和write必须要成对出现,不允许单一的操作,否则会造成从主内存读取的值,工作内存不接受或者工作内存发起的写入操作而主内存无法接受的现象。
(2) 在线程中使用了assign操作改变了变量副本,那么就必须把这个副本通过store-write同步回主内存中。如果线程中没有发生assign操作,那么也不允许使用store-write同步到主内存。
(3) 在对一个变量实行use和store操作之前,必须实行过load和assign操作。
(4) 变量在同一时刻只允许一个线程对其进行lock,有多少次lock操作,就必须有多少次unlock操作。
(5) 在lock操作之后会清空此变量在工作内存中原先的副本,需要再次从主内存read-load新的值。
(6) 在执行unlock操作前,需要把改变的副本同步回主存。
Java内存的三个特性:
1.原子性:保证他们会被当作不可分的操作来操作内存而不发生上下文切换(切换到其他线程)。原子操作可由线程机制保证其不可中断。要保证更大范围的原子性,可以在代码里使用synchronized关键字(也叫同步锁,对应字节码指令monitorenter/monitorexit)。一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。
2.可见性:指一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。synchronized关键字保证了可视性。上面的规则(6)保证了这一点。另外volatile关键字也有相同作用。
可见性要保证某个线程以一种可预测的方式来查看另一个线程的执行结果。(即当线程B在执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果(Happens-Before关系))。加锁保证可见性如下图
3.有序性:JMM(Java内存模型,Java Memory Model)不保证线程对变量操作发生的顺序和被其他线程看到的是同样的顺序。 JMM容许线程以写入变量时所不相同的次序把变量存入主存。编译器和处理器可能会为了性能优化,进行重新排序
Volatile关键字的另一个作用就是可以防止编译器的优化,防止重排序。后面讲到volatile,在详细谈重排序现象。
JAVA线程机制和JMM的特点决定了多线程运行过程中的不确定性,因此要想保证线程的安全就必须进行线程同步。下一篇博客详解synchronized关键字。