Java并发编程实战
第1章 简介
1.2 线程优势
1.2.1 发挥多处理器的强大能力
1.2.2 建模的简单性
1.2.3 异步事件的简化处理
1.2.4 响应更灵敏的用户界面
1.3 线程带来的风险
NPTL线程软件包是专门设计用于支持数十万个线程的,在大多数Linux发布版本中都包含了这个软件包。非阻塞I/O有其自身的优势,但如果操作系统能更好地支持线程,那么需要使用非阻塞IO的情况将变得更少。
1.3.1 安全性问题
UnsafeSequence类中一种常见的并发安全问题,称为竟态条件(Race Condition).
使用线程同步:
@ThreadSafe public class Sequence{ @GuardedBy("this") private int Value; public synchronized int getNext() { return this.Value++; } }
1.3.2 活跃性问题
死锁、饥饿、活锁:并发性错误
1.3.3 性能问题
服务时间过长、响应不灵敏、吞吐量、资源消耗、可伸缩性
上下文切换:保存和恢复上下文,丢失局部性、cpu消耗在线程调度上
1.4 线程无处不在
远程方法调用(Remote Method Invocation, RMI)
第2章 线程安全性
为了构建稳健的并发程序必须正确的使用线程和锁
Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但同步这个术语还包括volatile类型的变量,显示锁以及原子变量。
2.1 什么是线程安全性
可以在多个线程中调用,并且在线程之间调用不会出现错误的交互。
线程安全类:在并发环境和单线程环境中都不会被破坏的类。
无状态对象一定是线程安全的,因为没有共享状态,访问的都是不同的实例。
2.2 原子性
虽然++count是一种紧凑的语法,但这个操作并非原子操作的,因此它并不会作为一个不可分割的操作来执行。
2.2.1 竟态条件
“先检查后执行”
2.2.2 示例:延迟初始化中的静态条件
LazyInitRace
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance(){ if (instance == null) instance = new ExpensiveObject(); return instance; } }
2.2.3 复合操作
“先检查后执行”以及“读取-修改-写入”(例如递增运算)等操作 统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
使用AtomicLong类型的变量来统计已处理请求的数量
@ThreadSafe public class CountingFactorizer implements Servlet{ private final AutomicLong count = new AutomicLong(0); public long getCount(){ return count.get();} public void service (ServleteRequest req,ServletResponse resp){ BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i);//因数分解 count.incrementAndGet(); encodeIntoResponse(resp,factores); } }
在java.util.concurrent.atomic 包中包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。
2.3 加锁机制
线程安全类中可能存在竟态条件,产生错误的结果。
要保证状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
2.3.1 内置锁
同步代码块(Synchronized Block)
会带来性能上的缺少,因为同步方法同一时刻只能给一个客户访问。
2.3.2 重入
重入进一步提升了加锁行为的封装性
ReentrantLock:public class ReentrantLock implements Lock, java.io.Serializable
具备公平锁和非公平锁
https://blog.youkuaiyun.com/weixin_34192732/article/details/91399247
队列同步器(AbstractQueuedSynchronizer,后面简称AQS)
2.4 用锁来保护状态
如果只是将每个方法都作为同步方法,并不能确保复合操作都是原子的
if (!vector.contains(element)) vector.add(element);
2.5 活跃性与性能
同步方法带来的不良并发,可同时调用的数量受限。
应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。
@Threadsafe public class CachedFactorizer 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) ; } }
在 CachedFactorizer 中不再使用AtomicLong 类型的命中计数器,而是使用了一个long类型的变量。当然也可以使用AtomicLong类型,但使用CountingFactorizer带来的好处更多。
简单性:对整个方法进行同步。通常,在简单性与性能之间存在着相互制约的因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)
并发性:对尽可能短的代码路径进行同步
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。
第3章 对象的共享
3.1 可见性
在没有同步的情况下共享变量(不要这么做)
public class NoVisibility { private static boolean ready; private static intnumber; private static class ReaderThread extendd Thread { public void run(){ while(!ready){ Thread.yield(); System.out.println(number); } } } public static void main(String args[]){ new ReaderThread().start(); number = 42; ready = true; } }
NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写人number的值,这种现象被称为“重排序(Reordering)”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序)那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number,然后在没有同步的情况下写入ready,那么读线程看到的顺序可能与写人的顺序完全相反。
只要有数据在多个线程之间共享,就使用正确的同步。
3.1.1 失效数据
前面代码读线程查看ready变量时,读到的是失效数据。
在缺少同步的情况下,java内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中。此外,它还允许cpu对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。
非线程安全类:
@NotThreadsafe public class MutableInteger { private int value ; public intget () { return value; } public void set (int value) { this.value - value; } }
3.1.2 非原子的64位操作
当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性( out-of-thin-airsafety)。
这种级别将牺牲准确性以获取性能上的提升
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。JVM允许将64位的读写操作分解为32位的操作,存在高32位和低32位不是同一个的情况,使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。
3.1.3 加锁与可见性
内置锁可以确保某个线程以一种可预测的方式来查看另一个线程的执行结果
在访问某个共享变量的可变变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。
3.1.4 Volatile变量
没有加锁也不会执行线程阻塞,因此volatile变量是一种比sychronized关键字更加轻量级的同步机制。
transient volatile boolean value; public boolean getValue() { return value; } public void setValue(boolean value) { this.value = value; }
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。
volatile变量的一种典型用法:
volatile boolean asleep; ... while (!asleep){ countSomeSheep(); }
asleep必须为volatile变量。否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。我们也可以用锁来确保asleep更新操作的可见性,但这将使代码变得更加复杂。
加锁机制可以确保可见性又可以确保原子性,而volatile只能确保可见性
当且仅当满足以下所有条件时,才应该使用volatile变量:
-
对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
-
该变量不会与其他状态变量一起纳入不变性条件中。
-
在访问变量时不需要加锁。
3.2 发布与逸出
使用工厂方法来防止this引用在构造过程中逸出
public class SafeListener { private final EventListener listener; private SafeListener(){ listener = new EventListener(){ public void onEvent(Event e){ doSomething(e); } } } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }
具体来说,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this 引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。在程序清单3-8的SafeListener中就使用了这种技术。
3.3 线程封闭
java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类。
3.3.1 Ad-hoc线程封闭
比较脆弱
3.3.2 栈封闭
3.3.3 ThreadLocal类
使用ThreadLocal来维持线程封闭性
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection> (){ public Connection initialValue(){ return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection(){ return connectionHolder.get(); }
3.4 不变性
不可变对象一定是线程安全的
当满足以下条件时,对象才是不可变的:
-
对象创建以后其状态就不能修改
-
对象的所有域都是final类型
-
对象是正确创建的(在对象创建期间,this没有逸出)
3.4.1 Final域
关键字final可以视为C++中const机制
除非需要更高的可见性,否则应将所有的域都声明为私有域,是一个良好的编程习惯;除非需要某个域是可变的,否则应将其声明为final域,也是一个良好的编程习惯。
对数值及其因数分解结果进行缓存的不可变容器类
@Immutable class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i,BigInteger[] factors){ lastNumber = i; lastFactors = Arrays.copyOf(factors,factors.length); } public BigInteger[] getFactors(BigInteger i){ if(lastNumber == null || !lastNumber.equals(i)) return null; else return Array.copyOf(lastFactors,lastFactors.length); } }
使用Array.copyOf()使得类不可变
3.4.2 示例:使用Voltile类型来发布不可变对象
使用指向不可变容器对象的Volatile类型引用以缓存最新的结果
@ThreadSafe public class VolatileCachedFatorizer implements Servlet { private volatile OnValueCache cache = new OneValueCache(null,null); public void service(ServleteRequest req,ServletResponse resp){ BigInteger i = extractFronRequest(req); BigInteger[] factors = cache.getFactors(i); if(factors == null){ factors = factor(i);//求公因式方法 cache = new OneValueCache(i,factors); } encodeIntoResponse(resp,factors); } }
与cache相关的操作不会相互干扰,因为不可变且每条路径只访问一次。通过使用包含多个状态的变量的容器对象来维持不变性条件,并使用volatile类型确保可见性。
3.5 安全发布
多线程之间共享对象,必须确保安全的进行共享。
在没有足够同步的情况下发布对象(不要这么做)
//不安全的发布 public Holder holder; public void initialize(){ holder = new Holder(42); }
由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。这种不正确的发布导致其他线程看到尚未创建完成的对象。
3.5.1 不正确的发布:正确的对象被破坏
不能指望一个尚未完全被创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态突然发生变化。
由于未被正确发布,因此这个类可能出现故障
public class Holder { private int n; public Holder(int n) {this.n = n;} public void assertSanity(){ if(n != n) throw new AssertionError("This statement is false."); } }
如果没有足够的同步,那么当在多个线程间共享数据时将发生一些奇怪的事情。解决办法,改为不可变。
3.5.2 不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
3.5.3 安全发布的 常用模式
3.5.4 事实不可变对象
发布后不会被修改。可以简化开发过程,而且还能由于减少了同步而提高性能。
在没有额外同步的情况下,任何线程都可以安全的使用被安全发布的事实不可变对象。
3.5.5 可变对象
对象的发布需求取决于它的可变性:
-
不可变对象可以通过任意机制来发布。
-
事实不可变对象必须通过安全方式来发布。
-
可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
3.5.6 安全地共享对象
理解共享对象的“既定规则”,发布一个对象时,必须明确地说明对象的访问方式。
策略:
-
线程封闭:对象只能由一个线程拥有
-
只读共享
-
线程安全共享:对象内部实现同步
-
保护对象:持有特定的锁来访问。
第4章 对象的组合
4.1 设计线程安全的类
可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计线程安全类的过程中,需要包含以下三个基本要素
-
找出构成对象状态的所有变量。
-
找出约束状态变量的不变性条件。
-
建立对象状态的并发访问管理策略。
@ThreadSafe public final class Counter { @GuardedBy("this") private long value = 0; public synchronized long getValue(){ return value; } public synchronized long increment(){ if(value == Long.MAX_VALUE) throw new IllegalStateException("counter overflow"); return ++value; } }
同步策略(Synchronization Policy)
4.1.1 收集同步需求
保证原子性和封装性
4.1.2 依赖状态的操作
如果某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
先验条件、依赖状态的操作
4.1.3 状态所有权
所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权。最多的是“共享控制权”。
容器类通常表现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。Servlet框架中的ServletContext就是其中一个示例。
4.2 实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象方法上,从而更容易确保线程在访问数据时总能持有正确的锁
通过封闭机制来确保线程安全
@ThreadSafe public class personSet { private final Set<Person> mySet = new HashSet<Person>(); public synchronized void addPerson(Person p){ mySrt.add(p); } public synchronized boolean containsPerson(person p){ return mySet.contains(p); } }
4.2.1 Java监视器模式
通过一个私有锁来保护状态
public class PrivateLock { private final Object myLock = new Oject(); @GuardedBy("myLock") Widget widget; void someMethod(){ synchronized(myLock){ //访问修改Widget的状态 } } }
4.2.2 示例:车辆追踪
Map<string,Point> locations e vehicles.getLocations() ;for (string key : locations.keyset () ) rendervehicle (key, locations.get (key) );
基于监视器模式的车辆追踪
Threadsafe public class MonitorvehicleTracker { GuardedBy ( "this") private final Map<string,MutablePoint> locations; public MonitorvehicleTracker ( Map<string, MutablePoint> locations) {this . locations = deepCopy ( locations) ; } public synchronized Map<string,MutablePoint> getLocations ( ) { return deepCopy ( locations) ; public synchronizedMutablePoint getLocation(string id){ MutablePoint loc = locations.get (id) ; return loc == null ? null : new MutablePoint (loc) ; } public synchronizedvoid setLocation (String id, int x, int y) { MutablePoint loc - locations.get (id) ; if ( loc == null) throw new IllegalArgumentException ( "No such ID: " + id) ;loc.x = x ; loc.y = y; } private static Map<String,MutablePoint > deepcopy( Mapestring,MutablePoint> m){ Map<string,MutablePoint> result =· new HashMapestring,MutablePoint> ( ) ; for (string id : m.keyset ( ) ) result.put (id, new MutablePoint (m.get (id) ) ) ; return Co1lections.unmodifiableMap(result) ; }} public class MutablePoint {/**/}//设置x,y两个变量
4.3 线程安全性的委托
4.3.1 示例:基于委托的车辆追踪器
使用不可变的Point类
public class point { public final int x,y; public Poin(int x,int y){ this.x = x; this.y = y; } }
@Threadsafe public class DelegatingvehicleTracker { private inal ConcurrentMap<string,Point> locations;private final Map<string, Point> unmodifiableMap; public DelegatingvehicleTracker(Map<string,Point> points){ locations = new ConcurrentHashMap<string,Point>(points)unmodifiableMap - collections.unmodifableMap (locations) ; } public void setLocation (string id, int x, int y) { if(locations.replace(id, new Point(x, y)) == nul1) throw new IllegalArgumentException ( "invalid vehicle name : " +id) ; }
ConcurrentHashMap源码学习
4.3.3 当委托失效时
NumberRange类并不足以保护它的不变性条件
public class NumberRange { //不变性条件:lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { //注意-不安全的“先检查后执行” if(i > upper.get()) throw new IllegalArgumentException( "can't set lower to "+ i + " > upper"); lower.set(i); } }