一、为什么需要同步?
简单地说,之所以需要同步,是为了防止一个线程在执行一段代码块的这段时间内被中断。打个比方,你准备要吃包子,吃包子这个过程包括你把包子拿起来,和吃下去这两个原子动作,当你拿起包子的时候你当然不希望别人从你的手中把包子抢走,所以你希望在你把包子拿起来和吃下去这个过程中不会有别人来抢。 这里你和抢你包子的人好比两个线程,如果要实现吃包子的过程不被中断,就需要线程同步。
从程序角度来说,当有多个线程对同一个数据(即所谓的临界资源)进行写操作的时候,就需要同步,如果不同步则有可能产生不正确的结果。如果多个线程对临界资源仅仅是读,则不需要同步。考虑这样一个场景:铁道部销售火车票时,火车票的数量是固定的,每当有购票请求时,售票系统会启动一个线程执行出票、打印这两个操作,用程序模拟如下:
package com.litl.java.test; public class TicketSale { private static int[] tickets=new int[10]; private int mIndex; private void saleTicket(){ if(mIndex==tickets.length){ //System.out.println(Thread.currentThread().getName()+" 票已经卖完了!!!"); return; } int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息 System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户 tickets[mIndex]=-1;//第三步,修改票信息数据库 mIndex++; //第四步,可用票指针前移 } class BuyThread extends Thread{ public BuyThread(String name) { super(name); } @Override public void run() { saleTicket(); } } private void startSale() { // 100个人去抢10张票 for (int i = 0; i < 100; i++) { BuyThread buyThread = new BuyThread("thread-" + i); buyThread.start(); } } public static void main(String[] args) { TicketSale ts=new TicketSale(); //初始化票务信息 for(int i=0;i<tickets.length;i++){ tickets[i]=i; } ts.startSale(); } }
首先看一个正确的结果:
thread-1 购票成功,出票编号为:0
thread-3 购票成功,出票编号为:1
thread-5 购票成功,出票编号为:2
thread-7 购票成功,出票编号为:3
thread-9 购票成功,出票编号为:4
thread-11 购票成功,出票编号为:5
thread-13 购票成功,出票编号为:6
thread-15 购票成功,出票编号为:7
thread-17 购票成功,出票编号为:8
thread-19 购票成功,出票编号为:9
多运行几次(这也是多线程问题有时比较难以查找的原因,有时候概率甚至是非常低的,运行几十万次才出现一次),则出现在不想看到的结果:
thread-0 购票成功,出票编号为:0
thread-1 购票成功,出票编号为:1
thread-2 购票成功,出票编号为:1
thread-4 购票成功,出票编号为:3
thread-6 购票成功,出票编号为:3
thread-8 购票成功,出票编号为:3
thread-10 购票成功,出票编号为:3
thread-12 购票成功,出票编号为:3
thread-14 购票成功,出票编号为:3
thread-16 购票成功,出票编号为:3
thread-18 购票成功,出票编号为:3
thread-20 购票成功,出票编号为:3
thread-22 购票成功,出票编号为:3
thread-24 购票成功,出票编号为:3
thread-3 购票成功,出票编号为:3
thread-26 购票成功,出票编号为:3
thread-78 购票成功,出票编号为:3
thread-80 购票成功,出票编号为:3
thread-82 购票成功,出票编号为:3
thread-84 购票成功,出票编号为:3
thread-86 购票成功,出票编号为:3
thread-88 购票成功,出票编号为:3
thread-90 购票成功,出票编号为:3
thread-92 购票成功,出票编号为:3
thread-94 购票成功,出票编号为:3
thread-96 购票成功,出票编号为:3
thread-98 购票成功,出票编号为:3
Exception in thread "thread-89" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)
Exception in thread "thread-91" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)
Exception in thread "thread-93" java.lang.ArrayIndexOutOfBoundsException: 10
at com.litl.java.test.TicketSale.saleTicket(TicketSale.java:16)
at com.litl.java.test.TicketSale.access$0(TicketSale.java:9)
at com.litl.java.test.TicketSale$BuyThread.run(TicketSale.java:28)
上面的打印结果中,多个人买到了同一张票,可是一张票只能坐一个人!
为什么会出现上面这种错误的结果呢?从代码上看,原因很简单,所有人买到的票都是通过
int ticket=tickets[mIndex];这条代码实现的,多个线程得到的ticket相同,也就是多个线程在买票时,所读取到的mIndex值是一样的,也就是说,当卖完第一张票之后,系统还没来得及更新mIndex,又处理了第二个买票请求,而这时的mIndex还未改变,于是两个人买到了同一张票!产生这个现象的本质原因,是由于两个线程交替执行,第一个线程还没有完成它应该完成的一系列任务时,系统把cpu时间片交给了另一个线程,而两个线程都对同一个资源进行了操作,于是产生了错误的结果。
那么,如何解决这种资源访问的冲突呢?很简单,只要让这段代码“不可中断”,即当一个线程进入这段代码时,只要还没有执行完,其他想要执行这段代码的线程就乖乖地在那里等着,即可。
int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息 System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户 tickets[mIndex]=-1;//第三步,修改票信息数据库 mIndex++; //第四步,可用票指针前移
二、java中线程同步的实现
java中是通过锁机制来实现线程之间的同步的。何谓锁呢?打个通俗的比方,在没有加锁的情况下,包子是放在食堂的桌子上,而所有想吃包子的人都在食堂里,谁都可以去抢,但是现在加锁之后,每次只能一个人进食堂里去吃包子(这个比喻好像不太恰当~~),因为食堂门被第一个进入食堂的人反锁了,其他的人都乱哄哄地堵在食堂门口,只有等在食堂里吃包子的那个人吃完出来解除反锁之后,才可以进去。
java中有两种实现锁的方法,一是显式的Lock,而是通过synchronized关键字来实现对资源(也就是代码块)加锁。这里我们修改上面的程序,用synchronized对买票方法进行加锁,即可避免上面出现的错误情况:
private synchronized void saleTicket(){ if(mIndex==tickets.length){ //System.out.println(Thread.currentThread().getName()+" 票已经卖完了!!!"); return; } int ticket=tickets[mIndex]; //第一步,从系统中读取可用的票务信息 System.out.println(Thread.currentThread().getName()+" 购票成功,出票编号为:"+ticket); //第二步,打印票给客户 tickets[mIndex]=-1;//第三步,修改票信息数据库 mIndex++; //第四步,可用票指针前移 }分析:
1,通过对saleTicket()方法添加synchronized关键字,当一个线程在执行它的过程中,不会被其他线程中断,从而保证了整个买票过程的完整性,避免两个人买了同一张票的问题。
2,这里的 synchronized所获取的锁是当前类实例对象的锁,即TicketSale当前实例对象的锁。
有关synchronized关键字的详细分析,将在下篇中介绍。