多線程學習(3)線程之間的通訊方式

思考題:html

JAVA多線程之當一個線程在執行死循環時會影響另一個線程嗎?java

會,由於當一個沒有實際做用的線程作密集型輪詢的時候,它消耗了CPU性能,佔用了CPU時間,使得其餘線程須要等待更長的時間獲取CPU時間片斷。多線程

線程間的通訊方式

synchronized

synchronized用於多線程設計,有了synchronized關鍵字,多線程程序的運行結果將變得能夠控制。synchronized關鍵字用於保護共享數據。併發

synchronized實現同步的機制:synchronized依靠"鎖"機制進行多線程同步,"鎖"有2種,一種是對象鎖,一種是類鎖異步

  • 1.依靠對象鎖 鎖定

初始化一個對象時,會自動有一個對象鎖。分佈式

synchronized (對象){方法塊}依靠對象鎖工做,多線程訪問synchronized修飾的方法方法或方法塊時,一旦某個進程搶得鎖以後,其餘的進程只有排隊對待。ide

synchronized void method{} 功能上,等效於

void method{

   synchronized(this) {

    ...

   }

}

下面是舉例:函數

public class Thread5 extends Thread {
	private Test test;

	public Thread5(Test test) {
		this.test = test;
	}

	public void run() {
		test.method3();
	}
}


public class Thread6 extends Thread {
	private Test test;

	public Thread6(Test test) {
		this.test = test;
	}

	public void run() {
		test.method4();
	}
}


public class Test {

	public synchronized void method3() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000);
				System.out.println("print method3");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}
	}

	public synchronized void method4() {
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000);
				System.out.println("print method4");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}
	}

    public static void main(String[] args) {
        Test test1 = new Test();
		Thread5 thread5 = new Thread5(test1);
		Thread6 thread6 = new Thread6(test1);
		thread5.start();
		thread6.start();
	}
}




輸出結果:
print method3
print method3
print method3
print method3
print method3
print method4
print method4
print method4
print method4
print method4

看上面的例子,咱們在建立線程時,傳入了同一個Test對象,去執行不一樣的synchronized修飾的方法,運行時多線程會去爭搶同一個對象鎖,結果變成同步執行。若是咱們在建立線程時,傳入不一樣的對象,那它們獲取到的對象鎖就不是同一個,結果就變成併發執行了。高併發

public static void main(String[] args) {
		Test test1 = new Test();
		Test test2 = new Test();
		Thread5 thread5 = new Thread5(test1);
		Thread6 thread6 = new Thread6(test2);
		thread5.start();
		thread6.start();
	}

輸出結果:
print method3
print method4
print method4
print method3
print method3
print method4
print method4
print method3
print method3
print method4

synchronized void method{}整個函數加上synchronized塊,效率並很差。在函數內部,可能咱們須要同步的只是小部分共享數據,其餘數據,能夠自由訪問,這時候咱們能夠用 synchronized(表達式){//語句}更加精確的控制。性能

這裏講的同步是指多個線程經過synchronized關鍵字這種方式來實現線程間的通訊。

  • 2.使用類對象鎖去作線程的共享互斥.
synchronized static void method(){} 此代碼塊等效於

void method{
   
        synchronized(Obl.class)
  
    }
}

 舉例以下:

public class Thread7 {

	public static Thread7 test1 = new Thread7();
	public static Thread7 test2 = new Thread7();

	public synchronized static void method1() throws InterruptedException {
		System.out.println("method1 begin at:" + System.currentTimeMillis());
		Thread.sleep(6000);
		System.out.println("method1 end at:" + System.currentTimeMillis());
	}

	public synchronized static void method2() throws InterruptedException {
		while (true) {
			System.out.println("method2 running");
			Thread.sleep(200);
		}
	}

	public static void main(String[] args) {
		Thread thread1 = new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					// Thread7.method1();
					test1.method1();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				for (int i = 1; i < 4; i++) {
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("thread1 still alive");
				}

			}

		});

		Thread thread2 = new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					// Thread7.method2();
					test2.method2();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}

		});

		thread1.start();
		thread2.start();

	}
}


輸出結果:
method1 begin at:1554173225933
method1 end at:1554173231934
method2 running
thread1 still alive
method2 running
thread1 still alive
method2 running
thread1 still alive
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running
method2 running

從上面的例子,能夠看出,當類的方法是靜態方法,且由synchronized修飾後。在多個線程的run方法中,不論是經過類名.方法名執行(不一樣的線程能夠執行不一樣的方法,這些方法都是由synchronized static修飾的),仍是經過對象.方法名執行(不一樣的線程能夠持有不一樣的對象,但它們都是一個類型的),高併發狀況下,它們也會由於類對象鎖,而按前後順序,同步執行,一個線程持有鎖,其餘線程要等待。

下面咱們延伸一個例子:

public class Thread7 {

	public static Thread7 test1 = new Thread7();
	public static Thread7 test2 = new Thread7();

	public synchronized static void method1() throws InterruptedException {
		System.out.println("method1 begin at:" + System.currentTimeMillis());
		Thread.sleep(3000);
		System.out.println("method1 end at:" + System.currentTimeMillis());
	}

