如果当多个线程访问某个类时,这个类始终都能表现正确的行为,则可以称这个类是线程安全的
一般在线程安全类中封装了必要的同步机制即加锁机制,而加锁的目的是保证原子性操作,即让操作不可分割,从而让各个线程依次串行访问。
一个对象是否需要考虑线程安全,取决于它是否被多个线程访问,并且这个对象存在多种状态
如果一个对象是无状态,既它不包含任何域,也不包含任何对其他类中域的引用,那么它一定是线程安全的
原子性:
/*** @NotThreadSafe* Demo并非线程安全的,尽管它在单线程环境中能正确运行。* 因为++count是非原子操作,它不会作为一个不可分割的操作来执行,实际上,它包括读取-修改-写入,并且其状态依赖于之前的状态。* 由于运行时可能将多个线程之间的操作交替执行,因此两个线程可能同时执行读取操作,从而得到相同的值,并将值加1。而得不到期望的结果(两个线程顺序执行,依次加1)*/ public class Demo{private long count = 0;public void increase(){++count;} }
- 原子变量
/*** @ThreadSafe* 用AtomicLong来代替long类型的计数器,能够确保所有对计数器状态的访问操作都是原子的。* 事实上Demo的状态就是count的状态,而计数器count是线程安全的,因此Demo也是线程安全的。*/ public class Demo{private final AtomicLong count = new AtomicLong(0);public void increase(){count.incrementAndGet();} }
- 加锁机制
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();/*** @NotThreadSafe* 考虑存在多种状态且有相互约束关系的情况:示例Demo对请求参数进行因式分解并缓存上次结果。* 在lastFactors中缓存的因数之积应该等于lastNumber中缓存的值,只有确保了这个不变性条件(各个域之间的约束关系)不被破坏,Demo的状态才是正确的。* 当不变性条件涉及多个变量时,各个变量并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。* 因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。* 在使用原子引用的情况下,虽然每次对set的调用都是原子的,但仍然无法同时更新lastNumber和lastFactors,那么在两次修改操作之间,其他线程将发现不变性条件被破坏了。*/public BigInteger[] service(BigInteger param){if(param.equals(lastNumber.get())){return lastFactors.get();}else{BigInteger[] result = factor(param);lastNumber.set(param);lastFactors.set(result); return lastFactors.get(); }}
- 内置锁
java对象都可以用做一个实现同步的锁,这些锁是内置锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
内置锁是一种互斥锁,意味着最多只有一个线程能持有这种锁。当线程A试图获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B不释放,A将永远等下去。但是这种方式过于极端,因为多个线程无法同时访问,响应性极低。
private BigInteger lastNumber;private BigInteger[] lastFactors;/*** @ThreadSafe*/public synchronized BigInteger[] service(BigInteger param){if(param.equals(lastNumber)){return lastFactors;}else{BigInteger[] result = factor(param);lastNumber = param;lastFactors = result; return lastFactors; }}
- 内置锁—重入
当某个线程请求一个由其他线程持有的锁时,会发生阻塞。而由于内置锁时可重入的,因此如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求会成功。
重入意味着获取锁的操作粒度是线程,而不是调用。
重入进一步提升了加锁行为的封装性,简化了面向对象并发代码的开发。
/*** 子类LogWidget改写父类Widget的synchronized方法,然后调用父类的方法,此时如果没有可重入的锁,将发生死锁。* 因为Widget与LogWidget的dosomething方法都是synchronized方法,因此每个dosomething执行前都会获取Widget的内置锁。* 而如果内置锁不是可重入的,那么super.dosomething();将发生死锁。*/ public class Widget {public synchronized void dosomething(){} }class LogWidget extends Widget{public synchronized void dosomething(){super.dosomething();} }
- 加锁机制—优化
private long count = 0;private BigInteger lastNumber;private BigInteger[] lastFactors;/*** 缩小同步代码块的范围,既确保并发性,同时维护线程安全性。* 尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。* 这里计数器count不再使用AtomicLong。因为对于在单个变量上实现原子操作来说,原子变量是很有用的,* 但是由于这里已经使用了同步代码块来构造原子操作,而使用两种不同的同步机制不仅会带来混乱,也不会在性能或安全上带来任何好处。*/public BigInteger[] service(BigInteger param){BigInteger[] factors = null;synchronized(this){++count;if(param.equals(lastNumber)){factors = lastFactors;}}if(factors == null){factors = factor(param);synchronized(this){lastNumber = param;lastFactors = factors;}}return factors;}
在实际中要判断同步代码块的合理大小,需在各种设计需求之间进行权衡,包括安全性、简单性和性能。达到在简单性与并发性之间的平衡。
#笔记内容参考 《java并发编程实战》