java多線程

進程:就是應用程序在運行時期,所占用的內存空間區域,這個區域就稱為這個應用程序的進程。

線程:Thread,就是進程中執行的一個小程序,對于CPU,線程是一條可以被CPU執行的路徑。

一、多線程的創建方式

線程這個事物也是對象,對象的描述類是java.lang.Thread。之前我們使用的Thread.sleep()方法就是線程類中的靜態方法。線程進程都是由操作系統,依靠JVM找操作系統才實現線程的功能。

方式一:子類實現Thread

  • 定義類繼承Thread類,入伙
  • 重寫Thread類的run方法,為什么重寫,Java工程師,不知道其他程序人員要用線程運行什么代碼,規范就是run
  • 創建Thread子類的對象,創建了一個線程,多了一個CPU的執行路徑
  • 調用Thread類的start()方法,開啟線程。開始運行線程,JVM調用線程的run

方式二:實現Runnable接口

  • 創建一個類實現Runnable接口,重寫里面的run()方法
  • 創建一個線程對象 的時候,傳遞子類實例對象進線程的構造方法
  • 然后,線程對象調用start方法,開啟線程

兩種實現方式的比較:

多實現明顯比單實現好,避免了局限性,但是也因此將線程操作的數據變成了共享數據,存在需要處理的安全隱患。

二、線程名字的設置和獲取

設置和獲取線程的名字就是調用線程內置的兩個方法

public String getName();
public void setName(String name); 
static Thread currentThread();//返回當前方法運行的線程對象

getName方法相關:

如果是進程的實例來調用此方法并沒有什么可以說的,直接調用即可。但是要是在main調用此方法的話,需要先調用Thread中的靜態方法static Thread currentThread()來返回線程對象,然后再調用getName方法才能獲取線程名字。

線程名字設置方法相關:

其實設置線程名字還有另外一種更為簡便的方法,就是在構造線程的時候,傳遞String name給構造方法即可,Thread中有重載此類型的構造方法。但是中點不在這里,重點在于要是子類繼承Thread,要以此種方法實現線程的名字 的設置就需要再重載傳String name參數的構造方法,在方法的首行super(String name)來將參數傳遞給父類。

這里提供一個demo作為演示:

package thread;
/*
 * 進程和線程
 * 進程類
 * 進程的run方法和start
 * 進程名字的設置和獲取
 */

//定義實現一個Thread的子類
class Threadtemp extends Thread{
    
    public void run(){
        for(int i  = 0 ; i < 10 ; i++ ){
            System.out.println("thread:" + i);
        }
    }
}

public class ThreadDemo {
    //main也是一個線程
    public static void main(String[] args) {
        //創建進程實例
        Threadtemp thread  = new Threadtemp();
        thread.start();//調用父類start方法開啟線程
        thread.setName("六娃");
        System.out.println(thread.getName());
        
        for(int i  = 0 ; i < 10 ; i++ ){
            System.out.println("main:" + i);
        }
        System.out.println(Thread.currentThread().getName());
        
    }
}

三、線程共享數據的安全性

使用上述的第二種實現接口方式來創建多線程的方法,存在一個安全隱患。因為多個線程的構造可以傳遞同一個對象,這就使得該對象的數據變成了多個線程共享的數據。但是由于線程的并發性,會導致產生數據同步等安全問題的出現。

舉個例子不安全的例子:

這是一個實現Runnable接口的類:

class Ticket2 implements Runnable {
    private int ticketNum = 100;
    public void run() {

        while (true) {
        
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticketNum--);
                } else {
                    break;
                }
        }
    }
}

此時,在main中運行下面的代碼,就會使得對象中的ticketNum變成多個線程共享的數據。

public static void main(String[] args) {
        Ticket2 t = new Ticket2();

        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);

        t0.start();
        t1.start();
    }

現在,兩個線程同時對同一份數據進行操作,會使得兩次出現數據不同步的情況,甚至出現一些單線程下原本不會出現的數據異常情況。

問題描述到這里就很明白了,那怎么來解決這個問題呢,就這必須提到java中的同步代碼塊技術,代碼如下。

synchronized(Object o){
  ...
}

當一段代碼加上同步代碼塊的時候,多個線程就無法并發的訪問此代碼,每次最多只能有一個線程運行同步代碼塊中的代碼,當有線程運行同步代碼塊時,別的線程的訪問就會被拒絕,直到該進程執行完畢。所以這樣做也就會犧牲部分運行的性能。

下面提供一個demo來演示解決共享數據的安全問題:

