关键知识
- 编写正确的并发程序的关键在于对共享的、可变的状态进行访问管理。
- 可见性
定义:可见性是指内存可见性,当一个线程修改了对象的状态后,其他的线程能够真正看到改变。
- 重排序来看一段代码
public class NoVisibility{
private static boolean ready;
private static int number;
private static class MyThread extends Thread{
@Override
public void run(){
while(!ready)
Thread.yield();
System.out.println(number);
}
public static void main(String[] args){
MyThread t = new MyThread();
t.start();
number = 42;
ready = true;
}
}
这段代码并不会一定如我们所想输出42,它很有可能会输入0。为什么呢。因为很可能早在对number赋值之前,主线程早就写入ready并对其他线程可见。这是一种“重排序”现象。这具体跟操作系统指令集有关。在Java中只要不会破坏程序语义,为了提高性能,cpu就可能会对java代码进行重排序。对于Java而言存储模型允许编译器重排序操作。还允许cpu重新排序。
- 非原子的64位操作
没有申明为volatile的64位数值变量(double和long)。对于非volatile的long和double类型的变量,JVM允许将64位的读或者写划分为两个32位的操作。如果读和写
发生在不同的线程,这时候读取一个非volatile的变量可能会出现一个值的高32位和另一个值的低32位。
- 锁和可见性
内置锁可以确保一个线程可以某种可预见的方式看见另一个线程的影响。
锁不仅仅是关于同步互斥的,也是关于内存可见的。
- 使用volatile变量
除了内置锁之外,Java还提供了一种同步的弱形式:volatile变量。他确保对一个变量的更新能以可预见的方式告诉其他线程。当一个域声明为volatile时,编译器与运行时会监视这个变量:他是共享的,并且对它的操作不会与其他的内存操作一起被重排序。volatile变量不会存储在缓存或者其他对处理器隐藏的地方。所以读一个volatile类型的变量总会返回由某一个线程锁写入的新值。
volatile对于synchronized而言,只是轻量级的同步机制。
- 何时使用volatile变量:volatile变量固然方便,但也存在限制。他们通常是被当作标识完成、中断、状态的标记使用。只是决定这样做之前请格外小心——volatile变量的语义不足以使自增操作(++)原子化,除非你能保证只有一个线程对变量执行写操作。
枷锁可以保证可见性和原子性;volatile变量只能保证可见性。
- 发布和溢出
定义:发布一个对象的意思是使它能够被当前范围之外的代码说使用。很多时候,我们需要确保对象的及他们的内部状态不被暴露出来。
溢出:发布一个对象时,如果他还没有完成构造,会威胁线程安全。一个对象在尚未准备好就将它发布,这种情况称作溢出。
- 最常见的发布对象的方式就是将对象的引用存储到公共静态域中。任何类和线程都能看见这个域。
- 发布一个对象还会间接地发布其他对象。类似的从非私有方法中返回引用,也能发布返回的对象。发布一个对象,同样也发布了该对象所有非私有域所引用的对象。更一般的,在一个已经发布的对象中,那些非私有域的引用链,和方法调用链中的可获得对象也都会被发布。
- 一旦一个对象溢出,你就要假设存在其他的类或者线程可能误用它。
封装使得程序的正确性分析变得更可行。而且更不易偶然的破坏设计约束。
- this引用的溢出关于这点可以参考https://blog.csdn.net/qq_34228570/article/details/78639425
- 使用线程封闭技术可以实现线程安全一个常见的应用就是JDBC。在典型的服务器应用中,线程总是从池中获取一个Connection对象,并且用它处理一个单一的请求,然后把它归还。在Connection对象归还之前,池不会再将它分配给其他线程。
- 语言自身以及核心类库提供了某些机制(本地变量和ThreadLocal类)有助于维护线程限制。
- 栈限制将对象的引用声明为本地变量的形式。因为本地变量本身就限制在执行线程中,他们存在于执行线程栈,其他线程无法访问这个栈。这个很好理解,在方法中声明的变量被称为局部变量,局部变量限制于方法栈中,栈又是线程私有的。所以可以实现线程安全。
- ThreadLocal 一种维护线程限制的更加规范的方式是使用ThreadLocal,它允许你将每个线程与持有数值的对象关联在一起。Threadlocal提供了set与get访问器,为每个访问它的线程提供一份单独的拷贝。所以get总会返回由当前线程通过set设置的最新值。
- 线程本地(Threadlocal)变量通常用于防止在基于可变的单体或全局变量的设计中,出现(不正确的)共享。
- 为了满足同步的需要,另一种维护线程安全的方法就是使用不可变对象。
定义:创建后状态不能被修改的对象叫做不可变对象。不可变对象天生就是线程安全的。
不可变性并不简单的等于将对象中所有的域都声明为final类型。所有域都为final类型的对象仍然可以是改变的,因为final域可以获得一个到可变对象的引用。
- 只有满足如下状态,一个对象才是不可变的。
它的状态在创建后不能被修改
所有域都是final类型,并且他被正确创建(创建期间没有发生this引用的溢出)。
- 程序存储在不可变对象中的状态仍然可以通过替换一个带有新状态的不可变对象的实例得到更新。
- final域
final域使得确保初始化安全性成为可能。初始化安全性让不可变对象不需要同步就能自由的被访问和共享。
正如“将所有的域声明为私有的,除非他们需要更高的可见性“一样,“将所有的域声明为final类型,除非他们是可变的“,也是一条良好的实践。
- 我们来看书中展示的一个实例程序
class OneValueCache{
//属性私有且用final修饰
private final BigInteger lastNumber;
private final BigInteger [] lastFactors;
public OneValueCache(BigInteger i,BigInteger [] factors){
this.lastNumber = i;
this.lastFactors = Arrays.copyOf(factors,factors.length);
}
public BigInteger[] getFactors(BigInteger i){
if(lastNumber == null || !lastNumber.equals(i){
return null;
}else{
return Arrays.copyOf(lastFactors,lastFactors.length);
}
自定义的Servlet实现
public class VolatileCacheFactorizer implements Servlet{
private volatile OneValueCache cache = new OneValueCache(null,null);
public void service(ServletRequest req,ServletResponse rep){
//业务逻辑
BigInteger i = extractFromRequest(req);//从请求中解析待分解因式的数字。
BigInteger[] factors = cache.getFactors(i);
if(factors == null){
factors = factor(i);
cache = new OneValueCache(i,factors);
}
encodeIntoResponse(rep,factors);
}
}
VolatileCacheFactorizer实现了Servlet接口重写了service方法,接受http请求并从请求中解析出待分解因数的数,在service方法中完成因数分解后响应客户端因式分解结果。现在为了提高服务器效率。使用缓存对重复的因数分解结果进行缓存。在OneValueCache中,因数对应分解结果封装在容器中。书中说这个容器是不可变对象。现在解释一下为什么这个对象是不可变的:OneValueCache对象有两个域BigInteger类型的i和BigInteger数组类型的factors。都被private修饰确保只能在本类中被访问。final修饰域对象引用一旦指定不能修改。i值只能在初始化容器时指定,此外在无方法可以修改。factors引用也只在初始化容器时指定,这里使用了Arrays.copyOf方法而不是直接将传入的参数直接赋值给它。前者会返回一个新的引用,这样就保证了该域的引用只有容器保有。getFactors方法返回factors,这里同样使用了Arrays.copyOf方法,同样会创建一个新的数组引用并返回,保证了容器保管的factors不会被发布。从而保证了OneValueCache的不可变性。
书中前面有说过,在使用不可变对象过程中,碰到需要修改状态的需求时,往往返回一个新的其他的不可变对象。正如VolatileCacheFactorizer所做的。当一个新的因数需要因式分解时,会创建一个新的cache。而cache使用volatile修饰,其确保了当cache指向的引用发生改变的时候,其他线程会看到改变。
23. Valotile关键字使用需遵循以下原则:
对变量的写操作不依赖于当前值。
该变量没有包含在具有其他变量的不变式中。