多线程特性:原子性、可见性、有序性
-
原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就不执行(类似事务管理);
只有操作具备原子性才能保证不出现一些意外问题。 -
可见性
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即得到修改的值```c 例子: //线程1执行的代码 Int i=0; I = 10; //线程2执行代码 Int j=i; 如果线程1执行i =0;这句时,此时线程2执行j=i,它读取i的值加载到内存中,注意此时内存当中的i=0,那么就会使得j的值也为0,而不是10; ```
这就是可见性的问题,线程1对i的值修改之后,线程2没有立即看到线程1修改的值。
-
有序性
有序性:程序执行的顺序按照代码的先后顺序执行。
代码的先后顺序并不一定是JVM的执行顺序,可能会发生指令重排序。
什么是重排序?
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致。
As-if-serial:无论如何重排序,程序最终执行结果和代码顺序执行的结果是一致的。Java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义。
注意:重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性
要想并发程序正确执行,必须要保证原子性,可见性以及有序性。只要有一个没有被保证,就可能会导致程序运行不正确。
JMM线程操作内存的基本的规则:
第一条关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存(本地内存)中进行,不能直接从主存中读写
第二条关于线程间本地内存:不同线程之间无法直接访问其它线程本地内存中的变量,线程间变量值的传递需要经过主存来完成。
-
主内存:
主要存储的是java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量,当然也包括共享类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。 -
本地内存:
主要存储当前方法的所有本地变量信息,每个线程只能访问自己的本地内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括字节码行号指示器,相关Native方法的信息,注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在多线程安全问题。
小结:
JVM内存结构,和java虚拟机的运行时区域有关。Java对象模型,和java对象在虚拟机中的表现形式有关。Java内存模型,和java的并发编程有关。 -
内存的可见性:
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。 -
Synchronized
synchronized可以保证方法或者代码块运行时,同一时刻只有一个线程执行synchronized声明的代码块。还可以保证共享变量的内存可见性。同一时刻只有一个线程执行,这部分代码块的重排序也不会影响其它执行结果。
也就是说使用了synchronized可以保证并发的原子性,可见性,有序性,
解决可见性问题 -
使用volitale修饰变量也可以解决
JMM关于synchronized的两条规定:- 线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到 主内存中。
- 线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是用一把锁)