package thread;
/*  
 * 線程的兩種創建方式以及數據共享問題
 * 多線程共享數據問題
 * 模擬多個窗口售賣火車票問題
 */

//先來一個線程的類
class Ticket extends Thread {

    private static int ticketNum = 100;

    public void run() {
        while (true) {
            if (ticketNum > 0) {
                System.out.println(getName() + ":" + ticketNum--);
            }
        }
    }
}

// 實現Runnable的接口的類,加鎖保證共享數據的安全性
class Ticket2 implements Runnable {
    private int ticketNum = 100;
    private Object obj = new Object();

    public void run() {

        while (true) {
            synchronized (obj) {
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticketNum--);
                } else {
                    break;
                }
            }
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        // Ticket t0 = new Ticket();
        // Ticket t1 = new Ticket();
        // Ticket t2 = new Ticket();
        // Ticket t3 = new Ticket();
        //
        // t0.start();
        // t1.start();
        // t2.start();
        // t3.start();

        Ticket2 t = new Ticket2();

        Thread t0 = new Thread(t);
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

有同步代碼塊,就有同步方法,其實java源碼中也可以看到很多線程安全的對象使用同步方法來操作共享數據,保證其安全性。下面提供一個demo,演示同步方法的使用,代碼中有對同步方法的詳細解釋和說明,這里就不贅述:

package thread;
/*
java的共享數據問題
此處來模擬銀行的多個窗口同時對同一個賬戶存款的操作
來演示共享數據和同步方法的使用
*/

public class ThreadDemo2 {
    public static void main(String[] args) {
        //在main方法中,創建兩個進程,來模擬兩個銀行窗口存錢
        Customer cust = new Customer();
        
        Thread t1 = new Thread(cust);
        Thread t2 = new Thread(cust);
        
        t1.start();
        t2.start();
    }
}


class Bank {
    private static int money = 0;
  //常規的同步方法,直接使用synchronized關鍵字修飾方法
  //下面add和add2方法對其實現原理做出解釋
    public synchronized void method(){  
    }
  
  //如果同步方法是靜態的,鎖是本類對象 class
    public  static  void add(int sum) {
        synchronized(Bank.class){
        money = money + sum;
        System.out.println(money);
        }
    }
    
    //如果同步方法非靜態,鎖是本身 this
    public void add2(int sum){
        synchronized(this){
            money = money + sum;
            System.out.println(money);
            }
    }
}

class Customer implements Runnable {

    private Bank bank = new Bank();

    public void run() {
        for (int i = 0; i < 3; i++) {
            bank.add2(100);
        }
    }
}

四、線程通信

線程間的通信就是多個線程操作同一共享數據。一個線程負責數據的填充,一個線程負責數據的獲取。之前只是解決了線程數據的同步問題,但是數據塊的安全問題還是沒有解決。

現在,直接給出一個題目:

需要實現交替打印數據,一個線程填充完數據后由另一個線程負責將填充進去的數據打印出來。要實現這個就必須使用同步技術來實現。

直接給出demo:

package thread;

/*
 * 進程通信demo
 * 
 * 兩個Runnable的實現類對共享的數據進行讀寫操作
 * 互相傳遞數據和信息,實現進程簡單的通信
 * 
 * 在資源對象里面設置標志boolean型數據flag,來區分資源對象是否有數據在里面
 * 規定若flag為真,說明資源對象里面沒有數據,input線程就需要填充數據進去
 * 填充完了,再將標志位設置為false,此時就算output線程無法執行,
 * 當input線程去執行此處代碼的時候,會走wait()方法,無限期等待
 * 
 * 此時,標志位為false,也就是說表示有數據,output回走sop語句
 * 執行完了之后,設置標志位為true,表示沒有數據,等待數據填充,并
 * 執行notify()喚醒剛在在資源對象上等待的數據輸入線程
 * 
 * 這里面同時也就涉及到了wait()方法的一些特點
 * 
 * 分析:代碼里面已經表示的很清楚了,wait()方法是寫在同步代碼塊內的
 * 也就是說,一個線程拿到了鎖在同步代碼塊內執行了wait方法后,另一個拿同一把鎖的線程
 * 也進入到了自己的同步代碼塊,那就說明該線程也拿到了這把鎖。
 * 所以,wait()方法的特性就明白了,調用該方法會釋放會釋放鎖。
 * 與之相似的sleep方法不會釋放鎖,這就是為什么調用sleep方法后大家都
 * 得等的原因所在
 * 
 * 其實,這就是一個簡單的生產消費設計模式
 * 只不過這里是單生產和單消費
 */
public class ThreadCommunicationDemo {
    public static void main(String[] args) {
        Resource r = new Resource();

        new Thread(new Input(r)).start();
        new Thread(new Output(r)).start();
    }
}

class Resource {
    String name;
    String sex;
    boolean flag = true;
}

//填充數據的線程
class Input implements Runnable {
    
