多線程是提升效率性能的一個重要技術,企業級開發中運用的很普遍,我在學習了一段時間以後,仍是有不少地方是很懵比的,後續的會繼續學習更新。html
1. 多線程簡單的概述java
進程:正在進行的程序,好比任務管理器上的進程,QQ等等編程
線程:就是進程中負責執行程序的控制單元(或者執行路徑),一個進程中有多個執行路徑稱之爲多線程安全
開啓多線程是爲了同時運行多部分代碼,每一個線程都有本身的運行內容,這個內容稱之爲線程的任務。多線程
多線程的好處:解決了多程序同時運行的問題。多線程的弊端:線程太多回到效率下降。併發
其實應用程序的執行是CPU在作着快速切換完成的,這個切換是隨機的。dom
下面是多線程的幾種狀態ide
jdk 1.5以前的 多線程兩種建立方式就很少說了。函數
1.extend Thread ,覆寫run方法(run 方法就是描述的線程任務),將該類new一個對象就是建立了一個線程對象,用start()方法開啓線程性能
2.實現Runnable接口,覆寫run()方法,方法裏面也是描述線程任務,new一個Thread線程對象,將任務構造的時候傳遞進去,用start()方法開啓線程
注意:實現Runnable接口的好處
1.將線程的任務從線程的子類中分離出來,進行的單獨的封裝,按照面向對象的思想將任務進行了封裝
2.避免了java單繼承的侷限性。
2.0 多線程安全問題
public class ticket implements Runnable{ private int num=500; public void sale(){ while (true){ synchronized(this) { if (num>0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"...sale=="+num--); }else { break; } } } } @Override public void run() { sale(); } }
public static void main(String[] args){ //屢次啓動一個線程是非法的。 ticket t1= new ticket(); Thread thread1= new Thread(t1); Thread thread2= new Thread(t1); Thread thread3= new Thread(t1); Thread thread4= new Thread(t1); thread1.start(); thread2.start(); thread3.start(); thread4.start(); }
這段代碼很簡單,4 個線程操做同一個ticket對象,num是一個全局變量,每一個線程進來休眠10ms後都減1,直到爲0,能夠觀察到裏面加了synchronized關鍵字(同步),若是不加synchronized,將會出現線程不安全的因素。
附:屢次啓動一個線程對象是非法的!
打印的結果是
分析:出現這種緣由就是0線程進來後,此時num爲1,判斷(num>0)以後,線程休眠了,cpu釋放執行權(由於cpu執行那個線程是隨機切換的),1線程進來以後也出現這種狀況num=1的時候,cpu釋放執行權,同理其餘的2個線程也發生這種狀況,致使出現數據不一樣步的狀況,要解決這種就要加鎖
線程安全問題產生的緣由:
1.多個線程操做共享數據
2.操做共享數據的代碼有多條
用鎖去解決線程安全的問題,synchronized(鎖對象){須要被同步的代碼},this表示當前線程任務對象
同步的好處: 解決了線程的安全問題
同步的弊端:相對下降了效率,由於同步外的線程都會判斷同步鎖。
同步的前提:同步必須有多個線程並使用同一個鎖。
同步函數的鎖對象是當前固定的this,同步代碼塊的鎖是任意的對象。
靜態同步函數鎖的對象是該函數所屬的字節碼文件對象,能夠用this.getClass()或者類名.class表示
死鎖的狀況:同步鎖的的嵌套,死鎖可能會出現和諧的狀況。
2.1 單例模式下多線程的安全問題
/** * 懶漢式 * */ public static class Single{ private static Single s= null; private Single(){ } public static Single getInstance(){ /* * 加鎖是爲了解決線程的安全的問題,鎖以前加個判斷 是由於線程進來判斷鎖,比較消耗效率(解決多線程下單例的效率問題) * */ if (s==null){ synchronized (Single.class){ //多線程訪問須要同步,例如a,b兩個線程,a線程執行到s==null時失去了cpu的執行權, // 而後b線程同理執行到s==null失去cpu的執行權,這個就不是單例模式了。 //懶漢式在多線程狀況下,操做s的共享數據有多條代碼,所以存在線程不安全的狀況,須要同步操做s共享數據的多條代碼 if (s==null){ s= new Single(); } } } return s; } }
上述懶漢式在多線程狀況下就不是單例模擬了。
2.3 多線程之間的通訊 wait和notify機制(生成者-消費者模式)
先看一下簡單的例子,2個線程,一個輸入線程負責讀,一個輸出線程負責寫
/** * Created by ZC-16-012 on 2018/2/2. * * 定義多線程的資源共享數據對象 */ public class Resource { private String name; private String sex; private boolean flag=false; public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } //輸入線程set方法 public synchronized void set(String name,String sex){ if (flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { this.name= name; this.sex= sex; flag=true; this.notify(); } } //輸出線程 public synchronized void out(){ if (!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { System.out.println(this.getName()+"===="+this.getSex()); this.setFlag(false); this.notify(); } } }
public class InputTask implements Runnable{ Resource r; public InputTask(Resource r){ this.r = r; } @Override public void run() { int x=0; while (true){ /** * 分析:因爲資源Resource是共享數據,當輸入線程獲取的cpu執行權時,第一次賦值name是weiAnKang,sex是男, * 第二次輸入線程再次賦值name 是'麗麗'時,因爲線程之間是cpu執行權的切換,此時輸出線程獲取cpu的執行權, * 輸出線程的結果就是'麗麗====男',這樣致使多線程操做同一個資源時出現的安全問題 * */ //todo:此時在輸入線程加上同步鎖依然沒有解決,Resource共享數據同步的問題,出現這種問題應該考慮多個線程之間是否是判斷同一個鎖 if (x==0){ r.set("weiAnKang","nan"); }else { r.set("麗麗","女女女"); } x=(x+1)%2; } } }
/** * Created by ZC-16-012 on 2018/2/2. * * 定義輸出線程的任務 */ public class OutputTask implements Runnable{ Resource r; public OutputTask(Resource r){ this.r=r; } @Override public void run() { while (true){ r.out(); } } }
/** * Created by ZC-16-012 on 2018/2/2. * * 線程之間的通訊 * * 等待/喚醒機制 * wait(): 讓線程處於凍結狀態,被wait的線程會被存儲到線程池中 * notify():喚醒線程池中的一個線程(任意的) * notifyAll():喚醒線程池中的全部線程 * 這些方法必須定義在同步中,由於這些方法是用於操做線程的狀態的方法, * 必需要明確到底操做的是那個鎖上的線程 * */ public class ThreadCommunicationDemo { public static void main(String[] args){ Resource r= new Resource(); InputTask input= new InputTask(r); OutputTask output= new OutputTask(r); //建立線程,並將線程的任務傳遞給線程對象 Thread t1= new Thread(input); Thread t2= new Thread(output); //開啓線程 t1.start(); t2.start(); } }
等待/喚醒機制
* wait(): 讓線程處於凍結狀態,被wait的線程會被存儲到線程池中
* notify():喚醒線程池中的一個線程(任意的)
* notifyAll():喚醒線程池中的全部線程
* 這些方法必須定義在同步中,由於這些方法是用於操做線程的狀態的方法,
* 必需要明確到底操做的是那個鎖上的線程
2.4 jdk1.5以後的java.util.concurrent.locks.Lock的應用
假若有多個線程進行讀的操做,多個線程進行寫的操做呢
/** * Created by ZC-16-012 on 2018/2/5. * * 生產者和消費者 * while判斷標記,解決了線程獲取cpu執行權後,是否要執行 * notifyAll 解決了本方線程必定會喚醒對方線程 */ public class ResourceClass { private String name; private int count=1; private boolean flag=false; private Lock lock= new ReentrantLock();//使用jdk1.5版本以後的鎖對象 //經過已有的鎖對象獲取該鎖上的監視器對象,建立兩組監視器,一組監視生產者,一組監視消費者 Condition producerCon= lock.newCondition(); Condition consumerCon= lock.newCondition(); /** * 分析:0,1 是生產者線程,2 3是消費者線程,假設0先獲取cpu的執行權,2線程消費,有一種狀況當0,1線程 * t0(活),notify以後正好是 喚醒t1 線程,t1判斷flag=true,t0,t1一直在while循環中等待,出現了死鎖狀況 * 出現這種狀況就是生成者線程喚醒的是另外一個生成者線程,應該生成者要保證喚醒的至少有消費者線程 * */ /** * 對於synchronized 修飾的同步代碼塊,鎖是隱式的 * 可使用interrupt()方法將線程從凍結狀態強制恢復到運行狀態中,讓線程具備cpu的執行資格 * 該強制動做會發生 InterruptedException 異常 * * */ public void set(String name) { lock.lock(); try { while (flag) { try { // this.wait();// t0(活),notify 正好喚醒t1 //經過condition的await 讓線程凍結 producerCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } this.name = name + count; count++; System.out.println(Thread.currentThread().getName() + "..生產者.." + this.name); flag = true; // notifyAll();//喚醒了全部線程池中等待的線程 //經過condition的signalAll 喚醒消費者的全部線程 consumerCon.signal(); } finally { lock.unlock();//釋放鎖 } } public void out (){ lock.lock(); try { if (!flag) { try { // this.wait(); consumerCon.await(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println(Thread.currentThread().getName() + ".......消費者.." + this.name); flag = false; // notifyAll(); //消費者喚醒全部生成者的線程 producerCon.signal(); } } finally { lock.unlock(); } } }
public class Producer implements Runnable{ ResourceClass r; public Producer(ResourceClass r){ this.r=r; } @Override public void run() { while (true){ r.set("饅頭"); } } }
public class Consumer implements Runnable{ ResourceClass r; public Consumer(ResourceClass r){ this.r = r; } @Override public void run() { while (true){ r.out(); } } }
/** * Created by ZC-16-012 on 2018/2/5. */ public class ProducerAndConsumer { /** * Thread.currentThread().toString() 會打印線程的名稱,優先級,線程組 * 線程的優先級 MAX_PRIORITY 10,MIN_PRIORITY 1, NORM_PRIORITY 5,數字越高,表示優先級越大, * cpu執行該線程的機率也就越大,例如:t0.setPriority(Thread.MAX_PRIORITY); 將t0線程的優先級設置成最大 * * */ public static void main(String[] args){ ResourceClass r= new ResourceClass(); Producer pro= new Producer(r); Consumer con= new Consumer(r); Thread t0 = new Thread(pro); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); Thread t3 = new Thread(con); t0.start(); /** * try { t0.join(); //t0線程要申請加入進來,運行。讓這個臨時線程先執行完 } catch (InterruptedException e) { e.printStackTrace(); } * */ //t0.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); /** * t2.setDaemon(true) 設置成守護線程(後臺線程),前臺線程其餘線程結束,這個線程也結束 * */ t3.start(); } }
以上例子即是多生產者多消費者的例子
3.0 volatile關鍵字
不少時候不會區分volatile和synchronized,先看一下這個例子
public class RunThread extends Thread{ private boolean flag = true; public void setFlag(boolean flag){ this.flag=flag; } @Override public void run() { while (flag){ } System.out.println(Thread.currentThread().getName() + "...線程運行結束"); } /** * 分析:main方法中建立了t0線程,休眠3s後,將t線程中的全局變量設置了false,主線程結束後,卻沒有打印t0線程中的 "...線程運行結束" * 說明t0線程一直while循環中運行,對於t0線程而言,flag的值並無改變 * * 總結:在java中,每個線程都有一個內存區,其中存放着全部線程共享的主內存中的變量值的拷貝,當線程執行時,他在本身的工做 * 內存區中操做這些變量,爲了存取一個共享的變量,一個線程一般先獲取鎖定並去清楚它的內存工做區,把這些變量從全部線程的共享內存區中 * 正確的裝入到他本身所在的工做內存區中,當線程解鎖時保證該工做內存區中變量的值寫回到共享內存中 * * volatile 關鍵字強制線程到主內存中(共享內存中)去讀取變量,而不是去線程工做內存區中讀取,從而實現了多個線程見的變量可見 * */ public static void main(String[] args) throws InterruptedException { RunThread t0= new RunThread(); t0.start(); Thread.sleep(3000); t0.setFlag(false); System.out.println("flag的值已經被設置成了false。。"); Thread.sleep(1000); System.out.println(t0.flag); } }
網上也搜了不少博客,https://www.cnblogs.com/yzwall/p/6661528.html 這遍博客說的很清楚
3.0 Atomic原子類
/** * Created by ZC-16-012 on 2018/2/8. */ public class VolatileNoAtomic extends Thread{ @Override public void run() { addCount(); } /** * volatile關鍵字具備可見性,並不具有原子性 * Atomic原子類 能夠保證結果是原子性,並不能保證方法自己是原子性 * */ // private volatile static int count; private static AtomicInteger count =new AtomicInteger(0); private static void addCount(){ for (int i=0;i<1000;i++){ // count++; count.incrementAndGet(); } System.out.println(count); } public static void main(String[] args){ VolatileNoAtomic[] arr= new VolatileNoAtomic[10]; for (int i=0;i<10;i++){ arr[i]=new VolatileNoAtomic(); } for (int i=0;i<10;i++){ arr[i].start(); } } }
能夠經過這個例子看一下Atomic對象和 volatile關鍵字的一些區別
3.1 網上有一個聯繫題 有4個線程分別獲取C D E F 盤的大小,第5個線程統計總大小
public class ContactThread { public static void main(String[] args){ /** * CountDownLatch 是一個同步倒數計數器,構造時傳入int參數,每調用一次countDown()方法 * 計數器減1 * */ final CountDownLatch countDownLatch= new CountDownLatch(4); ExecutorService executorService= Executors.newFixedThreadPool(6); final DiskMemory diskMemory= new DiskMemory(); for (int i=0;i<4;i++) executorService.execute(new Runnable() { @Override public void run() { int timer = new Random().nextInt(5); try { Thread.sleep(timer * 1000); } catch (InterruptedException e) { e.printStackTrace(); } int diskSize = diskMemory.getSize(); System.out.printf("完成磁盤的統計任務,耗時%d秒,磁盤大小爲%d.\n", timer, diskSize); //統計每一個磁盤的大小 diskMemory.setSize(diskSize); //任務完成後,計數器減一 countDownLatch.countDown(); System.out.println("count num=" + countDownLatch.getCount()); } }); //主線程一直被阻塞,直到全部的線程統計完 try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("所有線程統計完,全部磁盤總大小.\n" + ",totalSize = " + diskMemory.getTotalSize()); executorService.shutdown(); } }
/** * Created by ZC-16-012 on 2018/2/27. * * 定義磁盤對象 */ public class DiskMemory { private int totalSize; public int getSize(){ return (new Random().nextInt(3)+1)*100; } public void setSize(int size){ totalSize+=size; } public int getTotalSize(){ return totalSize; } }
這邊用到了java.util.concurrent.CountDownLatch的一個同步倒數計數器。
固然java併發編程是一個很複雜的技術,遠遠不止於這些,後面學習到會陸續更新,好比java線程池的運用。
3.2synchronized關鍵字總結
上述synchronized關鍵字經過鎖的機制,保證了多線程之間共享變量的安全。那麼synchronized關鍵字何時釋放鎖?
要麼synchronized裏面的代碼執行完,要麼執行過程當中出現異常,synchronized 都將自動釋放鎖。synchronized 具備可重入性和不可中斷性。
可重入性:指的是同一線程的外層函數獲取到鎖後,內層函數能夠直接再次持有該鎖。
不可中斷性:指的是當前線程一旦持有該鎖,其餘線程只能等待,直到當前線程釋放鎖。
能夠看出來,synchronized關鍵字 經過指令monitorenter,monitorexit等指令進行控制鎖,monitorenter=0的時候說明該線程沒有持有鎖,一旦持有鎖,就經過該計數器加1,進入內層函數也是加1,同理monitorexit 進行減1,執行完計算器等於0.
可見性:因爲Java線程內存模型,每一個線程的線程內存 存儲的是主內存(共享內存)變量的一個副本,可見性保證了當前線程內存存儲的變量副本和主內存一致(讀寫)。
synchronized關鍵字進入鎖以前,先把共享變量從主內存中讀取出來,釋放鎖以前再把共享變量從當前線程內存中寫到主內存中。
synchronized 的缺陷:
1.效率低:鎖釋放的狀況少,試圖得到鎖時不能設置超時,不能中斷一個正在試圖得到鎖的過程
2.不夠靈活:加鎖和釋放鎖的時機單一,好比讀寫鎖,讀的操做不會加鎖,寫的時候加鎖
3.沒法知道是否成功獲取到鎖。還應該避免死鎖