多个线程一起办事固然能够加快处理速度,但是也带来一个问题:两个线程同时争抢某个资源时该怎么办?看来资源共享的另一面便是资源冲突,正所谓鱼与熊掌不可兼得,系统岂能让多线程这项技术专占好处?果然是有利必有弊,且看之前演示售票任务时候的多线程操作,具体代码如下所示:

	// 多个线程同时操作某个资源,可能会产生冲突
private static void testConflict() {
// 创建一个售票任务
Runnable seller = new Runnable() {
private Integer ticketCount = 100; // 可出售的车票数量 @Override
public void run() {
while (ticketCount > 0) { // 还有余票可供出售
ticketCount--; // 余票数量减一
// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息
// 为更好地重现资源冲突情况,下面尽量拉大访问ticketCount的时间间隔
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
String dateTime = sdf.format(new Date());
String desc = String.format("%s %s 当前余票为%d张", dateTime,
Thread.currentThread().getName(), ticketCount);
System.out.println(desc);
}
}
};
new Thread(seller, "售票线程A").start(); // 启动售票线程A
new Thread(seller, "售票线程B").start(); // 启动售票线程B
new Thread(seller, "售票线程C").start(); // 启动售票线程C
}

光光看代码感觉并无不妥之处,仅仅是起了三个售票线程共同卖票呗,这能有什么问题?!倘若只运行一次售票代码,倒也看不出什么名堂,可是一旦反复地多次运行这段售票代码,那么总会出现类似下列日志的意外情况,特别是在系统资源比较繁忙的时刻:

10:56:38.182 售票线程A 当前余票为97张
10:56:38.182 售票线程B 当前余票为97张
10:56:38.182 售票线程C 当前余票为97张
10:56:38.186 售票线程B 当前余票为95张
10:56:38.186 售票线程A 当前余票为95张
10:56:38.186 售票线程C 当前余票为93张
………………………这里省略余下的日志……………………

我的天,售票日志竟然打印出了相同的余票数量,这正是多线程并发造成的结果。因为在ticketCount的自减语句和后面的日志打印语句中间还有其它代码,每行代码都需要消耗一点点的时间,哪怕是零点几毫秒,但就在这一瞬间,余票可能又被别的线程卖掉了一张,所以等到线程A打印余票日志之时,ticketCount早已被卖了不止一次。如此一来,日志打印前后的余票数量遇到不一致的情况,也就不足为奇了。
问题的症结在于余票变量ticketCount是动态变化着的,三个售票线程争先恐后地卖票,故而任一时刻的余票数量都可能发生改变。解决问题的要点自然落在余票的管控上面,正好Java提供了一个名叫synchronized的关键字,它可用来修饰某个方法或者某块代码,目的是限定该方法/代码块为同步方法/同步代码块,也就是规定同一时刻只能有一个线程执行同步方法,其它线程来了以后必须在旁边等待,直到先来的线程跑完同步方法,其它线程方可依次排队执行该同步方法。
回到之前的售票代码,第一反应是能否把售票任务的run方法设置为同步方法?与其瞎猜测,不如试试再说,于是给run方法加上关键字synchronized之后的代码片段如下所示:

			// 指定整个run方法为同步方法,这样同一时刻只允许一个线程执行该方法
public synchronized void run() {
while (ticketCount > 0) { // 还有余票可供出售
ticketCount--; // 余票数量减一
// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息
String left = String.format("当前余票为%d张", ticketCount);
PrintUtils.print(Thread.currentThread().getName(), left);
}
}

添加完毕再次运行售票代码,观察到了以下的售票日志:

22:46:06.733 售票线程A 当前余票为99张
22:46:06.734 售票线程A 当前余票为98张
22:46:06.735 售票线程A 当前余票为97张
22:46:06.735 售票线程A 当前余票为96张
………………………这里省略余下的日志……………………

可见现在只剩线程A在兀自卖票,而线程B和线程C呆在一旁陪太子读书。原来synchronized给整个run方法加锁,那么只要线程A尚未结束运行,线程B和线程C就都不允许置身其中,结果便退化为只有一个线程在售票了。显然给run方法添加synchronized的做法管得太多了,其实仅有ticketCount这个余票变量会引起资源冲突,因此不妨缩小synchronized的管辖面,单单把余票减一的代码通过synchronized加以限定,并定义一个局部变量count来保存减一后的余票数值。重新修改后的售票代码片段示例如下:

			public void run() {
while (ticketCount > 0) { // 还有余票可供出售
int count;
// 指定某个代码块为同步代码块,这样同一时刻只允许一个线程执行该段代码
synchronized (this) {
count = --ticketCount; // 余票数量减一
}
// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息
String left = String.format("当前余票为%d张", count);
PrintUtils.print(Thread.currentThread().getName(), left);
}
}

多次运行修改后的售票代码,观察到的售票日志终于正常打印余票数量了:

16:33:10.265 售票线程A 当前余票为99张
16:33:10.265 售票线程C 当前余票为97张
16:33:10.265 售票线程B 当前余票为98张
16:33:10.266 售票线程A 当前余票为96张
16:33:10.266 售票线程B 当前余票为94张
16:33:10.266 售票线程C 当前余票为95张
………………………这里省略余下的日志……………………

