java核心技术卷I-同步(二)

同步阻塞

每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。

synchronized (obj) // this is the syntax for a synchronized block
{
	critical section
}

于是它获得 Obj 的锁

监视器概念

锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器(monitor)。
监视器具有如下特性:

监视器是只包含私有域的类。
每个监视器类的对象有一个相关的锁。
使用该锁对所有的方法进行加锁。换句话说,如果客户端调用obj.method(), 那 么 obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问该域。
该锁可以有任意多个相关条件

Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监视器方法。通过调用 wait/notifyAll/notify

Volatile域

volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
Volatile 变量不能提供原子性。

public void flipDone() { done = !done; } // not atomic
count ++;

final 变量

除非使用锁或 volatile 修饰符,否则无法从多个线程安全地读取一个域。还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。

final Map<String, Double> accounts = new HashMap<>();

其他线程会在构造函数完成构造之后才看到这个 accounts 变量。
如果不使用 final,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是看到 null , 而不是新构造的 HashMap。
当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。

原子性

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
java.util.concurrent.atomic 包中有很多类使用了很高效的机器级指令(而不是使用锁) 来保证其他操作的原子性。 例如, Atomiclnteger 类提供了方法 incrementAndGet 和decrementAndGet, 它们分别以原子方式将一个整数自增或自减。例如,可以安全地生成一个数值序列。

public static AtomicLong nextNumber = new AtomicLong() ;
// In some thread...
long id = nextNumber.increinentAndGet():

incrementAndGet 方法以原子方式将 AtomicLong 自增, 并返回自增后的值。也就是说,获得值、 增1并设置然后生成新值的操作不会中断。可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值。

死锁

所有线程都被阻塞,这样的状态称为死锁(deadlock )。

账户 1: $200
账户 2: $300
线程 1: 从账户 1 转移 $300 到账户 2
线程 2: 从账户 2 转移 $400到账户 1

线程 1 和线程 2 都被阻塞了。因为账户 1 以及账户 2 中的余额都不足以进行转账,两个线程都无法执行下去

线程局部变量

有时可能要避免共享变量, 使用ThreadLocal 辅助类为各个线程提供各自的实例。 例如,SimpleDateFormat 类不是线程安全的,假设有一个静态变量

public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作

String dateStamp = dateFormat.format(new Date());

结果可能很混乱,因为 dateFormat 使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大; 或者也可以在需要时构造一个局部 SimpleDateFormat 对象,不过这也太浪费了。
要为每个线程构造一个实例,可以使用以下代码:

public static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

要访问具体的格式化方法,可以调用

String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用 get 时, 会调用 initialValue 方法。在此之后, get 方法会返回属于当前线程的那个实例。
在多个线程中生成随机数也存在类似的问题。java…util.Rand0m 类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器, 这会很低效。
可以使用 ThreadLocal 辅助类为各个线程提供一个单独的生成器, 不过 Java SE 7 还另外提供了一个便利类。只需要做以下调用

int random = ThreadLocalRandom.current().nextlnt(upperBound):

ThreadLocalRandom.current() 调用会返回特定于当前线程的 Random 类实例。

锁测试与超时

线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。tryLock 方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回false, 而且线程可以立即离开去做其他事情。

if (myLock.tryLock())
{
// now the thread owns the lock
try { . . . }
finally { myLock.unlock(); }
}
else
// do something else

可以调用 tryLock 时,使用超时参数,像这样:

if (myLock.tryLock(100, TineUnit.MILLISECONDS)) . . .

TimeUnit 是一 枚举类型,可以取的值包括 SECONDS、MILLISECONDS,MICROSECONDS和 NANOSECONDS。
lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁, 那么,lock 方法就无法终止。
然而, 如果调用带有用超时参数的 tryLock, 那么如果线程在等待期间被中断,将抛出InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。
也可以调用 locklnterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。
在等待一个条件时, 也可以提供一个超时:

myCondition.await(100, TineUniBILLISECONDS))

如果一个线程被另一个线程通过调用 signalAU 或 signal 激活, 或者超时时限已达到,或者线程被中断, 那么 await 方法将返回。
如果等待的线程被中断, await 方法将抛出一个 InterruptedException 异常。在你希望出现这种情况时线程继续等待(可能不太合理,) 可以使用 awaitUninterruptibly 方法代替 await。

读 / 写锁

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(); }
}

为什么弃用stop和suspend方法

初始的 Java 版本定义了一个 stop 方法用来终止一个线程, 以及一个 suspend 方法用来阻塞一个线程直至另一个线程调用 resume。stop 和 suspend 方法有一些共同点:都试图控制一个给定线程的行为。
stop、 suspend 和 resume 方法已经弃用。stop 方法天生就不安全,经验证明 suspend方法会经常导致死锁。
首先来看看 stop 方法, 该方法终止所有未结束的方法, 包括 run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如’假定 TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。
当线程要终止另一个线程时, 无法知道什么时候调用 stop 方法是安全的, 什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程, 被中断的线程会在安全的时候停止。
接下来, 看看 suspend方法有什么问题。与 stop 不同,suspend 不会破坏对象。但是,如果用 suspend 挂起一个持有一个锁的线程, 那么,该锁在恢复之前是不可用的。如果调用suspend 方法的线程试图获得同一个锁, 那么程序死锁: 被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。

阻塞队列

对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插人元素, 消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。
当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队列(blocking queue ) 导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢, 第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快, 它将等待第二个队列集赶上来。
在这里插入图片描述
阻塞队列方法分为以下3类, 这取决于当队列满或空时它们的响应方式。如果将队列当作线程管理工具来使用, 将要用到 put 和 take 方法。当试图向满的队列中添加或从空的队列中移出元素时,add、 remove 和 element 操作抛出异常。当然,在一个多线程程序中, 队列会在任何时候空或满, 因此,一定要使用 offer、 poll 和 peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
poll和 peek 方法返回空来指示失败。 因此,向这些队列中插入 null 值是非法的。还有带有超时的 offer 方法和 poll 方法的变体。例如,下面的调用:

boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);

尝试在 100 毫秒的时间内在队列的尾部插人一个元素。如果成功返回 true ; 否则, 达到超时时,返回 false。类似地,下面的调用:

Object head = q.poll(100, TimeUnit.MILLISECONDS)

尝试用 100 毫秒的时间移除队列的头元素;如果成功返回头元素,否则,达到在超时时,返回 null。
如果队列满, 则 put 方法阻塞;如果队列空, 则 take 方法阻塞。在不带超时参数时,offer 和 poll 方法等效。
java.util.concurrent 包提供了阻塞队列的几个变种。 默认情况下,LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque 是一个双端的版本。ArrayBlockingQueue 在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数, 则那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。
PriorityBlockingQueue 是一个带优先级的队列, 而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的, 取元素的操作会阻塞。
DelayQueue 包含实现 Delayed 接口的对象:

interface Delayed extends Comparable<Delayed>
{
	long getDelay(TimeUnit unit);
}

getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的情况下才能从 DelayQueue 移除。还必须实现 compareTo 方法。DelayQueue 使用该方法对元素进行排序。
JavaSE 7增加了一个 TranSferQueUe 接口,允许生产者线程等待, 直到消费者准备就绪可以接收一个元素。如果生产者调用

q.transfer(item);

这个调用会阻塞, 直到另一个线程将元素( item) 删除。LinkedTransferQueue 类实现了这个接口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

局外人一枚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值