并发和同步(下)
Java核心技术卷(10th)读书笔记
同步阻塞
每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有 另一种机制可以获得锁,通过进入一个同步阻塞。
synchronized (obj) // this is the syntax for a synchronized block
{
critical section
}
监视器概念
- 锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的 。面向对象的解决方案是监视器。监视器具有以下特点:
- 监视器是只包含私有域的类。
- 每个监视器类的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用
obj.method()
, 那 么obj
对象的锁是在方法调用开始时自动获得, 并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。 - 该锁可以有任意多个相关条件。
Volatile域
-
同步格言: “ 如果向一个变量写入值,而这个变量接下 来可能会被另一个线程读取, 或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的, 此时必须使用同步 。
-
volatile
关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile
, 那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。 -
警告:
Volatile
变量不能提供原子性。例如, 方法public void flipDone() { done = !done; } // not atomic
不能确保翻转域中的值。不能保证读取、 翻转和写入不被中断。
final 变置
- 还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。
final Map<String, Double〉accounts = new HashKap<>();
-
其他线程会在构造函数完成构造之后才看到这个 accounts 变量 。
-
当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然 需要进行同步。
原子性
-
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为
volatile
。 -
java.util.concurrent.atomic
包中有很多类使用了很高效的机器级指令来保证其他操作的原子性。 例如,Atomiclnteger
类提供了方法incrementAndGet
和decrementAndGet
, 它们分别以原子方式将一个整数自增或自减。
public static AtomicLong nextNumber = new AtomicLong();
// In some thread...
long id = nextNumber.increinentAndGet():
-
有很多方法可以以原子方式设置和增减值, 不过, 如果希望完成更复杂的更新,就必须使用
compareAndSet
方法。 -
应当在一个循环中计算新值和使用
compareAndSet
。
do {
oldValue = largest.get();
newValue = Math.max (oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));
-
如果另一个线程也在更新
largest
,就可能阻止这个线程更新。这样一来,compareAndSet
会返回false
,而不会设置新值。在这种情况下,循环会更次尝试,读取更新后的值,并尝试修改。最终,它会成功地用新值替换原来的值。这听上去有些麻烦,不过compareAndSet
方 法会映射到一个处理器操作, 比使用锁速度更快。 -
在
Java SE 8
中,不再需要编写这样的循环样板代码。实际上,可以提供一个lambda
表 达式更新变量,它会为你完成更新。对于这个例子,我们可以调用:largest. updateAndGet(x -> Math.max(x, observed))
; -
类
Atomiclnteger、AtomicIntegerArray、AtomicIntegerFieldUpdater、AtomicLongArray、 AtomicLongFieldUpdater、AtomicReference、AtomicReferenceArray 和 AtomicReferenceFieldUpdater
也提供了这些方法。 -
如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重 试。Java SE 8 提供了
LongAdder
和LongAccumulator
类来解决这个问题。LongAdder
包括多 个变量(加数,) 其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动 提供新的加数。通常情况下, 只有当所有工作都完成之后才需要总和的值, 对于这种情况, 这种方法会很高效。性能会有显著的提升。 -
如果认为可能存在大量竞争, 只需要使用
LongAdder
而不是AtomicLong
。方法名稍有区 别。调用increment
让计数器自增,或者调用 add 来增加一个量, 或者调用 sum 来获取总和。 -
LongAccumulator 将这种思想推广到任意的累加操作。在构造器中,可以提供这个操作 以及它的零元素。要加人新的值, 可以调用 accumulate。调用 get 来获得当前值。下面的代 码可以得到与 LongAdder 同样的效果。
LongAccumulator adder = new LongAccumulator(Long::sum, 0);
// In some thread...
adder.accumulate(value);
- 在内部,这个累加器包含变量 a1, a2,… an。 每个变量初始化为零元素 。 如果选择一个不同的操作,可以计算最小值或最大值。一般地, 这个操作必须满足结合 律和交换律。这说明, 最终结果必须独立于所结合的中间值的顺序。
死锁
-
锁和条件不能解决多线程中的所有问题。考虑下面的情况:
账户1: $200
账户2: $300
线程1: 从账户1 转移$300 到账户2
线程2: 从账户2 转移$400 到账户1线程1 和线程2 都被阻塞了。因为账户1 以及账户2 中的余额都不足以
进行转账,两个线程都无法执行下去。
有可能会因为每一个线程要等待更多的钱款存人而导致所有线程都被阻塞。这样的状态称为死锁(deadlock )。 -
当程序挂起时, 键入
CTRL+\
, 将得到一个所有线程的列表。每一个线程有一个栈踪迹, 告诉你线程被阻塞的位置。
线程局部变量
-
在线程间共享变量的风险。有时可能要避免共享变量, 使用
ThreadLocal
辅助类为各个线程提供各自的实例。 -
对于某些类型,其内部实现可能是非线程安全的,可能会被并发的数据访问所破坏。当然可以使用同步, 但开销很大; 或者也可以在需要时构造一个局部
非线程安全类型
对象,不过这也太浪费了。所以为每个线程构造一个实例是一个不错的选择。示例:
public static final ThreadLocal<非线程安全类型> 变量 = ThreadLocal.withInitial(() -> new 非线程安全类型); // 需要访问该实例时 变量.get() // 即可获得该变量所支持的方法
在一个给定线程中首次调用
get
时, 会调用initialValue
方法。在此之后,get
方法会返回属于当前线程的那个实例。在多个线程中生成随机数也存在类似的问题。
java..util.Random
类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器, 这会很低效。
可以使用ThreadLocal
辅助类为各个线程提供一个单独的生成器, 不过Java SE 7 还另外提供了一个便利类。只需要做以下调用:int random = ThreadLocalRandom.currentO.nextlnt(upperBound);
ThreadLocalRandom.current()
调用会返回特定于当前线程的Random
类实例。
锁测试与超时
-
线程在调用
lock
方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。tryLock
方法试图申请一个锁, 在成功获得锁后返回true
, 否则, 立即返回false
, 而且线程可以立即离开去做其他事情。可以调用tryLock
时,使用超时参数,像这样:
if (myLock.tryLock(100, TineUnit.MILLISECONDS))
-
lock
方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁, 那么,lock
方法就无法终止。 -
然而, 如果调用带有用超时参数的
tryLock
, 那么如果线程在等待期间被中断,将抛出InterruptedException
异常。这是一个非常有用的特性,因为允许程序打破死锁。
也可以调用locklnterruptibly
方法。它就相当于一个超时设为无限的tryLock
方法。
在等待一个条件时, 也可以提供一个超时:
myCondition.await(100, TineUniBILLISECONDS))
如果一个线程被另一个线程通过调用signalAll
或signal
激活, 或者超时时限已达到, 或者线程被中断, 那么await
方法将返回。 -
提示
boolean tryLock() /* 尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁, 即使该锁有公平加锁策略, 即便其他线程已经等待很久也是如此。 */
读写锁
-
java.util.concurrent.locks
包定义了两个锁类,ReentrantLock
类和ReentrantReadWriteLock
类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话, 后者是十分有用的。在这种情况下, 允许对读线程共享访问是合适的。当然,写线程依然必须是互斥访问的。示例:
// 1 ) 构造一个ReentrantReadWriteLock 对象: private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(): // 2 ) 抽取读锁和写锁: private Lock readLock = rwl.readLock() ; private Lock writeLock = rwl.writeLock(); // 3 ) 对所有的获取方法加读锁: public double getTotalBalance() { readLock.lock(); try { . . . } finally { readLock.unlock() ; } } // 4 ) 对所有的修改方法加写锁: public void transfer(. . .) { writeLock.lock(); try { . . . } finally { writeLock.unlock(); } }
注:读锁会排斥所有写操作,写锁会排斥所有的读和写操作。
挂起
如果想安全地挂起线程, 引人一个变量suspendRequested
并在run
方法的某个安全的地方测试它, 安全的地方是指该线程没有封锁其他线程需要的对象的地方。当该线程发现suspendRequested
变量已经设置, 将会保持等待状态直到它再次获得为止。