注意到上述的同步代码块把余票数量赋值给一个局部变量,仿佛某个带返回值的方法,既然这块代码的形式与方法相像,干脆提取出来作为独立的同步方法,于是优化后的售票代码变成了下面这般:

	// 把操作共享资源的代码单独提取出来作为同步方法
private static void testSyncMinMethod() {
// 创建一个售票任务
Runnable seller = new Runnable() {
private Integer ticketCount = 100; // 可出售的车票数量 @Override
public void run() {
while (ticketCount > 0) { // 还有余票可供出售
// 获得减一后的余票数量。注意getDecreaseCount是个同步方法
int count = getDecreaseCount();
// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息
String left = String.format("当前余票为%d张", count);
PrintUtils.print(Thread.currentThread().getName(), left);
}
} // 将余票数量减一,并返回减后的余票数量
private synchronized int getDecreaseCount() {
return --ticketCount; // 余票数量减一
}
};
new Thread(seller, "售票线程A").start(); // 启动售票线程A
new Thread(seller, "售票线程B").start(); // 启动售票线程B
new Thread(seller, "售票线程C").start(); // 启动售票线程C
}

以上代码同样有效避免了售票之时的资源冲突,并且代码的组织结构更加清晰明了。

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(一百)线程同步synchronized的更多相关文章

  1. Java开发笔记(一百零三)线程间的通信方式

    前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...

  2. Java开发笔记(一百零四)普通线程池的运用

    前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...

  3. Java开发笔记(一百零五)几种定时器线程池

    前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...

  4. Java开发笔记(一百零一)通过加解锁避免资源冲突

    前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...

  5. Java开发笔记(九十七)利用Runnable启动线程

    前面介绍了线程的基本用法,按理说足够一般的场合使用了,只是每次开辟新线程,都得单独定义专门的线程类,着实开销不小.注意到新线程内部真正需要开发者重写的仅有run方法,其实就是一段代码块,分线程启动之后 ...

  6. Java开发笔记(一百零二)信号量的请求与释放

    前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:1.某块代码被加锁之后,对其它线程而言就处于繁忙状态,缺乏弹性的阈值范围:2 ...

  7. Java开发笔记(一百零六)Fork+Join框架实现分而治之

    前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...

  8. Java开发笔记(九十八)利用Callable启动线程

    前面介绍了如何利用Runnable接口构建线程任务,该方式确实方便了线程代码的复用与共享,然而Runnable不像公共方法那样有返回值,也就无法将线程代码的处理结果传给外部,造成外部既不知晓该线程是否 ...

  9. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

随机推荐

  1. TensorFlow 安装 Win10 Python+GPU

    前叙:有灵魂的程序都是每一个程序员的最终目标.TensorFlow了解下? 打算花几个月学机器学习,TensorFlow是很好的选择,折腾了会环境,略有心得分享下. 环境:win10 Python:3 ...

  2. 洛谷 P2604 [ZJOI2010]网络扩容

    题目描述 给定一张有向图,每条边都有一个容量C和一个扩容费用W.这里扩容费用是指将容量扩大1所需的费用.求: 1. 在不扩容的情况下,1到N的最大流: 2. 将1到N的最大流增加K所需的最小扩容费用. ...

  3. (四)docker创建私人仓库

    (一) 简介 仓库(Repository)是集中存放镜像的地方.仓库可以 被认为是一个具体的项目或目录.例如对于仓库地址 docker.sina.com.cn/centos:centos63 来说,d ...

  4. hasOneOf # if (data.otherDescArr.some(_ => '7'.indexOf(_) > -1)) {

    if (data.otherDescArr.some(_ => '7'.indexOf(_) > -1)) { export const hasOneOf = (targetarr, ar ...

  5. Zend studio 修改编码格式

    一.临时修改编码格式 edit -> Set Encoding... -> Other(选择) 二.修改软件默认编码格式

  6. protobuf的Compiler卸载

    一.首先,只用remove命令是不起作用的. 二.找寻到进行make的文件夹目录,然后执行make uninstall命令. 三.通过which protoc 命令,找到protoc所在位置,rm p ...

  7. CPP-基础:inline

    背景: 在C&C++中 一.inline关键字用来定义一个类的内联函数,引入它的主要原因是用它替代C中表达式形式的宏定义. 表达式形式的宏定义一例: #define ExpressionNam ...

  8. OpenCV:应用篇

    手势跟踪识别 车牌检测 人脸识别 去雾 图像阈值分割提取

  9. linux(Ubuntu/Centos) iproute 路由IP地址等命令集合,查看端口链接

    原 linux(Ubuntu/Centos) iproute 路由IP地址等命令集合,查看端口链接 2017年03月20日 16:55:57 风来了- 阅读数:2291 标签: centoslinux ...

  10. 读书笔记之《编程小白的第1本Python入门书》

    本书电子版下载地址:百度网盘 写在前面:你需要这本书的原因 有没有那一个瞬间,让你想要放弃学习编程? 在我决心开始学编程的时候,我为自己制定了一个每天编程1小时的计划,那时候工作很忙,我只能等到晚上9 ...