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开发笔记(九十六)线程的基本用法
每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...
随机推荐
- Debian9镜像安装问题
Debian9下载地址 https://www.debian.org/distrib/ Debian9有三个镜像文件 第一个包含系统2.3两个主要是一些软件的安装包只需下载第一个安装系统即可 默认安装 ...
- (译)IOS block编程指南 1 介绍
Introduction(介绍) Block objects are a C-level syntactic and runtime feature. They are similar to stan ...
- uva1612 Guess
和cf的打分有点像啊 因为一共只有三道题,所以每个人的得分最多有8种可能性.把这8种可能性都算出来,存在数组里,排好序备用排名就是一个天然的链表,给出了扫描的顺序扫描时,维护两个变量:前一个playe ...
- iview tabs里面放入 i-switch 注意slot不是写在 props里面
iview tabs里面放入 i-switch 注意slot不是写在 props里面 <Tabs value="name1"> <TabPane :label=& ...
- iview table的render()函数基本的用法
render:(h,params) => { return h(" 定义的元素 ",{ 元素的性质 }," 元素的内容"/[元素的内容]) }
- Android N requires the IDE to be running with Java 1.8 or later
Android Studio需要两个JDK: ide jdk和project jdk: 前者是IDE本身运行使用的JDK. 后者用于编译Java代码 Project JDK 可以通过file-&g ...
- git命令使用(三)
git的使用--分支的使用 我们都知道拉取代码的时候,拉下来的是默认的分支,但我们需要的是,其他分支的使用操作 开始,拉取项目 git clone url 查看分支,显示默认分支 git branch ...
- 01Ping程序的设计
1.Ping程序设计具体设计任务 1.1 实验目的 PING程序是我们使用的比较多的用于测试网络连通性的程序.PING程序基于ICMP,使用ICMP的回送请求和回送应答来工作.由计算机网络课程知道,I ...
- mysql在线开启或禁用GTID模式
在线开启步骤: 1.要求: (1)必须是5.7.6版本以上的mysql (2)GTID状态为OFF 2.开启步骤: (1):SET GLOBAL ENFORCE_GTID_CONSISTENCY = ...
- NFS和DHCP服务
1. NFS NFS,Network File System的简写,即网络文件系统.网络文件系统是FreeBSD支持的文件系统中的一种,也被称为NFS: NFS允许一个系统在网络上与他人共享目录和文件 ...