ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Java并发编程系列5】对象的共享

【Java并发编程系列5】对象的共享 可见性指当多个线程访问同一个变量时一个线程修改了这个变量的值其他线程能够立即看得到修改的值。在多线程环境下一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性当一个变量被volatile修饰后表示着线程本地内存无效当一个线程修改共享变量后他会立即被更新到主内存中其他线程读取共享变量时会直接从主内存中读取。当然synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。评价该小节内容主要是讲解的volatile的可见性提到volatile不具备原子性特性也引出了synchronize但是对于可见性final其实可见性是最强的。这部分内容中规中矩建议大家将volatile、synchronize和final的可见性、原子性对比起来看最好能理解他们可见性的原理以及重排序的机制这样就能对“可见性”这一概念理解的更加深入。发布和溢出发布发布一个对象是对象能够在当前作用域之外的代码中使用。比如将对象的引用保存到其它代码可以访问的地方或者在一个非私有方法中返回对象的引用简单来说就是外部可以访问到这个对象和里面的成员或者方法为了保证多线程下没有问题需要保证对象内部状态的封装性不被破坏。书中写的有点像八股文讲的也有点抽象我理解其实就拿到一个对象时需要保证对象内部数据已经完全初始化然后对象内部的成员和方法要么就完全封装不能将修改的方式对外暴露如果不能做到这一点就需要保证修改和访问是线程安全的。溢出当一个不应该发布的对象被发布时这种情况被称为溢出Escaspe。溢出的情况我总结有2种将成员变量的引用返回外部就可以修改数据多线程下可能会存在问题在构造函数过程中使this引用溢出。成员变量溢出class UnsafeStates { private String[] states new String[] {AK,AL ... public String[] getStates {return states;} }这个是书中的示例当你发布这个对象时任何调用者都可以修改数组中的内容有人可能会说我在states前面加一个final这个其实解决不了问题因为final可以保证states的地址不被改变但是不能保证内部的数据不被改变。this引用溢出书中的注册示例没有看懂还是自己太弱了。。。我给一个前面一篇文章的示例里面可以从原理讲解this引用是如何引出这个应该比书中给的示例更能深入理解。public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i 1; //1写final域 obj this; //2 this引用在此“逸出” } public static void writer() { new FinalReferenceEscapeExample (); } public static void reader { if (obj ! null) { //3 int temp obj.i; //4 } } }假设一个线程A执行writer()方法另一个线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步且即使在程序中操作2排在操作1后面执行read()方法的线程仍然可能无法看到final域被初始化后的值因为这里的操作1和操作2之间可能被重排序。实际的执行时序可能如下图所示从上图我们可以看出在构造函数返回前被构造对象的引用不能为其他线程可见因为此时的final域可能还没有被初始化。在构造函数返回后任意线程都将保证能看到final域正确初始化之后的值。线程封闭文章讲解了“栈封闭”和“ThreadLocal类”这两部分内容栈封闭“栈封闭”很好理解类似于函数中的变量函数结束后变量内容自动释放不会暴露对外“栈封闭”其实讲的和这个原理类似也就“栈封闭”的内容肯定不会“溢出”。“栈封闭”可以很好的保证线程安全性。ThreadLocal类这个其实和C多线程的线程特定数据是一个道理也就是每个线程可以有自己的ThreadLocal类里面只保存本线程的数据对其它线程不可见一般该线程的全局变量都可以保存到里面其它线程不会干扰。不变性主要讲解final知识强调了“不可变对象一定是线程安全的”这个可以直接参考《Java并发编程系列4-final》这篇文章里面的示例“使用volatile类型来发布不可变的对象”总结的不错里面有一句话印象很深刻通过使用包含多个状态的容器对象来维持不变形条件并使用一个volatile类型的引用来保证可见性使得对象的在没有显示地使用锁的情况下仍然是安全的。这个怎么理解呢书中的示例其实就是整了一个类OneValueCache和一个工厂Factory工厂中有一个OneValueCache的成员变量可以把Factory看成是获取OneValueCache对象的单例模式如果需要这个单例模式对所有线程可见就需要将该成员变量定义成volatile类型保证所有线程可见然后OneValueCache对象内部成员都是final所以可以保证线程安全。上述方法总结一句话就是“通过final保证对象线程安全通过volatile保证内存可见实现多线性安全性”安全发布不安全发布不正确的安全发布会导致多线程运行时出现异常public class test { private int n; public void(int n) { this.n n; } public void forTest() { if (n ! n) { // 这里可能会进入多线程下因为重排序影响this.nn可能会排在构造函数完成外面由于n!n不能保证原子性会出现问题。如果还是不懂建议先看一下重排序规则里面很多类似的示例 } } }安全发布常用模式书中总结了以下方法在静态初始化函数中初始化一个对象的引用将对象的引用保存在volatile类型的域或者AtomicReferance对象中将对象的引用保存在某个正确构造对象的final类型域中将对象的引用保存在一个由锁保护的域中。上面的4个方法后面3个很好理解对于第1个因为静态初始器由JVM在类的初始化阶段执行由于JVM内部存在这同步机制因此通过这种方式初始化的任何对象都可以安全地发布public static Holder holder new Holder(18);其它概念文章提到了“事实不可变对象”这个概念有点绕文中解释为”如果对象从技术上看是可变的但是状态在发布后不会再改变“。然后给了个例子public MapString, Data test Collections.synchronizedMap(new HashMapString, Data())虽然Data对象可变但是test不可变也就是通过不可变容器或者其它方式来装载可变对象让其处理成不可变的方式。有点绕还是个八股文就稍微了解即可。对于可变对象处理时就需要加锁。总结学习Java并发编程前面的基础知识学了快2周主要包括JMM、重排序规则、原子性、可见性和安全发布很多知识都是围绕volatile、synchronize、final三者展开之所以学了这么久主要还是想把基础打牢后面的应用总结和讲解可能就没有这么细致。
返回列表