-
对象的状态
(1) 对象的状态是指 类的实例或静态变量
(2) 也包括其他依赖对象的域
例如某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中
-
Java的同步机制
(1) synchronized
(2) volatile
(3) 显示锁
(4) 原子变量: package java.util.concurrent.atomic
-
将线程不安全的类修改为线程安全的类的方法
(1) 不在线程之间共享状态变量
(2) 将状态变量修改为不可变变量
(3) 在访问状态变量时使用同步
-
程序状态的封装性越好, 越容易实现程序的线程安全性
-
线程安全性
(1) 定义
当多个线程访问某个类时,不论运行时环境采用何种调度方式, 或者线程如何调度执行, 在主调代码中不需要任何额外的同步, 这个类总能表现出正确的行为
(2) 无状态的类一定是线程安全的
即,类中没有任何静态变量和实例变量,类只提供方法,所有变量都是局部变量。
没有状态变量,自然也就没有线程同步的问题
-
竞态条件
(1) 定义
由于执行时序的不同,造成不正确的结果称为竞态条件
(2) 常见的竞态条件情形
1° 先检查后执行
示例
if (!flag) { .... }
2° 读取-修改-写入
示例
count += 1
(因为count+=1这一条语句事实上包含了读取-修改-写入三个步骤,不是原子操作)
(3) 当在无状态的类中添加一个状态变量时,如果该状态完全由线程安全的对象来管理,那么这个类一定是线程安全的
实际情况中,应尽可能使用线程安全的类作为状态变量,例如java.util.concurrent.atomic中的类
示例
线程不安全
@NotThreadSafe public class UnsafeCountingFactorizer extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest req, ServletResponse resp) { ... ++count; ... } }
线程安全
@ThreadSafe public class CountingFactorizer extends GenericServlet implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(ServletRequest req, ServletResponse resp) { ... count.incrementAndGet(); encodeIntoResponse(resp, factors); } }
其中,count.get()和count.incrementAndGet();都是AtomicLong类提供的实例方法
(3) 状态变量有多个时,对每个变量的更新都使用原子操作,结果未必是原子操作
正确的做法是:
当更新某一个状态变量时, 如果各个状态变量之间不是彼此独立的,那么需要在同一个原子操作中, 对其他变量同时进行更新
-
内置锁与synchronized
(1) 每个Java对象都可以用作一个实现同步的锁,称为内置锁(Intrinsic Lock)或监控器锁(Monitor Lock),用作synchronized(...)
(2) 在synchronized块中,无论程序正常运行结束,还是抛出异常,都会在退出同步代码块时自动释放锁
(3) 重入
如果一个线程试图获得一个已经由它自己持有的锁, 那么这个请求一定会成功, 不会引发死锁
例如
1° 递归函数 如果加上了synchronized, 每次调用自己时不会被阻塞
2° __子类覆盖了父类的synchronized方法, 且在方法中又显示调用父类的方法时(super.xxx()),不会被阻塞
一种实现原理是, 为每个锁关联一个计数值和一个所有者线程,如果请求锁时发现占有锁的是所有者线程本身,那么只会增加计数值,不会阻塞
-
(1) 某个线程获得对象的锁以后, 只能阻止其他线程获得同一个锁, 不能阻止其他线程获取其他的锁
因此, 每个共享的和可变的变量都应该只由一个锁来保护
(2) 一种常见的加锁约定
将所有的可变、共享的状态变量都封装在对象内部, 并通过对象本身(this)的内置锁对所有访问可变状态的代码路径进行同步
-
同步代码块的大小
(1) 理论上, 可以将所有方法都尽可能加上synchronized,但是一来滥用会导致性能问题,二来每个方法都是原子操作,不代表整体就是原子操作
(2) 同步代码块的合理大小取决于三个因素
1° 安全性: 必须达到
2° 简单性: 同步的部分设计不能过于复杂
3° 性能: 同步代码块范围过大会导致性能问题
(3) 安全性必须达到, 简单性和性能常常是矛盾的。
当实现某个同步策略时,一定不能盲目的为了性能而牺牲简单性;
当执行时间较长的计算和IO操作时,一定不要继续持有锁。
(4) 一个设计合理的示例
@ThreadSafe public class CachedFactorizer extends GenericServlet implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; @GuardedBy("this") private long hits; @GuardedBy("this") private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = i; lastFactors = factors.clone(); } } encodeIntoResponse(resp, factors); } private void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } private BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } private BigInteger[] factor(BigInteger i) { return new BigInteger[]{i}; } }
这个示例首先满足了安全性; 其次将同步代码块的范围尽可能缩小, 满足了性能要求; 再次没有将同步代码块拆的很零碎,满足了简单性要求。所以是一个设计良好的类。
对单个变量实现原子操作来说, 原子变量(AtomicXXX)很有效;但是由于多个原子操作的叠加未必是一个原子操作,所以当状态变量很多时, 仍有考虑所有状态变量的同步更新访问问题,即便它们都使用了原子变量。
在这个示例中,对hits和cacheHits这两个状态变量的更新和访问使用了synchronized机制,没有必要引入原子变量,增大设计的复杂程度
-
总结
(1) 无状态的类一定是线程安全的
(2) 当在无状态的类中添加一个状态变量时,如果该状态完全由线程安全的对象来管理,那么这个类一定是线程安全的
例如只添加一个状态变量时,添加一个原子变量类型AtomicXXX就很合适
(3) 当更新某一个状态变量时, 如果各个状态变量之间不是彼此独立的,那么需要在同一个原子操作中, 对其他变量同时进行更新
一种常见的约定是: 将所有的可变、共享的状态变量都封装在对象内部, 并通过对象本身(this)的内置锁对所有访问可变状态的代码路径进行同步
(4) 同步代码块的大小要合理,必须满足安全性,且在简单性和性能方面权衡合适
网友评论