Java对多线程的支持(一) - Thread类、Runnable接口、Daemon线程、线程同步

Java是第一个在语言级提供了对多线程程序设计支持的语言。语言级支持是啥意思呢,就是抛开操作系统,只使用Java内置的语句就能实现多线程。一般来讲,多线程会依赖操作系统,我们要高用与系统相关的API才能创建多个线程,如在Windows中的CreateThread()函数。但JAVA就不用了。JAVA运行时系统实现了一个用于调用线程执行的线程调度器,用来决定在某一时刻哪个线程在CPU上运行


在java技术中,线程通常是抢占式的而不需要时间片分配进程(分配给每个线程相等的CPU时间的进程)。抢占式调度模型就是许多线程处于可以运行状态(等待状态),但实际上只有一个线程在运行。该线程一直运行到它终止进入可运行状态(等待状态),或者另一个具有更高优先级的线程变成可运行状态。在后一种情况下,低优先级的线程被高优先级的线程抢占,高优先级的线程获得运行的机会。线程调度器支持不同优先级线程的抢先方式,但其本身不支持相同优先级线程的时间片轮换。若要想让其支持,则必须将运行时系统运行在支持时间片轮换操作系统上


1、Thread类和Runnable接口

在Java中实现多线程可以有2种方式,一个是让类继承Thread类,另一个是实现Runnable接口。由于Java不允许多继承,所以当我们让自定义的类继承了Thread类后就不能再继承其它类了,这样会降低日后程序的可扩展性。所以一般我们都是采用实现Runnable接口的方法让一个类具有可执行的特性。

Runnable接口只有一个方法run(),该线程启动时,JVM就会调用run()方法以执行该线程。线程类创建后,必须调用start()方法才能启动线程。如:

  1. package cls;  
  2.   
  3. public class ThreadDemo  
  4. {  
  5.     public static void main(String[] args)  
  6.     {  
  7.         //创建线程类对象并实现 Runnable接口  
  8.         Thread th1 = new Thread(new Runnable()  
  9.         {  
  10.             // 实现Runnable中的run()方法  
  11.             public void run()  
  12.             {  
  13.                 while(true)  
  14.                     System.out.println("Thread1 is running !");  
  15.             }  
  16.         });  
  17.         Thread th2 = new Thread(new Runnable()  
  18.         {  
  19.             public void run()  
  20.             {  
  21.                 while(true)  
  22.                     System.out.println("Thread2 is running !");  
  23.             }  
  24.         });  
  25.           
  26.         // 调用start()方法启动线程  
  27.         th1.start();  
  28.         th2.start();  
  29.     }  
  30. }  

2、Daemon线程

一个Daemon线程指的是一个在后台执行服务的线程。在执行上例代码时,我们发现,当主线程(main函数)执行完th2.start()这一行的时候,主线程的代码就已经全部执行完,所以主线程退出了。但我们看到自己创建的th1和th2两个线程仍然在不停地执行,并没有退出。如果我们想让创建的线程在主函数退出后一同退出,那么就应当将我们创建的线程设置为daemon.Daemon线程当在所有的非Daemon线程都结束后自动退出

让我们修改一下上例:

  1. package cls;  
  2.   
  3. public class ThreadDemo  
  4. {  
  5.     public static void main(String[] args)  
  6.     {  
  7.         //创建线程类对象并实现 Runnable接口  
  8.         Thread th1 = new Thread(new Runnable()  
  9.         {  
  10.             // 实现Runnable中的run()方法  
  11.             public void run()  
  12.             {  
  13.                 while(true)  
  14.                     System.out.println("Thread1 is running !");  
  15.             }  
  16.         });  
  17.         Thread th2 = new Thread(new Runnable()  
  18.         {  
  19.             public void run()  
  20.             {  
  21.                 while(true)  
  22.                     System.out.println("Thread2 is running !");  
  23.             }  
  24.         });  
  25.           
  26.         // 设置成daemon线程  
  27.         th1.setDaemon(true);  
  28.         th2.setDaemon(true);  
  29.           
  30.         // 调用start()方法启动线程  
  31.         th1.start();  
  32.         th2.start();  
  33.           
  34.         // 主函数睡眠一小会  
  35.         try  
  36.         {  
  37.             Thread.currentThread().sleep(200);  
  38.         }  
  39.         catch(Exception e)  
  40.         {  
  41.             System.out.println(e.toString());  
  42.         }  
  43.     }  
  44. }  
运行后可以看到,200ms后主线程退出,相应的th1和th2也退出了。


3、线程同步

假设对象A有两个属性a和b,另有一个对象B有属性a和b,并且我们要用对象B来更新对象A。当这种更新操作完成后(对象A的a,b属性值变成了对象B的a,b属性值),则称A与B同步。

如果对象B在更新对象A时(还没更新完),又来了个对象C,对象C抢在B之前更新了A,则称A与B不同步。

讨论线程就不得不考虑线程同步的问题。当多个线程共享同一个数据时,容易造成数据的不同步。而这种不同步引发的错误往往难以修正,常常是程序在执行了几千次、几万次甚至是几年才会出现。因此线程同步就显得尤为重要。

