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

同步

在大多数实际的多线程应用中, 两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象, 并且每一个线程都调用了一个修改该对象状态的方法, 可以想象, 线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生错误的对象。这样一个情况通常称为竞争条件(race condition)。

竞争条件详解

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。

锁对象

有两种机制防止代码块受并发访问的干扰。Java语言提供一个 synchronized 关键字达到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。synchronized 关键字自动提供一个锁以及相关的“ 条件”, 对于大多数需要显式锁的情况, 这是很便利的。但是, 我们相信在读者分別阅读了锁和条件的内容之后, 理解 synchronized 关键字是很轻松的事情。java.util.concurrent 框架为这些基础机制提供独立的类

private Lock bankLock = new ReentrantLock();// ReentrantLock implements the Lock interface
myLock.lock(); // a ReentrantLock object
try{
	critical section
}
finally
{
	myLock.unlock();// make sure the lock is unlocked even if an exception is thrown
}

这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象, 其他任何线程都无法通过 lock 语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。
把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则, 其他线程将永远阻塞。
假定一个线程调用 方法A, 在执行结束前被剥夺了运行权。假定第二个线程也调用方法A, 由于第二个线程不能获得锁, 将在调用 lock 方法时被阻塞。它必须等待第一个线程完成 方法A的执行之后才能再度被激活。当第一个线程释放锁时, 那么第二个线程才能开始运行。
锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count) 来跟踪对lock方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。
通常, 可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后, 另一个线程才能使用相同对象。
要留心临界区中的代码,不要因为异常的抛出而跳出临界区。如果在临界区代码结束之前抛出了异常,finally 子句将释放锁,但会使对象可能处于一种受损状态。

条件对象

通常, 线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
因为不满足条件,无法继续执行,线程获得了对 bankLock 的排它性访问,因此别的线程没有进行存款操作的机会。这就是为什么我们需要条件对象的原因。
一个锁对象可以有一个或多个相关的条件对象。你可以用 newCondition 方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如

class Bank
{
	private Condition sufficientFunds;
	public Bank()
	{
		sufficientFunds = bankLock.newConditionO;
	}
}

不满足条件是调用:

sufficientFunds.await();

当前线程现在被阻塞了,并放弃了锁。它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 signalAll 方法时为止,如

sufficientFunds,signalAll();

这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时, 它们将试图重新进人该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回, 获得该锁并从被阻塞的地方继续执行。
至关重要的是最终需要某个其他线程调用 signalAll 方法。当一个线程调用 await 时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock) 现象。如果所有其他线程被阻塞, 最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法, 那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
应该何时调用 signalAll 呢? 经验上讲, 在对象的状态有利于等待线程的方向改变时调用signalAll。
注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞, 以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
另一个方法 signal, 则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行, 那么它再次被阻塞。如果没有其他线程再次调用 signal, 那么系统就死锁了。
当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 await、signalAll 或signal 方法。

synchronized关键字

锁的特点和作用

锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
锁可以管理试图进入被保护代码段的线程。
锁可以拥有一个或多个相关的条件对象。
每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

Lock 和 Condition 接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌人到 Java语言内部的机制。从 1.0 版开始,Java中的每一个对象都有一个内部锁。如果一个方法用 synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

public synchronized void method()
{
	method body
}

等价于

public void method()
{
	this.intrinsidock.lock();
	try
	{
		method body
	}
	finally { this.intrinsicLock.unlock(); }
}

内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAll /notify方法解除等待线程的阻塞状态。调用 wait 或 notityAll 等价于

intrinsicCondition.await();
intrinsicCondition.signalAIl();

wait、notifyAll 以及 notify 方法是 Object 类的 final 方法。Condition 方法必须被命名为 await、signalAll 和 signal 以便它们不会与那些方法发生冲突

class Bank
{
	private double[] accounts;
	public synchronized void transfer(int from,int to, int amount) throws  InterruptedException
	{
		while (accounts[from] < amount)
			wait(); // wait on intrinsic object lock's single condition
			accounts[from] -= amount ;
			accounts[to] += amount ;
			notifyAll();// notify all threads waiting on the condition
	}
	public synchronized double getTotalBalance() { . . . }
}

可以看到, 使用 synchronized 关键字来编写代码要简洁得多。当然,要理解这一代码,你必须了解每一个对象有一个内部锁, 并且该锁有一个内部条件。由锁来管理那些试图进入synchronized 方法的线程,由条件来管理那些调用 wait 的线程。
将静态方法声明为 synchronized 也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。
内部锁和条件存在一些局限

不能中断一个正在试图获得锁的线程。
试图获得锁时不能设定超时。
每个锁仅有单一的条件, 可能是不够的

Lock 和 Condition 对象还是同步方法,选择的建议

最好既不使用 Lock/Condition 也不使用 synchronized 关键字。在许多情况下你可以使用
java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。 如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。
如果特别需要 Lock/Condition 结构提供的独有特性时,才使用Lock/Condition。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

局外人一枚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值