天天看點

Java 知識點總結之Java 并發 API(二)

6、CountDownLatch的工作原理

答:CountDownLatch采用AQS(AbstractQueuedSynchronizer)隊列實作,先初始化Count,再countDown,當計數器值到達0時,表示所有任務都執行完了。

/**
 * 用CountDownLatch實作多個任務并發計算,并彙總結果
 * @author changtan.sun
 *
 */
public class MyComputeTask {
	static ConcurrentLinkedQueue<Long> sums = new ConcurrentLinkedQueue<Long>();

	public MyComputeTask() {
	}

	public long compute(int number, int part) throws Exception {
		number = number + 1;
		int parts = (number + part - 1) / part;
		CountDownLatch latch = new CountDownLatch(parts + 1);

		for (int i = 0; i < parts; i++) {
			long min = i * part;
			long max = (i + 1) * part < number ? (i + 1) * part : number;

			new Thread(new MyTask(min, max, latch)).start();
		}
		latch.countDown();
		latch.await();

		int sum = 0;
		for (Long s : sums) {
			sum += s;
		}
		return sum;
	}

	private static class MyTask implements Runnable {

		private long min;
		private long max;

		private CountDownLatch latch;

		public MyTask(long min, long max, CountDownLatch latch) {
			this.min = min;
			this.max = max;
			this.latch = latch;
		}

		@Override
		public void run() {
			long sum = 0;
			for (long i = min; i < max; i++) {
				sum += i;
			}
			sums.add(sum);
			latch.countDown();
		}

	}
}
           

7、wait和notify

答:(1)wait()方法使得目前線程必須要等待,等到另外一個線程調用notify()或者notifyAll()方法。

         目前的線程必須擁有目前對象的monitor,也即lock,就是鎖。

         線程調用wait()方法,釋放它對鎖的擁有權,然後等待另外的線程來通知它(通知的方式是notify()或者notifyAll()方法),這樣它才能重新獲得鎖的擁有權和恢複執行。

         要確定調用wait()方法的時候擁有鎖,即,wait()方法的調用必須放在synchronized方法或synchronized塊中。

         (2)notify()方法會喚醒一個等待目前對象的鎖的線程。

         notify方法調用必須放在synchronized方法或synchronized塊中。

8、用 wait-notify 寫一段代碼來解決生産者-消費者問題

9、什麼是線程局部變量

線程局部變量是局限于線程内部的變量,屬于線程自身所有,不在多個線程間共享。Java 提供 ThreadLocal 類來支援線程局部變量,是一種實作線程安全的方式。

10、用 Java 寫一個線程安全的單例模式(Singleton)

public class Test4 {
	private static volatile Test4 instance;  //懶漢模式
	private Test4() {
	}
	public static Test4 getInstance() {
		if (instance == null) { //雙重否定加鎖
			synchronized (Test4.class) {
				if (instance == null) {
					instance = new Test4();
				}
			}
		}
		return instance;
	}
}

這裡為什麼要使用volatile修飾instance?主要在于instance = new Singleton()這句,這并非是一個原子操作,事實上在JVM中這句話大概做了下面3件事情:
(1)給instance配置設定記憶體
(2)調用Singleton的構造函數來初始化成員變量
(3)将instance對象指向配置設定的記憶體空間(執行完這步instance就為非null了)。
但是在JVM的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在3執行完畢、2未執行之前,被線程二搶占了,這時instance已經是非null了(但卻沒有初始化),是以線程二會直接傳回instance,然後使用,然後順理成章地報錯。
           

11、Java記憶體逃逸

答:(1)當變量(或者對象)在方法中配置設定後,其指針被傳回或者被全局引用(這樣就會被其他過程或者線程所引用),這種現象稱作指針(或者引用)的逃逸(Escape)。

(2)指針逃逸場景:全局變量指派,方法傳回值,執行個體引用傳遞

class A {  
public static B b;  
   
public void globalVariablePointerEscape() { // 給全局變量指派,發生逃逸  
b = new B();  
}  
   
public B methodPointerEscape() { // 方法傳回值,發生逃逸  
return new B();  
}  
   
public void instancePassPointerEscape() {  
methodPointerEscape().printClassName(this); // 執行個體引用傳遞,發生逃逸  
}  
}  
   
class B {  
public void printClassName(A a) {  
System.out.println(a.class.getName());  
}  
}  
           

(3)this逃逸

this逃逸是指在構造函數傳回之前其他線程就持有該對象的引用. 調用尚未構造完全的對象的方法可能引發令人疑惑的錯誤, 是以應該避免this逃逸的發生.

public class ThisEscape {  
    public ThisEscape() {  
        new Thread(new EscapeRunnable()).start();  
        // ...  
    }  
      
    private class EscapeRunnable implements Runnable {  
        @Override  
        public void run() {  
            // 通過ThisEscape.this就可以引用外圍類對象, 但是此時外圍類對象可能還沒有構造完成, 即發生了外圍類的this引用的逃逸  
        }  
    }  
}  

解決方案:
public class ThisEscape {  
    private Thread t;  
    public ThisEscape() {  
        t = new Thread(new EscapeRunnable());  
        // ...  
    }  
      
    public void init() {  
        t.start();  
    }  
      
    private class EscapeRunnable implements Runnable {  
        @Override  
        public void run() {  
            // 通過ThisEscape.this就可以引用外圍類對象, 此時可以保證外圍類對象已經構造完成  
        }  
    }  
}  
           

12、final關鍵詞

答:(1)final的作用:

final關鍵字提高了性能。JVM和Java應用都會緩存final變量。

final變量可以安全的在多線程環境下進行共享,而不需要額外的同步開銷。

使用final關鍵字,JVM會對方法、變量及類進行優化。

(2)final的記憶體模型

final域,編譯器和處理器要遵守兩個重排序規則:

1)在構造函數内對一個final域的寫入,與随後把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。

2)初次讀一個包含final域的對象的引用,與随後初次讀這個final域,這兩個操作之間不能重排序。

寫final域的重排序規則會要求譯編器在final域的寫之後,構造函數return之前,插入一個StoreStore障屏。讀final域的重排序規則要求編譯器在讀final域的操作前面插入一個LoadLoad屏障。

Java 知識點總結之Java 并發 API(二)

(3)      final引用不能從構造函數内“逸出”

13、volatile關鍵字

答:(1)volatile關鍵字來保證可見性

1)當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去記憶體中讀取新值。

2)禁止進行指令重排序

         (2)volatile不能保證原子性

                   可見性隻能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。

         (3)volatile一定程度上保證有序性

//x、y為非volatile變量  
//flag為volatile變量  
  
x = 2;        //語句1  
y = 0;        //語句2  
flag = true;  //語句3  
x = 4;         //語句4  
y = -1;       //語句5  
           

由于flag變量為volatile變量,那麼在進行指令重排序的過程的時候,不會将語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

         (4)使用volatile必須具備以下2個條件:

對變量的寫操作不依賴于目前值

該變量沒有包含在具有其他變量的不變式中

    volatile boolean inited = false;

    private volatile static Singleton instance= null;

也就是最簡單的形式