	public synchronized static void method2() throws InterruptedException {
		while (true) {
			System.out.println("method2 running");
			Thread.sleep(200);
		}
	}
	
	public synchronized void method3() throws InterruptedException {
		while (true) {
			System.out.println("method3 running");
			Thread.sleep(200);
		}
	}

	public static void main(String[] args) {
		Thread thread1 = new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					 Thread7.method1();
					 // test1.method1();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				for (int i = 1; i < 4; i++) {
					try {
						Thread.sleep(200);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println("thread1 still alive");
				}

			}

		});

		Thread thread2 = new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					// Thread7.method2();
					test2.method2();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}

		});
		
		Thread thread3 = new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					// Thread7.method2();
					test2.method3();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}

		});

		thread1.start();
		thread2.start();
		thread3.start();

	}
}

輸出結果:
method1 begin at:1554174080333
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method3 running
method1 end at:1554174083333
method2 running
method3 running
method2 running
thread1 still alive
method3 running
thread1 still alive
method2 running
method3 running
thread1 still alive
method2 running
method3 running
method2 running
method3 running
method2 running
method3 running
method2 running
method3 running
method2 running
method3 running
method2 running
method3 running
method2 running
method3 running

上面這個例子,在類中synchronized既修飾靜態方法,又修飾普通方法,咱們建立了4個線程來執行併發,經過輸出結果能夠看出,類對象鎖和對象鎖互不影響。當兩個線程要執行synchronized修飾的靜態方法時,它們爭搶類對象鎖;當兩個線程要執行synchronized修飾的普通方法時,它們爭搶對象鎖。

3.synchronized 修飾線程的run方法

public class TestSychronized {
    static TestSychronized instance = new TestSychronized();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
                public synchronized void run() {
                    
                    for(int i=1; i<4; i++) {
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("Thread1 still alive, " + i);
                    }                    
                }
        });
        new Thread(thread1).start();
        new Thread(thread1).start();
    }
}

輸出結果:
Thread1 still alive, 1
Thread1 still alive, 2
Thread1 still alive, 3
Thread1 still alive, 1
Thread1 still alive, 2
Thread1 still alive, 3


public class Thread8 extends Thread {
	@Override
	public synchronized void run() {
		for (int i = 1; i < 4; i++) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("Thread still alive, " + i);
		}
	}

	public static void main(String[] args) {
		Thread thread1 = new Thread8();
		Thread thread2 = new Thread8();

		thread1.start();
		thread2.start();
	}
}
輸出結果:
Thread still alive, 1
Thread still alive, 1
Thread still alive, 2
Thread still alive, 2
Thread still alive, 3
Thread still alive, 3

爲何經過傳入Runnable對象的線程執行run方法時是同步的,而extends繼承的線程,執行方法時是異步的?

首先看run方法,是一個非靜態方法,就是一個普通方法,那麼synchronized修飾普通方法,線程爭搶的是對象鎖。

而咱們獲取經過extends繼承方式的線程對象時,是new出來的,並非同一個對象。因此執行

thread1.start();

thread2.start();

時不存在對同一個對象鎖的爭奪,就能夠併發執行。

下面咱們來看Thread的run方法源碼:

public class Thread implements Runnable {

private Runnable target;	

public void run() {
		if (this.target != null)
			this.target.run();
	}
}


public abstract interface Runnable {
	public abstract void run();
}

從源碼咱們能夠知道,線程在執行run方法時,經過extends方式繼承線程,會重寫run方法執行咱們本身的方法。若是經過傳入Runnable對象的方式建立線程,線程會執行傳入的Runnable對象的run方法!而上面咱們給兩個線程傳入的是同一個Runnable對象,就說明多個線程,調用的是同一個對象的run方法,而這個方法被synchronized修飾了,它們就須要爭搶對象鎖,因此就變成同步執行了。

②while輪詢的方式

import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 public class MyList {
 5 
 6     private List<String> list = new ArrayList<String>();
 7     public void add() {
 8         list.add("elements");
 9     }
10     public int size() {
11         return list.size();
12     }
13 }
14 
15 import mylist.MyList;
16 
17 public class ThreadA extends Thread {
18 
19     private MyList list;
20 
21     public ThreadA(MyList list) {
22         super();
23         this.list = list;
24     }
25 
26     @Override
27     public void run() {
28         try {
29             for (int i = 0; i < 10; i++) {
30                 list.add();
31                 System.out.println("添加了" + (i + 1) + "個元素");
32                 Thread.sleep(1000);
33             }
34         } catch (InterruptedException e) {
35             e.printStackTrace();
36         }
37     }
38 }
39 
40 import mylist.MyList;
41 
42 public class ThreadB extends Thread {
43 
44     private MyList list;
45 
46     public ThreadB(MyList list) {
47         super();
48         this.list = list;
49     }
50 
51     @Override
52     public void run() {
53         try {
54             while (true) {
55                 if (list.size() == 5) {
56                     System.out.println("==5, 線程b準備退出了");
57                     throw new InterruptedException();
58                 }
59             }
60         } catch (InterruptedException e) {
61             e.printStackTrace();
62         }
63     }
64 }
65 
66 import mylist.MyList;
67 import extthread.ThreadA;
68 import extthread.ThreadB;
69 
70 public class Test {
71 
72     public static void main(String[] args) {
73         MyList service = new MyList();
74 
75         ThreadA a = new ThreadA(service);
76         a.setName("A");
77         a.start();
78 
79         ThreadB b = new ThreadB(service);
80         b.setName("B");
81         b.start();
82     }
83 }

