默认情况下,线程都是独立的,而且异步执行,线程中包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其他线程的状态或行为。但是在多个线程在运行时共享数据的情况下,就需要考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。在某些项目中,经常会出现线程同步的问题,即:多个现场在访问同一资源时,会出现安全问题。

所谓同步,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上是否有这个标记。若有,就必须等待。