多个线程一块儿办事当然可以加快处理速度,可是也带来一个问题:两个线程同时争抢某个资源时该怎么办?看来资源共享的另外一面即是资源冲突,正所谓鱼与熊掌不可兼得,系统岂能让多线程这项技术专占好处?果真是有利必有弊,且看以前演示售票任务时候的多线程操做,具体代码以下所示:html
// 多个线程同时操做某个资源,可能会产生冲突 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); } }
添加完毕再次运行售票代码,观察到了如下的售票日志:ide
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); } }
屡次运行修改后的售票代码,观察到的售票日志终于正常打印余票数量了:this
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开发笔记(序)章节目录》orm