Java基礎——Java重點基礎之多線程(一)

一,多線程的引入

  • 1.什麼是線程
    • 線程是程序執行的一條路徑, 一個進程中可以包含多條線程
    • 多線程併發執行可以提高程序的效率, 可以同時完成多項工作
  • 2.多線程的應用場景
    • 紅蜘蛛同時共享屏幕給多個電腦
    • 迅雷開啓多條線程一起下載
    • QQ同時和多個人一起視頻
    • 服務器同時處理多個客戶端請求

二,多線程並行和併發的區別

  • 並行就是兩個任務同時運行,就是甲任務進行的同時,乙任務也在進行。(需要多核CPU)
  • 併發是指兩個任務都請求運行,而處理器只能按受一個任務,就把這兩個任務安排輪流進行,由於時間間隔較短,使人感覺兩個任務都在運行。
  • 比如我跟兩個網友聊天,左手操作一個電腦跟甲聊,同時右手用另一臺電腦跟乙聊天,這就叫並行。
  • 如果用一臺電腦我先給甲發個消息,然後立刻再給乙發消息,然後再跟甲聊,再跟乙聊。這就叫併發。

三,Java程序運行原理和JVM的啓動是多線程的嗎

  • A:Java程序運行原理

    • Java命令會啓動java虛擬機,啓動JVM,等於啓動了一個應用程序,也就是啓動了一個進程。該進程會自動啓動一個 “主線程” ,然後主線程去調用某個類的 main 方法。
  • B:JVM的啓動是多線程的嗎

    • JVM啓動至少啓動了垃圾回收線程和主線程,所以是多線程的。

四,多線程程序實現的方式1

  • 1.繼承Thread

    • 定義類繼承Thread
    • 重寫run方法
    • 把新線程要做的事寫在run方法中
    • 創建線程對象
    • 開啓新線程, 內部會自動執行run方法
    • public class Demo2_Thread {
      
          /**
           * @param args
           */
          public static void main(String[] args) {
              MyThread mt = new MyThread();                           //4,創建自定義類的對象
              mt.start();                                             //5,開啓線程
      
              for(int i = 0; i < 3000; i++) {
                  System.out.println("bb");
              }
          }
      
      }
      class MyThread extends Thread {                                 //1,定義類繼承Thread
          public void run() {                                         //2,重寫run方法
              for(int i = 0; i < 3000; i++) {                         //3,將要執行的代碼,寫在run方法中
                  System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
              }
          }
      }
      

五,多線程程序實現的方式2

  • 2.實現Runnable

    • 定義類實現Runnable接口
    • 實現run方法
    • 把新線程要做的事寫在run方法中
    • 創建自定義的Runnable的子類對象
    • 創建Thread對象, 傳入Runnable
    • 調用start()開啓新線程, 內部會自動調用Runnable的run()方法

    • public class Demo3_Runnable {
          /**
           * @param args
           */
          public static void main(String[] args) {
              MyRunnable mr = new MyRunnable();                       //4,創建自定義類對象
              //Runnable target = new MyRunnable();
              Thread t = new Thread(mr);                              //5,將其當作參數傳遞給Thread的構造函數
              t.start();                                              //6,開啓線程
      
              for(int i = 0; i < 3000; i++) {
                  System.out.println("bb");
              }
          }
      }
      
      class MyRunnable implements Runnable {                          //1,自定義類實現Runnable接口
          @Override
          public void run() {                                         //2,重寫run方法
              for(int i = 0; i < 3000; i++) {                         //3,將要執行的代碼,寫在run方法中
                  System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
              }
          }
      

六,實現Runnable的原理

  • 查看源碼
    • 1,看Thread類的構造函數,傳遞了Runnable接口的引用
    • 2,通過init()方法找到傳遞的target給成員變量的target賦值
    • 3,查看run方法,發現run方法中有判斷,如果target不爲null就會調用Runnable接口子類對象的run方法

七,兩種方式的區別(掌握)

  • 查看源碼的區別:

    • a.繼承Thread : 由於子類重寫了Thread類的run(), 當調用start()時, 直接找子類的run()方法
    • b.實現Runnable : 構造函數中傳入了Runnable的引用, 成員變量記住了它, start()調用run()方法時內部判斷成員變量Runnable的引用是否爲空, 不爲空編譯時看的是Runnable的run(),運行時執行的是子類的run()方法
  • 繼承Thread

    • 好處是:可以直接使用Thread類中的方法,代碼簡單
    • 弊端是:如果已經有了父類,就不能用這種方法
  • 實現Runnable接口
    • 好處是:即使自己定義的線程類有了父類也沒關係,因爲有了父類也可以實現接口,而且接口是可以多實現的
    • 弊端是:不能直接使用Thread中的方法需要先獲取到線程對象後,才能得到Thread的方法,代碼複雜

八,匿名內部類實現線程的兩種方式

  • 繼承Thread類

new Thread() {                                                  //1,new 類(){}繼承這個類
    public void run() {                                         //2,重寫run方法
        for(int i = 0; i < 3000; i++) {                         //3,將要執行的代碼,寫在run方法中
            System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        }
    }
}.start();

    實現Runnable接口

new Thread(new Runnable(){                                      //1,new 接口(){}實現這個接口
    public void run() {                                         //2,重寫run方法
        for(int i = 0; i < 3000; i++) {                         //3,將要執行的代碼,寫在run方法中
            System.out.println("bb");
        }
    }
}).start(); 

九,獲取名字和設置名字(掌握)

  • 1.獲取名字
    • 通過getName()方法獲取線程對象的名字
  • 2.設置名字

    • 通過構造函數可以傳入String類型的名字
  • new Thread("xxx") {
        public void run() {
            for(int i = 0; i < 1000; i++) {
                System.out.println(this.getName() + "....aaaaaaaaaaaaaaaaaaaaaaa");
            }
        }
    }.start();
    
    new Thread("yyy") {
        public void run() {
            for(int i = 0; i < 1000; i++) {
                System.out.println(this.getName() + "....bb");
            }
        }
    }.start(); 
通過setName(String)方法可以設置線程對象的名字
Thread t1 = new Thread() {
    public void run() {
        for(int i = 0; i < 1000; i++) {
            System.out.println(this.getName() + "....aaaaaaaaaaaaaaaaaaaaaaa");
        }
    }
};

Thread t2 = new Thread() {
    public void run() {
        for(int i = 0; i < 1000; i++) {
            System.out.println(this.getName() + "....bb");
        }
    }
};
t1.setName("芙蓉姐姐");
t2.setName("鳳姐");

t1.start();
t2.start();

十,獲取當前線程的對象

  • Thread.currentThread(), 主線程也可以獲取

new Thread(new Runnable() {
    public void run() {
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "...aaaaaaaaaaaaaaaaaaaaa");
        }
    }
}).start();

