JavaSE学习52:细说多线程之Thread类和Runable接口

一线程创建的两种方式比较

      线程创建和启动有两种方式,这里只是列出步骤,不再进行详细解释。

      (1)继承Thread类

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class MyThread extends Thread{  
  2.      public void run(){  
  3.          ...  
  4.      }  
  5. }  
  6.   
  7. MyThread mt=new MyThread();//创建线程  
  8. mt.start();//启动线程  

      (2)实现Runnable接口

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class MyThread implements Runnable{  
  2.      public void run(){  
  3.         ...  
  4.      }  
  5. }  
  6.   
  7. MyThread mt=new MyThread();  
  8. Thread td=new Thread(mt);//创建线程  
  9. td.start();//启动线程  

      (3)两种方式的比较

      1)Runnable方式可以避免Thread方式由于Java单继承特性带来的缺陷。

      2)Runnable方式的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况。


      二模拟应用场景

      模拟一个火车站买票的场景,某车次还剩下5张火车票,有三个窗口去卖这5张火车票,我们使用三个线程模拟三

个窗口同时卖这5张火车票,我们看Thread方式和Runnable方式这两种方式模拟出一个什么样的结果。

      (1)使用Thread方式模拟买票

      TicketsThread.java源文件代码:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class MyThread extends Thread{  
  2.     //一共有五张火车票  
  3.     private int ticketsCount = 5;  
  4.     //窗口,也就是线程的名字  
  5.     private String name;  
  6.   
  7.     //构造方法  
  8.     public MyThread(String name){  
  9.         this.name = name;  
  10.     }  
  11.   
  12.     public void run(){  
  13.         while(ticketsCount > 0){  
  14.             //如果还有票,就卖掉一张  
  15.             ticketsCount--;  
  16.             System.out.println(name+"卖了1张票,剩余票数为:"+ticketsCount);  
  17.         }  
  18.     }  
  19. }  
  20.   
  21. public class TicketsThread{  
  22.     public static void main(String[] args){  
  23.         //创建三个线程,模拟三个窗口买票  
  24.         MyThread mt1 = new MyThread("窗口1");  
  25.         MyThread mt2 = new MyThread("窗口2");  
  26.         MyThread mt3 = new MyThread("窗口3");  
  27.   
  28.         //启动三个线程,也就是窗口开始卖票  
  29.         mt1.start();  
  30.         mt2.start();  
  31.         mt3.start();  
  32.     }  
  33. }  

      运行结果:


      得到的结果并不是我们想要的结果。

      在票的数量加static修饰关键字得到的结果是正确的。这里不再进行演示。

      (2)使用Runnable方式模拟买票

      TicketsRunnable.java源文件代码:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class MyThread1 implements Runnable{  
  2.     //一共有五张火车票  
  3.     private int ticketsCount = 5;  
  4.       
  5.     public void run(){  
  6.         while(ticketsCount > 0){  
  7.             //如果还有票,就卖掉一张  
  8.             ticketsCount--;  
  9.             System.out.println(Thread.currentThread().getName()+"卖了1张票,剩余票数为:"+ticketsCount);  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. public class TicketsRunnable{  
  15.     public static void main(String[] args){  
  16.         MyThread1 mt = new MyThread1();  
  17.         //创建三个线程,模拟三个窗口买票  
  18.         Thread th1 = new Thread(mt,"窗口1");  
  19.         Thread th2 = new Thread(mt,"窗口2");  
  20.         Thread th3 = new Thread(mt,"窗口3");  
  21.           
  22.         //启动三个线程,也就是窗口开始卖票  
  23.         th1.start();  
  24.         th2.start();  
  25.         th3.start();  
  26.     }  
  27. }  

      得到了预期的结果:


       (3)结果分析

       由于线程的执行是随机的,打印的结果也是随机的。

       Thread方式

       三个线程,创建了三个Thread对象,每个线程都有自己的Thread对象,都有自己的ticketsCount变量,它们三个

线程并不是共享ticketsCount变量,也就是每个线程都可以卖出5张火车票,即三个窗口卖出去15张火车票。

       Runnable方式

       三个线程共用一个Runnable对象,也就是三个线程共用一个ticketsCount变量,即三个窗口一共卖了5张火车票。

       前者不是一个线程三个对象,是三个Thread对象,也是三个线程,这三个线程启动后都会执行5次卖票,实现不

了共享“5张票”这个资源,所以输出就会有15张票卖出去,显然不符合实际,用Runnable就可以解决这个问题,创建

的三个线程可以共享"5张票"这个资源。

      ticketsCont变量是实例变量,它的值自然是存在堆中(每个java对象在堆中都会占据一定内存,而实例变量的值就

是存储在这块内存中,类似于结构体,因此每个对象对应一个ticketsCont的值),ticketsCont跟值传递没有关系啊,如

果是Runnable方式的话,传递的也只是MyThread对象引用的副本,不管ticketsCont的事,但是因为ticketsCont的值

在引用和引用副本所指向的堆内存中,所以无论是引用还是引用副本改变了堆内存中ticketsCont的值,都会产生效

果!

       这个根据你的需要来操作,这样说吧,如果有一个比较大的资源要你下载,那么你用Thread方式那么你就只能一

个线程去吧这个资源下载完,如果是runable方式的话你就可以 多new几个子线程来出来,通过共享runable对象里面

的资源来用多个子线程来下载这个资源,这样的话,下载资源的时候 runable方法会使下载的线程多一些几率在cpu里

面,也会让你下载速度变快继承Thread类是多个线程分别完成自己的任务,实现Runnable接口是多个线程共同完成

一个任务。

       三线程的生命周期

       线程的生命周期转换示意图:


       线程的生命周期:

       1)创建:新建一个线程对象,如Thread thd=new Thread()。

       2)就绪:创建了线程对象后,调用了线程的start()方法(注意:此时线程只是进入了线程队列,等待获取CPU服

务,具备了运行的条件,但并不一定已经开始运行了)。

       3)运行:处于就绪状态的线程,一旦获取了CPU资源,便进入到运行状态,开始执行run()方法里面的逻辑。

       4)阻塞:一个正在执行的线程在某些情况下,由于某种原因而暂时让出了CPU资源,暂停了自己的执行,便进入