    private Resource r;
    
    Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            synchronized (r) {
                if(!r.flag)
                    try{r.wait();}catch(Exception e){}
                if (i % 2 == 0) {
                    r.name = "張三";
                    r.sex = "男";
                } else {
                    r.name = "李四";
                    r.sex = "女";
                }
                i++;
                r.flag = false;
                r.notify();
            }
        }
    }
}

//輸出數據的線程
class Output implements Runnable {

    private Resource r;

    Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            synchronized(r){
                if(r.flag)
                    try{r.wait();}catch(Exception e){}
            System.out.println(r.name + "..." + r.sex);
            r.flag = true;
            r.notify();
            }
        }
    }
}

1、wait()和sleep()方法的區別

  • 首先從外在的特點上來說,wait方法是Object中的非靜態方法,而sleep是Thread類中的靜態方法
  • wait方法必須有鎖的支持,而sleep任意程序都可以調用執行,不需要鎖
  • sleep方法不會釋放鎖,wait方法會使得線程釋放同步鎖,當線程被喚醒的時候,必須重新獲取同步鎖,才能夠繼續執行。

2、為什么線程的等待方法寫在Object中

wait和notify這兩個方法操作本鎖上的線程,必須有鎖 的支持,而鎖是任意對象。所以講wait和notify方法寫在Object類中,保證了任意對象都可以調用線程的等待喚醒方法。

3、生產和消費模式

上面的里中其實就是生產和消費模式,只不過是單生產和單消費,也就是都只有一個線程來運行。實際應用中的生產和消費模式都是多生產和多消費(有多個負責生產的線程和多個負責消費的線程),需要注意的是就算是多生產和多消費,也只能生產一個消費一個。

現在,將線程通信中的demo進行改造,使之支持多生產和多消費。有以下進行改造的點:

  • 將同步代碼塊改為同步方法,這樣可以代碼的可讀性,簡化邏輯
  • 將if改為while,使得在此等待的線程被喚醒時仍然需要進行鎖的邏輯判斷,不能直接執行生產和消費的代碼
package thread;

/*
 * 線程通信
 * 將ThreadCommunicationDemo中的代碼改造為同步方法
 * ,添加邏輯處理。到此,就引入了生產者和消費者的設計模式
 * 
 * 使其支持多線程生產,多線程消費
 * 
 * 缺點:每次都喚醒全部線程,浪費資源,影響運行速度
 */
public class ThreadCommunicationDemo2 {
    public static void main(String[] args) {
        Resource r = new Resource();
        //建立三個生產者,三個消費者
        new Thread(new Input(r)).start();
        new Thread(new Input(r)).start();
        new Thread(new Input(r)).start();
        new Thread(new Output(r)).start();
        new Thread(new Output(r)).start();
        new Thread(new Output(r)).start();
    }
}

class Resource {
    private String name;
    private String sex;
    private boolean flag = true;

    public synchronized void setNameSex(String name, String sex) {
        while (!this.flag)
            try {this.wait();}catch (Exception e){}
        this.name = name;
        this.sex = sex;
        System.out.println("制造:" + name + "." + sex);
        flag = false;
        this.notifyAll();
    }
    
    public synchronized void getNameSex() {
        while (this.flag){
            try {
                this.wait();
            } catch (Exception e) {}
        }
        System.out.println(name + "." + sex);
        this.flag = true;
        this.notifyAll();
    }
}

// 實現兩個用于讀取Resource的進程類
class Input implements Runnable {

    private Resource r = null;

    Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            if (i % 2 == 0) {
                r.setNameSex("張三", "男");
            } else {
                r.setNameSex("李四", "女");
            }
            i++;
        }
    }
}

class Output implements Runnable {

    private Resource r = null;

    public Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.getNameSex();
        }
    }
}

這兩個地方改造完成,基本上就實現了多生產和多消費,并且是生產一個消費一個。不過這樣寫并不是最好的實現方式,因為每次都notifyAll會不分青紅皂白的喚醒所有等待的線程,造成了大量的系統的資源浪費。我們希望實現的方式是生產者執行完生產代碼后只喚醒消費者中的一個,同理消費代碼執行完成后也是只喚醒一個生產者線程這樣。但是目前,這個代碼還沒有辦法做到這樣。

