线程同步、线程死锁
在上一篇文章中,有一个模拟售卖火车票系统,在卖车票的程序代码中,极有可能碰到一种意外,就是同一张票号被打印两次多次,也可能出现打印出0甚至负数的票号。具体表现为:假设tickets的值为1的时候,线程1刚执行完if(tickets>0)这行代码,正准备执行下面的代码,就在这时,操作系统将CPU切换到了线程2上执行,此时tickets的值仍为1,线程2执行完上面两行代码,tickets的值变为0后,CPU又切回到了线程1上执行,线程1不会再执行if(tickets>0)这行代码,因为先前已经比较过了,并且比较的结果为真,线程1继续执行后面的代码,最终导致tickets的值为0,而这个结果是我们不允许的。
一、线程同步
1.线程同步
为了解决上述多线程操作同一资源出现不同步的问题,我们可以这样做,即当一个线程运行到if(tickets>0)后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的有关代码块。这段代码就好比一座独木桥,任意时刻,只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。
2.synchronized语句
格式:synchronized(object){代码段}
//pbject可以是任意的一个对象
synchronized语句内的代码段,就形成了同步代码块。也就是说,在同一时刻只能有一个线程可以进入同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内运行。object为任意类型的对象,该对象都有一个标志位,该标志位具有0、1两种状态,其开始状态为1,当执行synchronized(object)语句后,object对象的标志位变为0状态,知道执行完整个synchronized语句中的代码块后又回到了1状态。一个线程执行到synchronized(object)语句处时,先检查object对象的标志位(即同步对象的锁标旗),如果为0状态,表明已经有另外的线程的执行状态正在有关的同步代码块中,这个线程将暂时阻塞,让出CPU资源,知道另外的线程执行完有关的同步代码块,将object对象的标志位恢复到1状态这个阻塞就被取消。
一个用于synchronized语句中的对象称为一个监视器,当一个线程获得了synchronized(object)语句中的代码块的执行权,即意味着它锁定了监视器,在一段时间内,只能有一个线程可以锁定监视器。当同步块代码执行完毕或者遇到break语句或抛出异常时,线程也会释放该锁旗标。
synchronized语句同步代码实现:
public class DemoThread { public static void main(String[] args) { ThreadTest runnable = new ThreadTest(); //声明一个Runnable子类对象 new Thread(runnable).start(); //创建四个线程,它们使用的是同一资源 new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); } } //实现一个Runnable子类 public class ThreadTest implements Runnable { int tickets=100; String str = new String(); //定义一个锁旗标 public void run() { while(true) { synchronized(str) { if(tickets>0) { try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" is saling tickets"+tickets--); } } } } }
分析:如果我们将
synchronized同步对象的锁标旗在run()方法中定义时,多线程不能实现同步。那是因为当一个线程启动后将会调用run方法,对每一次调用,程序都产生一个不同的str局部对象,这四个线程使用的同步监视器完全是四个不同的对象,所以彼此之间不能同步。
3.不同代码块相互同步
上面我们提到的同步代码块,是指不仅同一个代码块在多个线程间实现同步。当然,若干个不同的代码块也可以实现相互之间的同步,只要各synchronized(object)语句中的object完全是同一个对象就可以了。
4.同步函数
除了可以对代码块进行同步外,也可以对函数实现同步,即只要在需要同步的函数定义前加上synchronized关键字即可。
(1)DemoThread.java
public class DemoThread { public static void main(String[] args) { ThreadTest runnable = new ThreadTest(); //声明一个Runnable子类对象 new Thread(runnable).start(); //创建四个线程,它们使用的是同一资源 new Thread(runnable).start(); new Thread(runnable).start(); new Thread(runnable).start(); } }
(2)ThreadTest.java
//实现一个Runnable子类
public class ThreadTest implements Runnable { int tickets=100; public void run() { while(true) { sale(); } } public synchronized void sale() { if(tickets>0) { try { Thread.sleep(1000); } catch(Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" is saling tickets"+tickets--); } } }
分析1:在同一类中,使用synchronized关键字定义的若干方法,可以在多个线程之间同步,当有一个线程进入了synchronized修饰的方法(获得监视器),其他线程就不能进入同一个对象的所有使用了synchronized修饰的方法,直到第一个线程执行完它所进入的synchronized修饰的方法未知(离开监视器)。
分析2:我们通过观察程序结果发现,程序的运行速度要比原来没有使用同步处理时慢,那是因为系统要不停的对同步监视器进行,需要更多时间的开销。所以说同步是以牺牲程序的性能为代价的,如果我们能够确定程序没有安全性问题,就没必要使用同步控制。
分析3:在实现代码块与函数之间的同步时,由于同步函数使用的监视器是this对象(类中的非静态方法始终都能访问到的一个对象就是这个对象本身即this),所以同步代码块中应使用this对象来作为同步监视器。
二、线程死锁
死锁是一种少见的、而且难以调试的错误,在两个线程对两个同步对象具有循环依赖时具就会出现死锁。例如一个线程进入对象X的监视器,而另一个对象进入了对象Y的监视器,这时进入X对象监视器的线程如果还试图进入Y对象的监视器就会被阻隔,接着进入Y对象监视器的线程如果试图进入X对象的监视器也会被阻隔,这样两个线程都处于挂起状态。
(1)A.java
public class A { synchronized void foo(B b) { String name = Thread.currentThread().getName(); //获取当前线程的名称 System.out.println(name+"enter A.foo"); try { Thread.sleep(100); } catch(Exception e) { e.printStackTrace(); } System.out.println(name+" is trying to call b.last"); b.last(); } synchronized void last() { System.out.println("inside A.last"); } }
(2)B.java
public class B { synchronized void bar(A a) { String name = Thread.currentThread().getName(); //获取当前线程的名称 System.out.println(name+"enter B.bar"); try { Thread.sleep(100); } catch(Exception e) { e.printStackTrace(); } System.out.println(name+" is trying to call b.last"); a.last(); } synchronized void last() { System.out.println("inside B.last"); } }
(3)BlockRunnable.java
public class BlockRunnable implements Runnable { A a=new A(); //创建一个A类对象 B b=new B(); //创建一个B类对象 BlockRunnable() //构造方法 { Thread.currentThread().setName("MainThread"); //设置主线程名字 new Thread(this).start(); //创建并启动一个子线程 a.foo(b); //主线程中调用a类的foo方法,foo方法试图调用B类的bar方法 System.out.println("back in the Main thread"); } public void run() { Thread.currentThread().setName("ChildThread"); b.bar(a); System.out.println("back in the ChildThread"); } //主 public static void main(String[] args) { new BlockRunnable(); } }