zoukankan      html  css  js  c++  java
  • 多线程(一)多线程实现、线程状态、线程调度、线程同步、线程数据传递

          主要讲java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述、线程池等等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别:

      进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)

      线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

      线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

      多进程是指操作系统能同时运行多个任务(程序)。

      多线程是指在同一程序中有多个顺序流在执行。

          java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程

    (一)多线程的实现方式

    多线程的实现方式有三种:1、继承Thread类,重写run()方法;2、实现Runnable接口,实现run()方法;3、实现Callable()接口,实现call()方法

    一个类如果实现了Runnable接口或者继承了Thread类,那么它就是一个多线程类,如果是要实现多线程,还需要重写run()方法,所以run() 方法是多线程的入口。

      但是在启动多线程的时候,不是从run()方法开始的,而是从start()开始的 理由是:当执行多线程的时候,每一个线程会抢占资源,而操作系统会为其分配资源,在start()方法中不仅执行了多线程的代码,除此还调用了一个start0()方法,该方法的声明是native,在Java语言中用一种技术叫做JNI,即JavaNativeInterface,该技术特点是使用Java调用本机操作系统提供的函数,但是有一个缺点是不能离开特定的操作系统,如果线程需要执行,必须有操作系统去分配资源,所以此操作主要是JVM根据不同的操作系统来实现的

      如果多线程是通过实现Runnable接口来实现的,那么与通过继承Thread来实现有一个区别,那就是多线程的启动方式——必须是通过start()来启动,但是Runnable接口只有一个方法,并没有start()方法,所以在启动多线程的时候必须调用Thread类的一个构造方法——Thread(Runnable target),该构造方法得到了Runnable接口的一个实现,于是就可以调用Thread类的start()方法了。

    多线程的两种实现方式的区别:

       1.Thread是Runnable接口的子类,实现Runnable接口的方式解决了Java单继承的局限

       2.Runnable接口实现多线程比继承Thread类更加能描述数据共享的概念

           3、线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

      注意:通过实现Runnable接口解决了Java单继承的局限,所以不管其他的区别联系是什么,这一点就决定了多线程最好是通过实现Runnable接口的方式

      1、继承Thread类实现多线程

        继承Thread类的方法尽管被我列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方           法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

    1 public class MyThread extends Thread {  
    2   public void run() {  
    3    System.out.println("MyThread.run()");  
    4   }  
    5 } 

    在合适的地方启动线程:

    1 MyThread myThread1 = new MyThread();  
    2 MyThread myThread2 = new MyThread();  
    3 myThread1.start();  
    4 myThread2.start();  

    2、实现Runnable接口,实现多线程

    如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

    1 public class MyThread extends OtherClass implements Runnable {  
    2   public void run() {  
    3    System.out.println("MyThread.run()");  
    4   }  
    5 }

    为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

    1 MyThread myThread = new MyThread();  
    2 Thread thread = new Thread(myThread);  //调用Thread类的一个构造方法——Thread(Runnable target)
    3 thread.start(); 

    事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

    1 public void run() {  
    2   if (target != null) {  
    3    target.run();  
    4   }  
    5 }  

    3、使用ExecutorService、Callable、Future实现有返回结果的多线程

    ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。想要详细了解Executor框架的可以访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。
    可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

     1 import java.util.concurrent.*;
     2 import java.util.Date;
     3 import java.util.List;
     4 import java.util.ArrayList;
     5 
     6 /**
     7 * 有返回值的线程
     8 */
     9 @SuppressWarnings("unchecked")
    10 public class Test {
    11 public static void main(String[] args) throws ExecutionException,
    12     InterruptedException {
    13    System.out.println("----程序开始运行----");
    14    Date date1 = new Date();
    15 
    16    int taskSize = 5;
    17    // 创建一个线程池
    18    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    19    // 创建多个有返回值的任务
    20    List<Future> list = new ArrayList<Future>();
    21    for (int i = 0; i < taskSize; i++) {
    22     Callable c = new MyCallable(i + " ");
    23     // 执行任务并获取Future对象
    24     Future f = pool.submit(c);
    25     // System.out.println(">>>" + f.get().toString());
    26     list.add(f);
    27    }
    28    // 关闭线程池
    29    pool.shutdown();
    30 
    31    // 获取所有并发任务的运行结果
    32    for (Future f : list) {
    33     // 从Future对象上获取任务的返回值,并输出到控制台
    34     System.out.println(">>>" + f.get().toString());
    35    }
    36 
    37    Date date2 = new Date();
    38    System.out.println("----程序结束运行----,程序运行时间【"
    39      + (date2.getTime() - date1.getTime()) + "毫秒】");
    40 }
    41 }
    42 
    43 class MyCallable implements Callable<Object> {
    44 private String taskNum;
    45 
    46 MyCallable(String taskNum) {
    47    this.taskNum = taskNum;
    48 }
    49 
    50 public Object call() throws Exception {
    51    System.out.println(">>>" + taskNum + "任务启动");
    52    Date dateTmp1 = new Date();
    53    Thread.sleep(1000);
    54    Date dateTmp2 = new Date();
    55    long time = dateTmp2.getTime() - dateTmp1.getTime();
    56    System.out.println(">>>" + taskNum + "任务终止");
    57    return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
    58 }
    59 }

    代码说明:
      上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
      public static ExecutorService newFixedThreadPool(int nThreads)  //创建固定数目线程的线程池。
      public static ExecutorService newCachedThreadPool()    //创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从           缓存中移除那些已有 60 秒钟未被使用的线程。
      public static ExecutorService newSingleThreadExecutor()  //创建一个单线程化的Executor。
      public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)   //创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
      ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

    (二)线程的状态转换

                                                                       

    1、新建状态(New):新创建了一个线程对象。
    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (1)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    (2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    (3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    (三)线程调度

     1、调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

    static int MAX_PRIORITY   //线程可以具有的最高优先级,取值为10。  
    static int MIN_PRIORITY   //线程可以具有的最低优先级,取值为1。  
    static int NORM_PRIORITY  //分配给线程的默认优先级,取值为5。 
    hread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
     每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
    JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用同样的调度方式。
    2、Thread.Sleep(100)线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
    3、Obj.wait()线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
        Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用

     单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:

      建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

     1 /**
     2  * wait用法
     3  * @author DreamSea 
     4  * @time 2015.3.9 
     5  */
     6 package com.multithread.wait;
     7 public class MyThreadPrinter2 implements Runnable {   
     8       
     9     private String name;   
    10     private Object prev;   
    11     private Object self;   
    12   
    13     private MyThreadPrinter2(String name, Object prev, Object self) {   
    14         this.name = name;   
    15         this.prev = prev;   
    16         this.self = self;   
    17     }   
    18   
    19     @Override  
    20     public void run() {   
    21         int count = 10;   
    22         while (count > 0) {   
    23             synchronized (prev) {   
    24                 synchronized (self) {   
    25                     System.out.print(name);   
    26                     count--;  
    27                     
    28                     self.notify();   
    29                 }   
    30                 try {   
    31                     prev.wait();   
    32                 } catch (InterruptedException e) {   
    33                     e.printStackTrace();   
    34                 }   
    35             }   
    36   
    37         }   
    38     }   
    39   
    40     public static void main(String[] args) throws Exception {   
    41         Object a = new Object();   
    42         Object b = new Object();   
    43         Object c = new Object();   
    44         MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   
    45         MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   
    46         MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   
    47            
    48            
    49         new Thread(pa).start();
    50         Thread.sleep(100);  //确保按顺序A、B、C执行
    51         new Thread(pb).start();
    52         Thread.sleep(100);  
    53         new Thread(pc).start();   
    54         Thread.sleep(100);  
    55         }   
    56 }  

    输出结果:

    ABCABCABCABCABCABCABCABCABCABC

      先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

    4、Thread.yield()线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
    5、Threat.join()线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
    6、Obj.notify()线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
     注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向
    不同点: 
    1. Thread类的方法:sleep(),yield()等 
       Object的方法:wait()和notify()等 
    2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 
       sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 
    3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 
    所以sleep()和wait()方法的最大区别是:
        sleep()睡眠时,保持对象锁,仍然占有该锁;
        而wait()睡眠时,释放对象锁。
      但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

    (四)常见的线程名词解释

    主线程:JVM调用程序main()所产生的线程。
    当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
    后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
    前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。

    线程类的一些常用方法: 
      sleep(): 强迫一个线程睡眠N毫秒。 
      isAlive(): 判断一个线程是否存活。 
      join(): 等待线程终止。 
      activeCount(): 程序中活跃的线程数。 
      enumerate(): 枚举程序中的线程。 
           currentThread(): 得到当前线程。 
      isDaemon(): 一个线程是否为守护线程。 
      setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
      setName(): 为线程设置一个名称。 
      wait(): 强迫一个线程等待。 
      notify(): 通知一个线程继续运行。 
      setPriority(): 设置一个线程的优先级。

    (五)线程同步

    1、synchronized关键字的作用域有二种: 
      1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法; 
      2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。 
    2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象; 
    3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法; 

      Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。

    总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。

      在进一步阐述之前,我们需要明确几点:

        A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

        B.每个对象只有一个锁(lock)与之相关联。

        C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    (六)线程数据传递

    1、通过构造方法传递数据 

    在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据: 

     1  
     2 package mythread; 
     3 public class MyThread1 extends Thread 
     4 { 
     5 private String name; 
     6 public MyThread1(String name) 
     7 { 
     8 this.name = name; 
     9 } 
    10 public void run() 
    11 { 
    12 System.out.println("hello " + name); 
    13 } 
    14 public static void main(String[] args) 
    15 { 
    16 Thread thread = new MyThread1("world"); 
    17 thread.start(); 
    18 } 
    19 } 

    2、通过变量和方法传递数据 

    向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量: 

     1  
     2 package mythread; 
     3 public class MyThread2 implements Runnable 
     4 { 
     5 private String name; 
     6 public void setName(String name) 
     7 { 
     8 this.name = name; 
     9 } 
    10 public void run() 
    11 { 
    12 System.out.println("hello " + name); 
    13 } 
    14 public static void main(String[] args) 
    15 { 
    16 MyThread2 myThread = new MyThread2(); 
    17 myThread.setName("world"); 
    18 Thread thread = new Thread(myThread); 
    19 thread.start(); 
    20 } 
    21 }

    3、通过回调函数传递数据 

    上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。 

     1  
     2 package mythread; 
     3 class Data 
     4 { 
     5 public int value = 0; 
     6 } 
     7 class Work 
     8 { 
     9 public void process(Data data, Integer numbers) 
    10 { 
    11 for (int n : numbers) 
    12 { 
    13 data.value += n; 
    14 } 
    15 } 
    16 } 
    17 public class MyThread3 extends Thread 
    18 { 
    19 private Work work; 
    20 public MyThread3(Work work) 
    21 { 
    22 this.work = work; 
    23 } 
    24 public void run() 
    25 { 
    26 java.util.Random random = new java.util.Random(); 
    27 Data data = new Data(); 
    28 int n1 = random.nextInt(1000); 
    29 int n2 = random.nextInt(2000); 
    30 int n3 = random.nextInt(3000); 
    31 work.process(data, n1, n2, n3); // 使用回调函数 
    32 System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" 
    33 + String.valueOf(n3) + "=" + data.value); 
    34 } 
    35 public static void main(String[] args) 
    36 { 
    37 Thread thread = new MyThread3(new Work()); 
    38 thread.start(); 
    39 } 
    40 } 
  • 相关阅读:
    ELK+FileBeat 开源日志分析系统搭建-Centos7.8
    ORACLE转换时间戳方法(1546272000)
    由Swap故障引起的ORA-01034: ORACLE not available ORA-27102: out of memory 问题
    数据库设计规范
    数据库字段备注信息声明语法 CDL (Comment Declaration Language)
    渐进式可扩展数据库模型(Progressive Extensible Database Model, pedm)
    使用 ES6 的 Promise 对象和 Html5 的 Dialog 元素,模拟 JS 的 alert, confirm, prompt 方法的阻断执行功能。
    在sed中引入shell变量的四种方法
    参考文献中的[EB/OL]表示什么含义?
    优秀看图软件 XnViewMP v0.97.1 / XnView v2.49.4 Classic
  • 原文地址:https://www.cnblogs.com/wangleBlogs/p/7363592.html
Copyright © 2011-2022 走看看