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开发笔记(序)章节目录

posted @ 2019-05-20 21:54  pinlantu  阅读(436)  评论(0编辑  收藏  举报