编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。对象的状态是指存储在状态变量(实例或静态域)中的数据。对象的状态还可能包括其他依赖对象的域。(Map.Entry)
Java常用的同步机制是Synchronized,还包括 volatile类型的变量,显示锁以及原子变量。
线程安全的程序是否完全由线程安全的类构成?答案是否定的,完全由线程安全的类构成的程序并不一定是线程安全的,线程安全类中也可以包含非线程安全的类。只有当类中仅包含自己的状态时,线程安全类才有意义!
如果多个线程访问一个可变的状态变量时没有使用适合的同步,那么程序就会出现错误,有三种方式可以修复这个错误:
不在线程之间共享该状态变量
将状态变量修改为不可变的变量
在访问状态变量时使用同步
当设计线程安全类的时候,良好的面向对象技术,不可修改性,以及明确的不变性规范都能够起到一定的帮助作用。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。
无状态对象一定是线程安全的
竟态条件:在并发编程中,由于不恰当的执行顺序而出现不正确的结果
首先观察到某个条件为真,然后开始执行相关的程序,但是在多线程的运行环境中,条件判断的结果以及开始执行程序中间,观察结果可能变得无效(另外一个线程在此期间执行了相关的动作),从而导致无效。常见的就是(Lazy Singleton)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /** 2 * describe: 懒汉式单例模式 3 * 优点:只有在需要使用LazySingleton1对象时,才真正生成一个LazySingleton1对象 4 * 缺点:会因为某些Java 平台内存模型允许无序写入,使得getInstance方法可能返回 5 * 一个尚未执行构造函数的对象 6 * Created by tianc on 2017/4/15. 7 */ 8 public class LazySingleton { 9 private static LazySingleton lazyInstance = null; 10 private LazySingleton() { 11 } 12 public static LazySingleton getInstance(){ 13 if(lazyInstance == null){ 14 synchronized (LazySingleton.class){ 15 if(lazyInstance == null){ 16 lazyInstance = new LazySingleton(); 17 } 18 } 19 } 20 return lazyInstance; 21 } 22 }
在java.util.concurrent.atomic包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换,通过用AtomicLong来替代long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。
在实际情况中,应尽可能的使用现有的线程安全对象来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更加容易,从而也更加容易维护和验证线程安全性。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁:
同步代码块(Synchronized Block)包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。关键字Synchronized修饰方法就是一种同步代码块,锁就是方法调用所在的对象,静态的Synchronized方法以Class对象作为锁。内置锁或监视锁就是以对象作为实现同步的锁。
Java内置锁,进入的唯一途径是执行进入由锁保护的同步代码块或方法。它相当于一种互斥锁。
重入锁:当一个持有锁的线程再次请求进入自己持有的锁时,该请求会成功。"重入"意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方式,为每个锁关联一个计数器和线程持有者。
用锁来保护状态
由于锁能使其保护的代码路径以串行形式访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都要使用同一个锁来保护/同步。
活跃性和性能:
通过缩小同步代码块的作用范围,我们很容易做到即确保Servlet的并发性,同时又维护线程安全性,要确保同步代码块不要过小,并且不要将原本应是原子操作拆分到各个同步块中,应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。
通常,在简单性和性能之间存在着相互制约的因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性。
当执行时间较长的计算或者可能无法快速完成操作时,一定不要持有锁。