多线程并行编程中,线程间同步与互斥是一个很有技巧的也很容易出错的地方。
线程间互斥应对的是这种场景:多个线程操作同一个资源(即某个对象),为保证线程在对资源的状态(即对象的成员变量)进行一些非原子性操作后,状态仍然是正确的。典型的例子是“售票厅售票应用”。售票厅剩余100张票,10个窗口去卖这些票。这10个窗口,就是10条线程,售票厅就是他们共同操作的资源,其中剩余的100张票就是这个资源的一个状态。线程买票的过程就是去递减这个剩余数量的过程。不进行互斥控制的代码如下:
package cn.test; public class TicketOffice { private int ticketNum = 0; public TicketOffice(int ticketNum) { super(); this.ticketNum = ticketNum; } public int getTicketNum() { return ticketNum; } public void setTicketNum(int ticketNum) { this.ticketNum = ticketNum; } /** * 售票厅卖票的方法,这个方法操作了售票厅对象唯一的状态--剩余火车票数量。 * 该方法此处并未进行互斥控制。 */ public void sellOneTicket(){ ticketNum--; // 打印剩余票的数量 if(ticketNum >= 0){ System.out.println("售票成功,剩余票数: " + ticketNum); }else{ System.out.println("售票失败,票已售罄!"); } } public static void main(String[] args) { final TicketOffice ticketOffice = new TicketOffice(100); // 启动10个线程,即10个窗口开始卖票 for(int i=0;i<10;i++){ new Thread(new Runnable(){ @Override public void run() { // 当还有剩余票的时候,就去执行 while(ticketOffice.getTicketNum() > 0){ ticketOffice.sellOneTicket(); } } }).start(); } } }
最后打印的部分结果如下:
售票成功,剩余票数: 93 售票成功,剩余票数: 92 售票成功,剩余票数: 91 售票成功,剩余票数: 95 售票成功,剩余票数: 96 售票成功,剩余票数: 87 售票成功,剩余票数: 86 售票成功,剩余票数: 88 售票成功,剩余票数: 89 售票成功,剩余票数: 83 售票成功,剩余票数: 82 售票成功,剩余票数: 81 售票成功,剩余票数: 90 售票成功,剩余票数: 79 售票成功,剩余票数: 93
可以看到售票厅资源的状态:剩余票的数量,是不正确的。数量忽大忽小,这就是对统一资源进行操作没有控制互斥的结果。
互斥操作的控制,Java提供了关键字synchronized进行的。synchronized可以修饰方法,也可以修饰代码段。其代表的含义就是:进入他修饰的这段代码内的线程必须先去获取一个特定对象的锁定标示,并且虚拟机保证这个标示一次只能被一条线程拥有。通过这两种方式修改上述代码的方法sellOneTicket(),如下:
/** * 已经进行了互斥控制。这里是通过synchronized修饰整个方法实现的。 * 线程想进入这个方法,必须获取当前对象的锁定表示! */ public synchronized void sellOneTicket(){ ticketNum--; // 打印剩余票的数量 if(ticketNum >= 0){ System.out.println("售票成功,剩余票数: " + ticketNum); }else{ System.out.println("售票失败,票已售罄!"); } } /** * 已经进行了互斥控制。这里是通过synchronized修饰代码块实现的。线程要想进入修饰的代码块, * 必须获取lock对象的对象标示。 */ private Object lock = new Object(); public void sellOneTicket2(){ synchronized(lock){ ticketNum--; // 打印剩余票的数量 if(ticketNum >= 0){ System.out.println("售票成功,剩余票数: " + ticketNum); }else{ System.out.println("售票失败,票已售罄!"); } } }
通过互斥控制后的输出为:非常整齐,不会出现任何状态不对的情况。
售票成功,剩余票数: 99 售票成功,剩余票数: 98 售票成功,剩余票数: 97 售票成功,剩余票数: 96 售票成功,剩余票数: 95 售票成功,剩余票数: 94 售票成功,剩余票数: 93 售票成功,剩余票数: 92 售票成功,剩余票数: 91 售票成功,剩余票数: 90 售票成功,剩余票数: 89 售票成功,剩余票数: 88 售票成功,剩余票数: 87 售票成功,剩余票数: 86 售票成功,剩余票数: 85 售票成功,剩余票数: 84 售票成功,剩余票数: 83 售票成功,剩余票数: 82
同步的概念再于线程间通信,比较典型的例子就是“生产者-消费者问题”。
多个生产者和多个消费者就是多条执行线程,他们共同操作一个数据结构中的数据,数据结构中有时是没有数据的,这个时候消费者应该处于等待状态而不是不断的去访问这个数据结构。这里就涉及到线程间通信(当然此处还涉及到互斥,这里暂不考虑这一点),消费者线程一次消费后发现数据结构空了,就应该处于等待状态,生产者生产数据后,就去唤醒消费者线程开始消费。生产者线程某次生产后发现数据结构已经满了,也应该处于等待状态,消费者消费一条数据后,就去唤醒生产者继续生产。
实现这种线程间同步,可以通过Object类提供的wait,notify, notifyAll 3个方法去进行即可。一个简单的生产者和消费者的例子代码为:
package cn.test; public class ProducerConsumer { public static void main(String[] args) { final MessageQueue mq = new MessageQueue(10); // 创建3个生产者 for(int p=0;p<3;p++){ new Thread(new Runnable(){ @Override public void run() { while(true){ mq.put("消息来了!"); // 生产消息后,休息100毫秒 try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "Producer" + p).start(); } // 创建3个消费者 for(int s=0;s<3;s++){ new Thread(new Runnable(){ @Override public void run() { while(true){ mq.get(); // 消费消息后,休息100毫秒 try { Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "Consumer" + s).start(); } } /** * 内部类模拟一个消息队列,生产者和消费者就去操作这个消息队列 */ private static class MessageQueue{ private String[] messages;// 放置消息的数据结构 private int opIndex; // 将要操作的位置索引 public MessageQueue(int size) { if(size <= 0){ throw new IllegalArgumentException("消息队列的长度至少为1!"); } messages = new String[size]; opIndex = 0; } public synchronized void put(String message){ // Java中存在线程假醒的情况,此处用while而不是用if!可以参考Java规范! while(opIndex == messages.length){ // 消息队列已满,生产者需要等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } messages[opIndex] = message; opIndex++; System.out.println("生产者 " + Thread.currentThread().getName() + " 生产了一条消息: " + message); // 生产后,对消费者进行唤醒 notifyAll(); } public synchronized String get(){ // Java中存在线程假醒的情况,此处用while而不是用if!可以参考Java规范! while(opIndex == 0){ // 消息队列无消息,消费者需要等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String message = messages[opIndex-1]; opIndex--; System.out.println("消费者 " + Thread.currentThread().getName() + " 消费了一条消息: " + message); // 消费后,对生产者进行唤醒 notifyAll(); return message; } } }
一次输出为:
消费者 Consumer1 消费了一条消息: 消息来了! 生产者 Producer0 生产了一条消息: 消息来了! 消费者 Consumer0 消费了一条消息: 消息来了! 生产者 Producer2 生产了一条消息: 消息来了! 消费者 Consumer2 消费了一条消息: 消息来了! 生产者 Producer1 生产了一条消息: 消息来了! 消费者 Consumer0 消费了一条消息: 消息来了! 生产者 Producer0 生产了一条消息: 消息来了! 消费者 Consumer1 消费了一条消息: 消息来了! 生产者 Producer2 生产了一条消息: 消息来了! 消费者 Consumer2 消费了一条消息: 消息来了! 生产者 Producer0 生产了一条消息: 消息来了! 消费者 Consumer1 消费了一条消息: 消息来了! 生产者 Producer1 生产了一条消息: 消息来了! 消费者 Consumer0 消费了一条消息: 消息来了! 生产者 Producer2 生产了一条消息: 消息来了! 消费者 Consumer0 消费了一条消息: 消息来了! 生产者 Producer1 生产了一条消息: 消息来了! 生产者 Producer0 生产了一条消息: 消息来了! 消费者 Consumer2 消费了一条消息: 消息来了! 消费者 Consumer1 消费了一条消息: 消息来了! 生产者 Producer1 生产了一条消息: 消息来了!
多线程应用中,同步与互斥用的特别广泛,这两个是必须要理解并掌握的!