了阻塞状态,如调用了sleep()方法。

      5)终止:线程的run()方法执行完毕,或者线程调用了stop()方法,线程便进入终止状态。

      这里我们可以用一个经典的线问题就是生产者和消费者问题的实例:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. import java.util.*;  
  2.   
  3. public class ProducerConsumer{  
  4.     public static void main(String[] args){  
  5.         SyncStack ss = new SyncStack();  
  6.         Producer p = new Producer(ss);  
  7.         Consumer c = new Consumer(ss);  
  8.           
  9.         new Thread(p).start();  
  10.         new Thread(c).start();  
  11.     }  
  12. }  
  13.   
  14.   
  15. //生产与消费对象  
  16. class WoTou{  
  17.     int id;   
  18.       
  19.     //构造方法  
  20.     WoTou(int id){  
  21.         this.id = id;  
  22.     }  
  23.       
  24.     //重写toString()方法  
  25.     public String toString(){  
  26.         return "WoTou : " + id;  
  27.     }  
  28. }  
  29.   
  30.   
  31. //容器类  
  32. class SyncStack{  
  33.     int index = 0;  
  34.     WoTou[] arrWT = new WoTou[4];  
  35.       
  36.     public synchronized void push(WoTou wt){  
  37.         while(index == arrWT.length){  
  38.             try{  
  39.                 //这里的wait()方法指的是Object类中的方法  
  40.                 this.wait();  
  41.             }catch(InterruptedException e){  
  42.                 e.printStackTrace();  
  43.             }  
  44.         }  
  45.         //这里的notifyAll()方法指的是唤醒所有线程,而notify()方法唤醒一个线程  
  46.         this.notifyAll();         
  47.         arrWT[index] = wt;  
  48.         index ++;  
  49.     }  
  50.       
  51.     public synchronized WoTou pop(){  
  52.         while(index == 0){  
  53.             try{  
  54.                 //这里的wait()方法指的是Object类中的方法  
  55.                 this.wait();  
  56.             }catch(InterruptedException e){  
  57.                 e.printStackTrace();  
  58.             }  
  59.         }  
  60.         //这里的notifyAll()方法指的是唤醒所有线程,而notify()方法唤醒一个线程  
  61.         this.notifyAll();  
  62.         index--;  
  63.         return arrWT[index];  
  64.     }  
  65. }  
  66.   
  67.   
  68. //生产者  
  69. class Producer implements Runnable{  
  70.     SyncStack ss = null;  
  71.       
  72.     Producer(SyncStack ss){  
  73.         this.ss = ss;  
  74.     }  
  75.       
  76.     public void run(){  
  77.         for(int i=0; i<20; i++){  
  78.             WoTou wt = new WoTou(i);  
  79.             ss.push(wt);  
  80.             System.out.println("生产了:" + wt);  
  81.             try{  
  82.                 Thread.sleep((int)(Math.random() * 200));  
  83.             }catch(InterruptedException e){  
  84.                 e.printStackTrace();  
  85.             }             
  86.         }  
  87.     }  
  88. }  
  89.   
  90.   
  91. //消费者  
  92. class Consumer implements Runnable{  
  93.     SyncStack ss = null;  
  94.       
  95.     Consumer(SyncStack ss){  
  96.         this.ss = ss;  
  97.     }  
  98.       
  99.     public void run(){  
  100.         for(int i=0; i<20; i++){  
  101.             WoTou wt = ss.pop();  
  102.             System.out.println("消费了: " + wt);  
  103.             try{  
  104.                 Thread.sleep((int)(Math.random() * 1000));  
  105.             }catch(InterruptedException e){  
  106.                 e.printStackTrace();  
  107.             }             
  108.         }  
  109.     }  
  110. }  

      运行结果:


      关于一些问题的解析:

      执行线程sleep()方法是依然占着cpu的,操作系统认为该当前线程正在运行,不会让出系统资源。

      执行wait()方法是让线程到等待池等待,让出一系列的系统资源,其他线程可以根据调度占用cpu线程的资源有不