額外提供一個邏輯看起來比較清晰的demo:

package thread;

/*
 * 模式:
 * 多生產,多消費
 * 
 * 目標:
 * 生產一個,就必須消費一個
 */
public class ThreadCommunicationDemo3 {
    public static void main(String[] args) {
        Product p = new Product();
         
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Produce(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
        new Thread(new Consumer(p)).start();
    }
}

//產品類
class Product {
    private String name;
    private int count;
    private boolean flag = true;
    
    //生產方法
    public synchronized void setName(String name){
        while(!this.flag)
            try{this.wait();}catch(Exception e){}
        this.name = name + count++;
        System.out.println(Thread.currentThread().getName() +" 生產:" + this.name );
        flag = false;
        this.notifyAll();
    }
    
    //消費方法
    public synchronized void getName(){
        while(this.flag)
            try{this.wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName() +" 消費了產品:" + name);
        flag = true;
        this.notifyAll();
    }
}

//生產者類
class Produce implements Runnable{
    private Product p = null;
    Produce(Product p){
        this.p = p;
    }
    
    public void run(){
        while(true){
            p.setName("Apple II ");
        }
    }
}

//消費者
class Consumer implements Runnable{
    private Product p = null;
    
    Consumer(Product p){
        this.p = p;
    }
     public void run(){
         while(true){
             p.getName();
         }
     }
}

4、使用Lock和Condition替換synchronized

從JDK1.5開始,java提供了新的線程管理方式,就是接口Lock和類Condition。

  • 首先是用Lock代替synchronized實現同步代碼塊的加鎖和上鎖,Lock的實現類是ReentrantLock

    之前的同步代碼塊是這樣寫的:

    synchronized(Object o){
      ...
    }
    

    與之對應的Lock是這么實現的:

    lock();
      ...//中間部分就是以前同步代碼塊中的代碼
    unLock(); 
    
  • Conditon接口替換了原來的線程監視器方法

    原來線程的等待和喚醒是這么寫的:

    //假設o是鎖對象
    o.wait();
    o.notify();
    o.notifyAll();
    

    那現在的使用Conditon接口的實現類是這么寫的:

    //假設con是Conditon接口的實現類對象、
    con.await();//替換原來的wait
    con.signal();//替換原來的notify
    con.signalAll();//替換原來的notifyAll
    

    那Condition的實現類其實不用自己實現,也不用知道是誰,直接使用Lock接口方法中的newCondition()獲取,然后多態調用即可。

    demo:

    package lock;
    
    import java.util.concurrent.locks.*;
    /*
     * Lock和Condition替換synchronized方式
     * 直接來改造之前的多生產和多消費的代碼
     */
    public class LockDemo {
      public static void main(String[] args) {
          Product p = new Product();
           
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Produce(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          new Thread(new Consumer(p)).start();
          
      }
    }
    
    //產品類
    class Product {
      private String name;
      private int count;
      private boolean flag = true;
      
      private Lock lock = new ReentrantLock(); 
      
      //創建兩個鎖的管理器,一個管生產,一個管消費
      private Condition pro = lock.newCondition();
      private Condition con = lock.newCondition();
      
      //生產方法
      public void setName(String name){
          //上鎖
          lock.lock();
          
          while(!this.flag)
              //注意管理器的調用:等待是等待自己方的線程,喚醒是喚醒對方線程
              try{pro.await();}catch(Exception e){}
          this.name = name + count++;
          System.out.println(Thread.currentThread().getName() +" 生產:" + this.name );
          flag = false;
          
          //一次喚醒對方等待線程的一個即可,避免的資源的浪費
          con.signal();
          //解鎖
          lock.unlock();
      }
      
      //消費方法
      public void getName(){  
          //上鎖
          lock.lock();
    
          while(this.flag)
              try{con.await();}catch(Exception e){}
          System.out.println(Thread.currentThread().getName() +" 消費了產品:" + name);
          flag = true;
          
          //一次喚醒對方等待線程的一個即可,避免的資源的浪費
          pro.signal();
          //解鎖
          lock.unlock();
          
      }
    }
    
    //生產者類
    class Produce implements Runnable{
      
      private Product p = null;
      
      Produce(Product p){
          this.p = p;
      }
      
      public void run(){
          while(true){
              p.setName("Apple II ");
          }
      }
    }
    
