Java开发笔记(一百)线程同步synchronized
多个线程一起办事固然能够加快处理速度,但是也带来一个问题:两个线程同时争抢某个资源时该怎么办?看来资源共享的另一面便是资源冲突,正所谓鱼与熊掌不可兼得,系统岂能让多线程这项技术专占好处?果然是有利必有弊,且看之前演示售票任务时候的多线程操作,具体代码如下所示:
// 多个线程同时操作某个资源,可能会产生冲突
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的更多相关文章
- Java开发笔记(一百零三)线程间的通信方式
前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...
- Java开发笔记(一百零四)普通线程池的运用
前面介绍了线程的基本用法,以及多线程并发的问题处理,但实际开发中往往存在许多性质相似的任务,比如批量发送消息.批量下载文件.批量进行交易等等.这些同类任务的处理流程一致,不存在资源共享问题,相互之间也 ...
- Java开发笔记(一百零五)几种定时器线程池
前面介绍了普通线程池的用法,就大多数任务而言,它们对具体的执行时机并无特殊要求,最多是希望早点跑完早点出结果.不过对于需要定时执行的任务来说,它们要求在特定的时间点运行,并且往往不止运行一次,还要周期 ...
- Java开发笔记(一百零一)通过加解锁避免资源冲突
前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:1.synchronized必 ...
- Java开发笔记(九十七)利用Runnable启动线程
前面介绍了线程的基本用法,按理说足够一般的场合使用了,只是每次开辟新线程,都得单独定义专门的线程类,着实开销不小.注意到新线程内部真正需要开发者重写的仅有run方法,其实就是一段代码块,分线程启动之后 ...
- Java开发笔记(一百零二)信号量的请求与释放
前面介绍了同步与加锁两种并发处理机制,虽然加锁比起同步要灵活一些,但是加锁在某些高级场合依然力有未逮,包括但不限于下列几点:1.某块代码被加锁之后,对其它线程而言就处于繁忙状态,缺乏弹性的阈值范围:2 ...
- Java开发笔记(一百零六)Fork+Join框架实现分而治之
前面依次介绍了普通线程池和定时器线程池的用法,这两种线程池有个共同点,就是线程池的内部线程之间并无什么关联,然而某些情况下的各线程间存在着前因后果关系.譬如人口普查工作,大家都知道我国总人口为14亿左 ...
- Java开发笔记(九十八)利用Callable启动线程
前面介绍了如何利用Runnable接口构建线程任务,该方式确实方便了线程代码的复用与共享,然而Runnable不像公共方法那样有返回值,也就无法将线程代码的处理结果传给外部,造成外部既不知晓该线程是否 ...
- Java开发笔记(九十六)线程的基本用法
每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...
随机推荐
- [译] 用win7自带工具找出svchost.exe的CPU使用率达到100%的元凶
本文是我对自己上一篇转载的博客 <Figuring out why my SVCHOST.EXE is at 100% CPU without complicated tools in Wind ...
- TIOJ1208 第K大连续和
第k大的题一般都有点麻烦 pbds库的tree,需要研究一下https://codeforces.com/blog/entry/11080find_by_order() and order_of_ke ...
- light oj 1336 sigma function
常用的化简方法(高中就常用了): p^(e+1)-1/p-1= [ p^(e+1) -p + (p-1) ]/ (p-1) = p*(p^e-1)/(p-1) + 1 ...
- Opencv竟然有中文资料
最近对于OpenCV看的较多,竟然不知不觉找到了一个中文网站,对于母语真的桥开心的嘻嘻 直方图均衡化: http://www.opencv.org.cn/opencvdoc/2.3.2/html/do ...
- kotlin - Parcelable implementations generator
本文摘自——https://kotlinlang.org/docs/tutorials/android-plugin.html Android Extensions plugin provides P ...
- mysql5.7zip安装
一.下载mysql zip文件 二.解压.(我的目录A:\mysql\mysql-5.7.23-winx64) 三.配置环境变量 Path后面追加%A:\mysql\mysql-5.7.23-wi ...
- python 04 学生信息管理系统
今天任务不多,做了学生信息管理系统1.0,使用字典存储学生个体信息,列表存储学生字典.注意dict定义要在循环体内,若定义成全局变量或循环体外,则旧数据会被新数据覆盖.dict属于可变类型数据,内容改 ...
- Python机器学习2.2
使用Python实现感知器学习算法 在<Python机器学习>中的2.2节中,创建了罗森布拉特感知器的类,通过fit方法初始化权重self.w_,再fit方法循环迭代样本,更新权重,使用p ...
- crontab 和 supervisor
crontab linux系统自带的工具,可以做定时任务,最小间隔是1分钟 配置crontab 命令 如果是以root用户编辑的,那么最后运行也是以root用户运行脚本文件 crontab -e 命令 ...
- tornado框架基础01-路由简介
tornado 小而精 Django 大而全 Web框架 Tornado是一个由Python开发的Web框架 Web服务 利用Tornado,可以快速搭建和一个高性能的Web服务 非阻塞 Tornad ...