Java编程思想20

2. 解决共享资源竞争
  前面的示例展示了使用线程时的一个基本问题∶你永远都不知道一个线程何时在运行。想象一下,你坐在桌边手拿叉子,正要去叉盘子中的最后一片食物,当你的叉子就要够着它时,这片食物突然消失了,因为你的线程被挂起了,而另一个餐者进入并吃掉了它。这正是在你编写并发程序时需要处理的问题。对于并发工作,你需
某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。
  防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推。
  基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)。
  考虑一下屋子里的浴室多个人(即多个由线程驱动的任务)都希望能单独使用浴室(即共享资源)。为了使用浴室,一个人先敲门,看看是否能使用。如果没人的话,他就进入浴室并锁上门。这时其他人要使用浴室的话,就会被“阻挡”,所以他们要在浴室门口等待,直到浴室可以使用。当浴室使用完毕,就该把浴室给其他人使用了(别的任务就可以访问资源了),这个比喻就有点不太准确了。事实上,人们并没有排队,我们也不能确定谁将是下一个使用浴室的人,因为线程调度机制并不是确定性的。实际情况是∶等待使用浴室的人们簇拥在浴室门口,当锁住浴室门的那个人打开锁准备离开的时候,离门最近的那个人可能进入浴室。如前所述,可以通过yield()和setPriority()来给线程调度器提供建议,但这些建议未必会有多大效果,这取决于你的具体平台和JVM实现。

1. 使用synchronized关键字
  Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。

  在生成偶数的代码中,你已经看到了,你应该将类的数据成员都声明为private的,而且只能通过方法来访问这些数据;所以可以把方法标记为synchronized来防止资源冲突。下面是声明synchronized方法的方式∶

synchronized void f() { /* ... */ }
synchronized void g() { /* ... */ }

所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。对于前面的方法,如果某个任务对对象调用了f(),对于同一个对象而言,就只能等到f()调用结束并释放了锁之后,其他任务才能调用f()和g()。所以,对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问被编码为对象内存。

  注意,在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。
  一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数变为1。每当这个相同的任务在这个对象上获得锁时,计数都会递增。显然,只有首先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用此资源。

  针对每个类,也有一个锁(作为类的Class对象的一部分);所以synchronized static方法可以在类的范围内防止对static数据的并发访问。
同步控制EventGenerator,通过在EventGenerator.java中加入synchronized 关键字,可以防止不希望的线程访问:

package concurrency;

/**
* @author Mr.Sun
* @date 2022年09月04日 10:49
*
* 同步控制EventGenerator
*/
public class SyncEventGenerator extends IntGenerator {
private int currentCountVal = 0;

@Override
public synchronized int next() {
++currentCountVal;
Thread.yield();
++currentCountVal;
return currentCountVal;
}

public static void main(String[] args) {
EventChecker.test(new SyncEventGenerator());
}
}

 

对Thread.yield()的调用被插入到了两个递增操作之间,以提高在currentEvenValue是奇数状态时上下文切换的可能性。因为互斥可以防止多个任务同时进入临界区,所以这不会产生任何失败。但是如果失败将会发生,调用yield()是一种促使其发生的有效方式。
  第一个进入next()的任务将获得锁,任何其他试图获取锁的任务都将从其开始尝试之时被阻塞,直至第一个任务释放锁。通过这种方式,任何时刻只有一个任务可以通过由互斥量看护的代码。

posted @   就叫清风吧  阅读(17)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示