默认情况下,线程都是独立的,而且异步执行,线程中包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其他线程的状态或行为。但是在多个线程在运行时共享数据的情况下,就需要考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。在某些项目中,经常会出现线程同步的问题,即:多个现场在访问同一资源时,会出现安全问题。
所谓同步,Synchronize,就是在发出一个功能调用时,在没有得到结果前,该调用就不返回,同事其他线程也不能调用这个方法。通俗地讲,一个线程能否够抢占cpu,必须考虑另一个线程中的某种条件,而不能随便让操作系统按照默认方式分配cpu,如果条件不具备,就应该等待另一个线程运行,直到条件具备。
一个有问题的案例
案例:有若干张飞机票,用2个线程销售,要求没有票的时候能够提示“没有票了”。以最后剩下3张票为例。首先用传统方法编写这段代码
class TicketRunnable implements Runnable
{
private int ticketNum =3;
public void run()
{
while(true)
{
String tName = Thread.currentThread().getName();
if(ticketNum<=0)
{
System.out.println(tName+" 无票");
break;
}
else
{
ticketNum --;
System.out.println(tName+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
public class ThreadSynTest1
{
public static void main(String[] args)
{
TicketRunnable r = new TicketRunnable();
Thread th1 = new Thread(r,"线程1");
Thread th2 = new Thread(r,"线程2");
th1.start();
th2.start();
}
}
class TicketRunnable implements Runnable
{
private int ticketNum =3;
public void run()
{
while(true)
{
String tName = Thread.currentThread().getName();
if(ticketNum<=0)
{
System.out.println(tName+" 无票");
break;
}
else
{
ticketNum --;
System.out.println(tName+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
public class ThreadSynTest1
{
public static void main(String[] args)
{
TicketRunnable r = new TicketRunnable();
Thread th1 = new Thread(r,"线程1");
Thread th2 = new Thread(r,"线程2");
th1.start();
th2.start();
}
}
执行结果:
线程1卖出一张票,还剩2张票
线程1卖出一张票,还剩0张票
线程2卖出一张票,还剩1张票
线程1 无票
线程2 无票
从执行的结果看,显然有问题,线程1第二次执行完后,应该是还剩1张票,线程2执行完,应该还剩0张票。
代码改一下,更能说明问题的严重性:
class TicketRunnable implements Runnable
{
private int ticketNum =3;
public void run()
{
while(true)
{
String tName = Thread.currentThread().getName();
if(ticketNum<=0)
{
System.out.println(tName+" 无票");
break;
}
else
{
try{
Thread.sleep(1000);
}
catch(Exception e)
{}
ticketNum --;
System.out.println(tName+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
public class ThreadSynTest1
{
public static void main(String[] args)
{
TicketRunnable r = new TicketRunnable();
Thread th1 = new Thread(r,"线程1");
Thread th2 = new Thread(r,"线程2");
th1.start();
th2.start();
}
}
class TicketRunnable implements Runnable
{
private int ticketNum =3;
public void run()
{
while(true)
{
String tName = Thread.currentThread().getName();
if(ticketNum<=0)
{
System.out.println(tName+" 无票");
break;
}
else
{
try{
Thread.sleep(1000);
}
catch(Exception e)
{}
ticketNum --;
System.out.println(tName+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
public class ThreadSynTest1
{
public static void main(String[] args)
{
TicketRunnable r = new TicketRunnable();
Thread th1 = new Thread(r,"线程1");
Thread th2 = new Thread(r,"线程2");
th1.start();
th2.start();
}
}
执行的结果如下:
线程1卖出一张票,还剩2张票
线程2卖出一张票,还剩1张票
线程1卖出一张票,还剩0张票
线程1 无票
线程2卖出一张票,还剩-1张票
线程2 无票
最后一张票被卖出了两次。
问题在于:当只剩下一张票时,线程1卖出了最后一张票,此时线程1已经打印出了 无票,正要执行 break,退出循环,但是此时线程2抢占了cpu,线程2继续其上次的运行,继续卖出票,导致最后一张票被卖出了两次。
更为严重的问题是,改问题的出现具有随机性。以上案例是多个线程消费有限资源的情况,该情况下还有很多其他案例,如多个线程,向有限空间写数据时,线程1写完数据,空间满了,但是还没有来得及告诉系统,两外一个线程就抢占了cpu,也来写数据,不知道空间已经满了,造成溢出。
如何解决此问题
就是让一个线程卖票时,其他线程不能抢占cpu,根据定义,实际上相当于要实现线程的同步,通俗的讲,可以给共享资源加一把锁,这只锁只有一把钥匙,哪个线程获得了这把锁,才有权利访问该共享资源。
在java语言中,synchronized关键字可以解决这个问题,整个语法形式表现为:
synchronized(同步锁对象)
{
//访问共享资源,需要同步的代码段
}
注意:synchronized后的"同步锁对象",必须是可以被各个线程共享的,如this、某个全局变量等。不能是一个局部变量。
其原理是:当某一线程运行同步代码段时,在"同步锁对象"上置一标记,运行完这段代码,标记消除。其他线程要想抢占cpu运行这段代码,必须在“同步锁对象”上先检查该标记,只有标记处于消除状态,才能抢占cpu。在上面的例子中,this是一个“同步锁对象”。因此,在上面的案例中,可以将卖票的代码用 synchronized代码包围起来,“同步锁对象”取this。代码如下:
class TicketRunnable implements Runnable
{
private int ticketNum = 3;
public void run()
{
while(true)
{
String name = Thread.currentThread().getName();
synchronized(this)
{
if(ticketNum<=0)
{
System.out.println(name+"无票");
break;
}
else
{
try
{
Thread.sleep(1000);
}catch(Exception e)
{}
ticketNum --;
System.out.println(name+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
}
public class ThreadSynTest3
{
public static void main(String[] args)
{
TicketRunnable tr = new TicketRunnable();
Thread th1 = new Thread(tr,"线程1");
Thread th2 = new Thread(tr,"线程2");
th1.start();
th2.start();
}
}
class TicketRunnable implements Runnable
{
private int ticketNum = 3;
public void run()
{
while(true)
{
String name = Thread.currentThread().getName();
synchronized(this)
{
if(ticketNum<=0)
{
System.out.println(name+"无票");
break;
}
else
{
try
{
Thread.sleep(1000);
}catch(Exception e)
{}
ticketNum --;
System.out.println(name+"卖出一张票,还剩"+ticketNum+"张票");
}
}
}
}
}
public class ThreadSynTest3
{
public static void main(String[] args)
{
TicketRunnable tr = new TicketRunnable();
Thread th1 = new Thread(tr,"线程1");
Thread th2 = new Thread(tr,"线程2");
th1.start();
th2.start();
}
}
可以看出,该方法的本质是将需要独占cpu的代码用synchronized(this)包围起来。如前所述,一个线程加上这段代码之后,就在this加上了一个标记,知道该线程净这段代码运行完毕,才释放这个标记。如果其他线程想要抢占cpu,先要检查this上是否有这个标记。若有,就必须等待。