1. 共享对象
可见性
可见性涉及的方面:
(1) 过期数据
非线程安全的计数器:
/**
* @Title: NotThreadSafetyCounter.java
* @Package visibility
* @Description: TODO
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午6:47:35
* @version V1.0
*/
package visibility;
/**
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午6:47:35
*/
public class NotThreadSafetyCounter {
private int counter;
/**
* @return the counter
*/
public int getCounter() {
return counter;
}
/**
* @param counter the counter to set
*/
public void setCounter(int counter) {
this.counter = counter;
}
}
非线程安全的计数器:
/**
* @Title: ThreadSafetyCounter.java
* @Package visibility
* @Description: TODO
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午6:47:10
* @version V1.0
*/
package visibility;
/**
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午6:47:10
*/
public class ThreadSafetyCounter {
private int counter;
/**
* @return the counter
*/
public synchronized int getCounter() {
return counter;
}
/**
* @param counter the counter to set
*/
public synchronized void setCounter(int counter) {
this.counter = counter;
}
}
(2) 非原子的64位操作
Java内存模型要求获取和存储操作都为原子的,但是非volatile的long和double变量,JVM允许将64位的读写操作划分为两个32位的操作。如果读写发生在不同的线程,这种情况读取一个非volatile类型的long或者double就可能出现读取了一个long的高32位和另一个long的低32位。因此在多线程中使用共享的可变的long或者double变量也可能是不安全的。除非将其生命为volatile或者用锁保护起来。
(3) Volatile
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
1. 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
2. 可见性
Java提供的弱同步:volatile变量。它确保对一个变量的写入以可以预见的方式告知其他线程。当一个变量被声明为volatile后,编译器会监视这个变量:它是共享的,并且对它的操作不会与其他内存操作一起被重排序。所以读一个volatile变量总是会返回某一线程所写入的最新值。
线程A向volatile变量写入值,随后线程B读取该变量,所有A线程执行对volatile变量执行写操作之前的可见变量,在B线程读取volatile变量后,也会对B线程可见。所以从内存可见性的角度看,写入volatile变量就像是退出了同步块,读取volatile变量就像是进入了同步块。
Volatile语义不足以使自增操作(counter++)成为原子性。因为自增操作是读写改的复合操作。原子变量提供读写改的支持(AtomicLong、AtomicInteger等)
加锁可以保证原子性和可见性。Volatile只能保证可见性,不能保证原子性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
3. 有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
·程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
·锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
·volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
·传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
·线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
·线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
·线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
·对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条原则摘自《深入理解Java虚拟机》。
(4) 深入剖析volatile
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
先看一段代码,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
volatile能保证有序性吗?
在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
可能上面说的比较绕,举个简单的例子:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
volatile的原理和实现机制
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
发布和逸出
发布一个对象的意思是使它能够被当前范围之外的代码所使用。比如将一个引用存储到其他代码可以访问的地方,在一个非私有的方法中返回这个对象的引用,也可以把它传递到其他类的方法中。
一个对象尚未准备好时就将其发布出去,这种情况就叫做逸出(escape)
允许内部可变数据逸出的示例:
/**
* @Title: InnerVariableEscape.java
* @Package publish
* @Description: 内部可变数据逸出
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午10:59:35
* @version V1.0
*/
package publish;
import java.util.HashSet;
import java.util.Set;
/**
* @author 落叶飞翔的蜗牛
* @date 2017年12月3日 下午10:59:35
*/
public class InnerVariableEscape {
public static HashSet<String> objectSets = new HashSet<String>() {
/**
* @Fields serialVersionUID : TODO
*/
private static final long serialVersionUID = 5050434981450895395L;
{
add("销售额");
add("销售");
add("销售金额");
add("销售值");
add("销售总金额");
add("销售总额");
add("销售总值");
}
};
/**
* @Title: getObjects
* @Description: 这种方式发布会出问题。任何一个调用者都可以修改objectSets
* @param: @return
* @return: Set<String>
* @throws
*/
public Set<String> getObjects() {
return objectSets;
}
}
线程封闭
将对象封闭在一个线程中,这种做法会使得对象自动成为线程安全的,即使被封闭的对象本身并不是线程安全的。
常见的使用线程限制的应用程序是应用池化的JDBC Connection对象。JDBC规范并没有要求Connection对象是线程安全的。在实际应用中,线程总是从池中获取一个Connection对象,并且用它处理一个单一的请求,用完之后把它归还。
每个线程接入连接池以后,从连接池中获取一个连接,放入该线程的ThreadLocal中,在该线程的后续DAO操作中,都是用封装在ThreadLocal中的那个连接,就可以做到对多个DAO操作的事物管理。
栈限制是线程限制的一种特例。本地变量就被限制在执行线程中,它们存在于执行线程栈,而这个栈是不会再线程间共享的,每个线程都有自己的执行栈。
一种维护线程安全的更加规范的方式是使用ThreadLocal,它允许你把每个线程与数值的对象关联在一起。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
(5) 参考文献
① http://www.importnew.com/18126.html