    //消費者
    class Consumer implements Runnable{
    private Product p = null;
      
      Consumer(Product p){
          this.p = p;
      }
       public void run(){
           while(true){
               p.getName();
           }
       }
    }
    

五、線程停止和線程方法

線程停止有兩種方法:

  • 停止線程的第一種方式,設置標志位,傳遞false給死循環的true
  • 第二種是調用中斷方法interrupt() ,若是此時線程處于wait狀態,報出異常后傳遞false停止

線程的方法:

  • 線程的toString()方法,會打印三個信息【線程名,線程優先級,線程組】
  • 改變線程的優先級setPriority(int i),范圍是1-10
  • setDeamon()設置線程為守護線程,必須寫在start方法之前

守護線程就是當主線程結束后,其守護線程也都會跟隨結束。如果進程內的線程全部變為守護線程,那所有的線程都會結束退出。

demo:

package thread;
/*
 * 線程的方法:
 * 1.停止線程的第一種方式,設置標志位,傳遞false給死循環的true
 * 2.第二種是調用中斷方法,interrupt(),若是此時出于wait狀態,報出異常后傳遞false
 * 3.線程的toString()方法
 *   會打印三個信息【線程名,線程優先級,線程組】
 * 4.改變線程的優先級setPriority,范圍是1-10
 * 5.setDeamon設置線程為守護線程
 */
public class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        
        Thread t = new Thread(st);
        t.start();
        t.setPriority(1);   
        System.out.println(t);
        
        //循環跑產生延遲,之后結束進程
//      for(int i = 0 ; i < 40000 ; i++){
//          if(i == 39999){
//              st.setFalse(false);
//              
//          }
//      }
        
        //中斷線程結束方法,線程必須處于wait狀態中
        t.interrupt();
        
    }
}

//第一種停止方式,傳遞false給死循環的true
//調用中斷方法,interrupt(),若是此時出于wait狀態,報出異常后抓住結束方法

class StopThread implements Runnable{
    
    private boolean flag = true;
    
    public void run(){
         while(flag){
             synchronized (this) {
                try{this.wait();}catch(Exception e ){
                    System.out.println(e + "中斷異常");
                    flag = false;
                }
             System.out.println("running");
             }
         }
    }
    
    public void setFalse(boolean flag){
        this.flag = flag;
    }
}

六、Timer類

java.util.Timer是java中的定時器。可以讓程序與指定的時間和時間間隔后運行。

構造方法:

Timer的構造方法可以傳遞String類型的線程名,將指定線程變為定時執行的線程,當然不指定,那該方法在哪里執行,就默認是哪個線程。還可以傳遞boolean類型的參數,指定時候講該線程變成守護線程。

類方法:

schedule(TimerTask task, Date firstTime, long period);

TimerTask參數是需要運行的代碼程序,Date開始時間,long類型的period則是間隔時間。

TimerTask是接口,實際傳參的時候必須傳遞實現類對象。現實使用中都是直接寫匿名內部類來進行。

demo:

package thread.timer;

/*
 * timer的練習使用
 */
import java.util.Timer;
import java.util.TimerTask;
import java.util.Date;
import java.io.IOException;
import java.text.SimpleDateFormat;

public class TimerDemo {
    public static void main(String[] args) {
        
        SimpleDateFormat sdf = new SimpleDateFormat("yy年MM月dd日  HH:mm:ss");
        Timer t = new Timer();
        
        t.schedule(new TimerTask(){
            public void run(){
                System.out.println(sdf.format(new Date()));
            }
        }, new Date(), 1000);
    }
}

七、線程中的其它方法

  • join(),等待該線程終止,也就是該線程執行完了,其他線程開始爭搶資源
  • yield(),線程讓出執行的機會,讓其他線程先運行,必須寫在run方法里

demo:

package thread;

/*
 * 線程類的方法
 * 
 * join()
 * 等待該線程終止,也就是該線程執行完了,其他線程開始爭搶資源
 * 
 * yield()  不演示了就
 * 線程讓出執行的機會,讓其他線程先運行,必須寫在run方法里
 */

class JoinThread implements Runnable{
    public void run(){
        for(int i = 1 ; i < 10 ; i ++){
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

public class ThreadJoinDemo {
    public static void main(String[] args) throws Exception{
        JoinThread jt = new JoinThread();
        
        Thread t0 = new Thread(jt);
        Thread t1 = new Thread(jt);
        t0.start();
        t0.join();
        t1.start();
        
        for(int i = 1 ; i < 10 ; i ++){
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容