在這種方式下,線程A不斷地改變條件,線程ThreadB不停地經過while語句檢測這個條件(list.size()==5)是否成立 ,從而實現了線程間的通訊。可是這種方式會浪費CPU資源。之因此說它浪費資源,是由於JVM調度器將CPU交給線程B執行時,它沒作啥「有用」的工做,只是在不斷地測試 某個條件是否成立。就相似於現實生活中,某我的一直看着手機屏幕是否有電話來了,而不是: 在幹別的事情,當有電話來時,響鈴通知TA電話來了。

這種方式還存在另一個問題:

輪詢的條件的可見性問題,關於內存可見性問題,可參考:http://www.cnblogs.com/hapjin/p/5492880.html 中的第一點「一,volatile關鍵字的可見性

線程都是先把變量讀取到本地線程棧空間,而後再去再去修改的本地變量。所以,若是線程B每次都在取本地的 條件變量,那麼儘管另一個線程已經改變了輪詢的條件,它也察覺不到,這樣也會形成死循環。

③wait/notify機制

import java.util.ArrayList;
 2 import java.util.List;
 3 
 4 public class MyList {
 5 
 6     private static List<String> list = new ArrayList<String>();
 7 
 8     public static void add() {
 9         list.add("anyString");
10     }
11 
12     public static int size() {
13         return list.size();
14     }
15 }
16 
17 
18 public class ThreadA extends Thread {
19 
20     private Object lock;
21 
22     public ThreadA(Object lock) {
23         super();
24         this.lock = lock;
25     }
26 
27     @Override
28     public void run() {
29         try {
30             synchronized (lock) {
31                 if (MyList.size() != 5) {
32                     System.out.println("wait begin "
33                             + System.currentTimeMillis());
34                     lock.wait();
35                     System.out.println("wait end  "
36                             + System.currentTimeMillis());
37                 }
38             }
39         } catch (InterruptedException e) {
40             e.printStackTrace();
41         }
42     }
43 }
44 
45 
46 public class ThreadB extends Thread {
47     private Object lock;
48 
49     public ThreadB(Object lock) {
50         super();
51         this.lock = lock;
52     }
53 
54     @Override
55     public void run() {
56         try {
57             synchronized (lock) {
58                 for (int i = 0; i < 10; i++) {
59                     MyList.add();
60                     if (MyList.size() == 5) {
61                         lock.notify();
62                         System.out.println("已經發出了通知");
63                     }
64                     System.out.println("添加了" + (i + 1) + "個元素!");
65                     Thread.sleep(1000);
66                 }
67             }
68         } catch (InterruptedException e) {
69             e.printStackTrace();
70         }
71     }
72 }
73 
74 public class Run {
75 
76     public static void main(String[] args) {
77 
78         try {
79             Object lock = new Object();
80 
81             ThreadA a = new ThreadA(lock);
82             a.start();
83 
84             Thread.sleep(50);
85 
86             ThreadB b = new ThreadB(lock);
87             b.start();
88         } catch (InterruptedException e) {
89             e.printStackTrace();
90         }
91     }
92 }

線程A要等待某個條件知足時(list.size()==5),才執行操做。線程B則向list中添加元素,改變list 的size。

A,B之間如何通訊的呢?也就是說,線程A如何知道 list.size() 已經爲5了呢?

這裏用到了Object類的 wait() 和 notify() 方法。

當條件未知足時(list.size() !=5),線程A調用wait() 放棄CPU,並進入阻塞狀態。---不像②while輪詢那樣佔用CPU

當條件知足時,線程B調用 notify()通知 線程A,所謂通知線程A,就是喚醒線程A,並讓它進入可運行狀態。

這種方式的一個好處就是CPU的利用率提升了。

可是也有一些缺點:好比,線程B先執行,一會兒添加了5個元素並調用了notify()發送了通知,而此時線程A還執行;當線程A執行並調用wait()時,那它永遠就不可能被喚醒了。由於,線程B已經發了通知了,之後再也不發通知了。這說明:通知過早,會打亂程序的執行邏輯。

④管道通訊就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通訊

具體就不介紹了。分佈式系統中說的兩種通訊機制:共享內存機制和消息通訊機制。感受前面的①中的synchronized關鍵字和②中的while輪詢 「屬於」 共享內存機制,因爲是輪詢的條件使用了volatile關鍵字修飾時,這就表示它們經過判斷這個「共享的條件變量「是否改變了,來實現進程間的交流。

而管道通訊,更像消息傳遞機制,也就是說:經過管道,將一個線程中的消息發送給另外一個。

相關文章
相關標籤/搜索