new Thread(new Runnable() {
    public void run() {
        for(int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + "...bb");
        }
    }
}).start();
Thread.currentThread().setName("我是主線程");                    //獲取主函數線程的引用,並改名字
System.out.println(Thread.currentThread().getName());       //獲取主函數線程的引用,並獲取名字

十一,休眠線程Thread.sleep(毫秒,納秒), 控制當前線程休眠若干毫秒1秒= 1000毫秒 1秒 = 1000 * 1000 * 1000納秒 1000000000
 new Thread() {
        public void run() {
            for(int i = 0; i < 10; i++) {
                System.out.println(getName() + "...aaaaaaaaaaaaaaaaaaaaaa");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();

    new Thread() {
        public void run() {
            for(int i = 0; i < 10; i++) {
                System.out.println(getName() + "...bb");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }.start();

十二,守護線程
  • setDaemon(), 設置一個線程爲守護線程, 該線程不會單獨執行, 當其他非守護線程都執行結束後, 自動退出

Thread t1 = new Thread() {
    public void run() {
        for(int i = 0; i < 50; i++) {
            System.out.println(getName() + "...aaaaaaaaaaaaaaaaaaaaaa");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};

Thread t2 = new Thread() {
    public void run() {
        for(int i = 0; i < 5; i++) {
            System.out.println(getName() + "...bb");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};

t1.setDaemon(true);                     //將t1設置爲守護線程

t1.start();
t2.start();

十三,加入線程
  • join(), 當前線程暫停, 等待指定的線程執行結束後, 當前線程再繼續
  • join(int), 可以等待指定的毫秒之後繼續

  • final Thread t1 = new Thread() {
        public void run() {
            for(int i = 0; i < 50; i++) {
                System.out.println(getName() + "...aaaaaaaaaaaaaaaaaaaaaa");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    
    Thread t2 = new Thread() {
        public void run() {
            for(int i = 0; i < 50; i++) {
                if(i == 2) {
                    try {
                        //t1.join();                        //插隊,加入
                        t1.join(30);                        //加入,有固定的時間,過了固定時間,繼續交替執行
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                    }
                }
                System.out.println(getName() + "...bb");
    
            }
        }
    };
    
    t1.start();
    t2.start();
    

十四,禮讓線程
  • yield讓出cpu

十五,設置線程的優先級)
  • setPriority()設置線程的優先級
十六,同步代碼塊
  • 1.什麼情況下需要同步
    • 當多線程併發, 有多段代碼同時執行時, 我們希望某一段代碼執行的過程中CPU不要切換到其他線程工作. 這時就需要同步.
    • 如果兩段代碼是同步的, 那麼同一時間只能執行一段, 在一段代碼沒執行結束之前, 不會執行另外一段代碼.
  • 2.同步代碼塊

    • 使用synchronized關鍵字加上一個鎖對象來定義一段代碼, 這就叫同步代碼塊
    • 多個同步代碼塊如果使用相同的鎖對象, 那麼他們就是同步的

  • class Printer {
        Demo d = new Demo();
        public static void print1() {
            synchronized(d){                //鎖對象可以是任意對象,但是被鎖的代碼需要保證是同一把鎖,不能用匿名對象
                System.out.print("黑");
                System.out.print("馬");
                System.out.print("程");
                System.out.print("序");
                System.out.print("員");
                System.out.print("\r\n");
            }
        }
    
        public static void print2() {   
            synchronized(d){    
                System.out.print("傳");
                System.out.print("智");
                System.out.print("播");
                System.out.print("客");
                System.out.print("\r\n");
            }
        }
    }
    

十七,同步方法
  • 使用synchronized關鍵字修飾一個方法, 該方法中所有的代碼都是同步的

  • class Printer {
        public static void print1() {
            synchronized(Printer.class){                //鎖對象可以是任意對象,但是被鎖的代碼需要保證是同一把鎖,不能用匿名對象
                System.out.print("黑");
                System.out.print("馬");
                System.out.print("程");
                System.out.print("序");
                System.out.print("員");
                System.out.print("\r\n");
            }
        }
        /*
         * 非靜態同步函數的鎖是:this
         * 靜態的同步函數的鎖是:字節碼對象
         */
        public static synchronized void print2() {  
            System.out.print("傳");
            System.out.print("智");
            System.out.print("播");
            System.out.print("客");
            System.out.print("\r\n");
        }
    }
    

十八,線程安全問題
  • 多線程併發操作同一數據時, 就有可能出現線程安全問題
  • 使用同步技術可以解決這種問題, 把操作數據的代碼進行同步, 不要多個線程一起操作

  •  public class Demo2_Synchronized {
    
            /**
             * @param args
             * 需求:鐵路售票,一共100張,通過四個窗口賣完.
             */
            public static void main(String[] args) {
                TicketsSeller t1 = new TicketsSeller();
                TicketsSeller t2 = new TicketsSeller();
                TicketsSeller t3 = new TicketsSeller();
                TicketsSeller t4 = new TicketsSeller();
    
                t1.setName("窗口1");
                t2.setName("窗口2");
                t3.setName("窗口3");
                t4.setName("窗口4");
                t1.start();
                t2.start();
                t3.start();
                t4.start();
            }
    
        }
    
        class TicketsSeller extends Thread {
            private static int tickets = 100;
            static Object obj = new Object();
            public TicketsSeller() {
                super();
    
            }
            public TicketsSeller(String name) {
                super(name);
            }
            public void run() {
                while(true) {
                    synchronized(obj) {
                        if(tickets <= 0) 
                            break;
                        try {
                            Thread.sleep(10);//線程1睡,線程2睡,線程3睡,線程4睡
                        } catch (InterruptedException e) {
    
                            e.printStackTrace();
                        }
                        System.out.println(getName() + "...這是第" + tickets-- + "號票");
                    }
                }
            }
        }

十九,火車站賣票的例子用實現Runnable接口
二十,死鎖
  • 多線程同步的時候, 如果同步代碼嵌套, 使用相同鎖, 就有可能出現死鎖

    • 儘量不要嵌套使用

private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
    new Thread() {
        public void run() {
            while(true) {
                synchronized(s1) {
                    System.out.println(getName() + "...拿到" + s1 + "等待" + s2);
                    synchronized(s2) {
                        System.out.println(getName() + "...拿到" + s2 + "開吃");
                    }
                }
            }
        }
    }.start();

    new Thread() {
        public void run() {
            while(true) {
                synchronized(s2) {
                    System.out.println(getName() + "...拿到" + s2 + "等待" + s1);
                    synchronized(s1) {
                        System.out.println(getName() + "...拿到" + s1 + "開吃");
                    }
                }
            }
        }
    }.start();
}

二十一,以前的線程安全的類回顧
  • A:回顧以前說過的線程安全問題
    • 看源碼:Vector,StringBuffer,Hashtable,Collections.synchroinzed(xxx)
    • Vector是線程安全的,ArrayList是線程不安全的
    • StringBuffer是線程安全的,StringBuilder是線程不安全的
    • Hashtable是線程安全的,HashMap是線程不安全的

發佈了46 篇原創文章 · 獲贊 11 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章