下面写一个模拟火车站卖票的例子,故意使得线程不同步,看看会发生什么问题:

  1. package cls;  
  2.   
  3. public class ThreadSynchronized  
  4. {  
  5.     public static void main(String[] args)  
  6.     {  
  7.         Ticket tk = new Ticket();  
  8.           
  9.         Thread th1 = new Thread(new Sold(tk));  
  10.         Thread th2 = new Thread(new Sold(tk));  
  11.           
  12.         th1.start();  
  13.         th2.start();  
  14.     }  
  15. }  
  16.   
  17. class Ticket  
  18. {  
  19.     int amount = 100// 有100张票  
  20.       
  21.     // 卖出一张票,总票数减1,返回被卖出的票号  
  22.     public int sold()  
  23.     {  
  24.         return amount--;  
  25.     }  
  26.     // 返回当前剩余票数  
  27.     public int getAmount()  
  28.     {  
  29.         return amount;  
  30.     }  
  31. }  
  32.   
  33. // 售票线程  
  34. class Sold implements Runnable  
  35. {  
  36.     private Ticket tk;  
  37.       
  38.     public Sold1(Ticket tk)  
  39.     {  
  40.         this.tk = tk;  
  41.     }  
  42.     public void run()  
  43.     {  
  44.         while(tk.getAmount() > 0// 还有余票  
  45.         {  
  46.             try  
  47.             {  
  48.                 Thread.currentThread().sleep(10); // 睡眠一小会  
  49.             }  
  50.             catch(Exception e)  
  51.             {  
  52.                 System.out.println(e.toString());  
  53.             }  
  54.               
  55.             System.out.println("Thread1 sold " + tk.sold());  
  56.         }  
  57.     }  
  58. }  

执行:

我们发现,Thread1这个线程居然卖出了0这张不存在的票。售票线程在卖票前已经做了

  1. tk.getAmount() > 0  
的判断了,为什么还会卖出0这张票呢?

我们来分析下程序执行的过程:

当还剩下1这张票时,Thread1用getAmout()判断出票数大于0,则进入while循环进行售票。在还没有卖之前遇到了sleep(),于是Thread1会放弃执行机会,进入阻塞状态;这时轮到Thread2执行了,由于Thead1还没有把1卖出,因此Thread2用getAmout()的结果仍然大于0,所以就把1这张票卖出去了。这时候Thread1睡醒了,则继续从sleep()这行开始执行,而这时候票数已经变成了0,那么Thead1自然就会卖出0这张票了。


为了避免此类情况的发生,我们可以使用synchronized关键字来进行线程的同步。

synchronized关键字可以用在方法上,也可以用在语句块中。我们可以形象地把synchronized想象成一把锁。当程序执行到synchronized语句块(以后称为同步块)时,会将这个同步块的代码“上锁”,这时如果我们调用了sleep()方法或者恰好CPU分配的时间片用光了,另一个线程如果也要执行这段代码时,由于先前的线程已经对这个同步块进行了“上锁”,因此这个线程就不能执行同步块中的代码,只能等待,直到它的时间片也用完为止。这时前一个线程又得到了执行的机会,执行完代码块,它会将这个代码块“解锁”,这样别的线程才可以执行这个代码块。也就是说,一个同步块在一个时间段内只能被一个特定的线程执行,在这个线程没有执行完代码块的语句之前,其它线程是没有办法再执行这个块中的代码的。Java是通过请求一个对象的监视器实现这种功能的。一个对象只有一个监视器,当一个线程请求到后,另一个线程在这个线程归还(执行完同步块)监视器之前是无法请求到该对象的监视器的。

现在我们来改写上面的例子:

  1. package cls;  
  2.   
  3. class ThreadSynchronized    
  4. {    
  5.     public static void main(String[] args)    
  6.     {    
  7.         Ticket tk = new Ticket();    
  8.         Sold sd = new Sold(tk);    
  9.             
  10.         Thread th1 = new Thread(sd);    
  11.         Thread th2 = new Thread(sd);    
  12.             
  13.         th1.start();    
  14.         th2.start();    
  15.     }    
  16. }    
  17.     
  18. class Ticket    
  19. {    
  20.     int amount = 100// 有100张票    
  21.         
  22.     // 卖出一张票,总票数减1,返回被卖出的票号    
  23.     public int sold()    
  24.     {    
  25.         return amount--;    
  26.     }    
  27.     // 返回当前剩余票数    
  28.     public int getAmount()    
  29.     {    
  30.         return amount;    
  31.     }    
  32. }    
  33.     
  34. // 售票线程    
  35. class Sold implements Runnable    
  36. {    
  37.     private Ticket tk;    
  38.         
  39.     public Sold(Ticket tk)    
  40.     {    
  41.         this.tk = tk;    
  42.     }    
  43.     public void run()    
  44.     {    
  45.             
  46.         while(true)    
  47.         {    
  48.             // 同步语句块    
  49.             synchronized(this)//请求 this对象的监视器    
  50.             {    
  51.                 if(tk.getAmount() > 0)    
  52.                 {    
  53.                     try    
  54.                     {    
  55.                         Thread.currentThread().sleep(1); // 睡眠一小会    
  56.                     }    
  57.                     catch(Exception e)    
  58.                     {    
  59.                         System.out.println(e.toString());    
  60.                     }    
  61.                         
  62.                     System.out.println(Thread.currentThread().getName() + " sold " + tk.sold());    
  63.                 }    
  64.             }    
  65.         }    
  66.     }    
  67. }   




可以看到,问题就解决了。

原文地址:点击打开链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值