少,但应该包含CPU资源和锁资源这两类。

      sleep(long mills):让出CPU资源,但是不会释放锁资源。

      wait():让出CPU资源和锁资源。

      锁是用来线程同步的,sleep(long mills)虽然让出了CPU,但是不会让出锁,其他线程可以利用CPU时间片了,但

如果其他线程要获取sleep(long mills)拥有的锁才能执行,则会因为无法获取锁而不能执行,继续等待。但是那些没有

和sleep(long mills)竞争锁的线程,一旦得到CPU时间片即可运行了

       四守护线程

       (1)守护线程理论知识

       Java线程分为两类:

       1)用户线程:运行在前台,执行具体的任务。程序的主线程、连接网络的子线程等都是用户线程。

       2)守护线程:运行在后台,为其他前台线程服务。守护线程的特点是一旦所有用户线程都结束运行,守护线程会

随JVM一起结束工作。守护线程的应用:数据库连接池中的检测线程和JVM虚拟机启动后的检测线程等等。最常见的

守护线程:垃圾回收线程

       设置守护线程:

       可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程。

       使用守护线程的注意事项:

       1)setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常。

       2)在守护线程中产生的新线程也是守护线程。

       3)不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。

       (2)守护线程代码示例:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. import java.io.*;  
  2. import java.util.*;  
  3.   
  4. class DaemonThread implements Runnable{  
  5.     public void run(){  
  6.         System.out.println("进入守护线程"+Thread.currentThread().getName());  
  7.         try{  
  8.             writeToFile();  
  9.         }catch(Exception e){  
  10.             e.printStackTrace();  
  11.         }  
  12.         System.out.println("退出守护线程");  
  13.     }  
  14.       
  15.     private void writeToFile() throws Exception{  
  16.         File filename = new File("E:\\Java\\JavaSE\\Thread"+File.separator+"daemon.txt");   
  17.         OutputStream os = new FileOutputStream(filename,true);  
  18.         int count = 0;  
  19.         while(count < 999){  
  20.             os.write(("\r\nword"+count).getBytes());  
  21.             System.out.println("守护线程"+Thread.currentThread().getName()+"向文件中写入了word"+ count++);  
  22.             Thread.sleep(1000);  
  23.         }  
  24.     }  
  25. }  
  26.   
  27. public class DaemonThreadDemo{  
  28.     public static void main(String[] args){  
  29.         System.out.println("进入主线程"+Thread.currentThread().getName());  
  30.           
  31.         DaemonThread daemonThread = new DaemonThread();  
  32.         Thread thread =new Thread(daemonThread);  
  33.         thread.setDaemon(true);  
  34.         thread.start();  
  35.           
  36.         Scanner sc = new Scanner(System.in);  
  37.         sc.next();  
  38.           
  39.         System.out.println("退出主线程"+Thread.currentThread().getName());  
  40.     }  
  41. }  

       运行结果:



       (3)使用jstack生成线程快照

       作用:生成JVM当前时刻线程的快照(threaddump,即当前进程中所有线程的信息)。

       目的:帮助定位程序问题出现的原因,如长时间停顿、CPU占用率高等。

        jstack命令行工具

       你安装JDK安装目录下的bin文件夹下:

       位置:E:\Java\develop\jdk1.8.0_25\bin


       如何使用jstack

       在cmd中输入:jstack


       我们去任务管理器中找到一个进程的PID:


       生成快照:


       五总结

       (1)怎样解决死锁的问题

       1)尽量避免不必要的synchronized关键字。

       2)可以用其他方法替换synchronized关键字,比如标志不可变量。

       3)保证synchronized代码块简练。

       (2)创建线程的建议

       根据两种创建线程方法的比较,得出的结论是使用实现Runnable接口的方法创建的多线程更好一些,另外,就是

需要重点注意,程序中的同一个资源指的是同一个Runnable对象。建议多使用Runnable这种方式创建多线程。

       (3)补充:

      1)程序中的同一资源指的是同一个Runnable对象。

      2)安全的卖票程序中需要加入同步(Synchronized)。我们的代码过程中并没有加入,如果需要完善,就必须加入

同步锁。


from: http://blog.csdn.net/erlian1992/article/details/51707369

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值