Java併發程式設計規則:構建執行緒安全的共享物件
構建執行緒安全的共享物件,使其在多執行緒環境下能夠提供安全的訪問。編寫正確的併發程式關鍵在於控制共享、可變的狀態進行訪問管理。synchornized關鍵字既可以阻塞程式,也可以維護操作的原子性,它是一個執行緒安全與非執行緒安全的臨界區標識,通過它我們可以控制物件的記憶體可見性。不得不提到volatile,volatile僅僅是控制可見性,而同步性卻不及synchornized。
多執行緒訪問共享變數的詭異結果:
package net.jcip.examples; /** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */ public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
static的變數意味著,在記憶體中它是一個公用變數;在多執行緒中,這裡輸出的結果是難於預估的,可能是42也可能是0,甚至執行緒不停止;因為這裡沒用使用任何的同步機制加以控制。處理方法是:需要跨執行緒的變數,需要加入恰當的同步。
解決多執行緒同步的可見性問題:
鎖不僅僅是關於同步與互斥的,也是關於記憶體可見的。為了保證所有執行緒都能夠看到共享的、可變變數的最新值,讀取和寫入必須使用公共的鎖進行同步。根據上述多執行緒詭異問題,下面來加入適當的同步:
package net.jcip.examples; /** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */ public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { synchronized (NoVisibility.class) { while (!ready) Thread.yield(); System.out.println(this.getName() + number); } } } public static void resetValue(boolean ready, int number) { synchronized (NoVisibility.class) { NoVisibility.number = number; NoVisibility.ready = ready; } } public static void main(String[] args) { for (int i = 0; i < 200; i++) { Thread thread = new ReaderThread(); thread.setName("THREAD:"+i + "---"); thread.start(); NoVisibility.resetValue(true, i); } } }
最簡單的改法是將變數的可見性改了,使用volatile關鍵字(這裡不推薦,請看程式碼後面的注意):
package net.jcip.examples; /** * NoVisibility * <p/> * Sharing variables without synchronization * * @author Brian Goetz and Tim Peierls */ public class NoVisibility { private static volatile boolean ready; private static volatile int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(this.getName() + number); } } public static void resetValue(boolean ready, int number) { NoVisibility.number = number; NoVisibility.ready = ready; } public static void main(String[] args) { for (int i = 0; i < 200; i++) { Thread thread = new ReaderThread(); thread.setName("THREAD:"+i + "---"); thread.start(); NoVisibility.resetValue(true, i); } } }
注意:只有當volatile變數能夠簡化實現和同步策略的驗證時,才使用它們。當驗證正確性必須推斷可見性問題時,應當避免使用volatile變數。正確使用volatile變數的方式包括:用於確保它們所引用物件的狀態的可見性,或者用於標識重要的生命週期事件的發生(如,狀態:啟用、關閉)。加鎖可以保證可見性和原子性,volatile只能保證可見性。
Volatile變數使用的條件:
1.寫入變數不依賴於當前值;或者確保只有單一執行緒在修改該值;
2、變數不需要與其他狀態參與不變約束;
3、訪問變數時,沒有其他原因需要加鎖;
多執行緒訪問造成過期資料問題:
當一個執行緒在沒有同步的情況下讀取變數,它可能會得到一個過期值。還是上面那段程式碼中的例子。在多執行緒中,可能ready在其它執行緒中的值還未被修改。所以很有可能ready=false;如果在多執行緒中處理業務,那麼變數的狀態將是資料出錯的致命元凶。有時候資料不能快速地實現同步並不傷大雅,但是是以犧牲體驗效果為代價的。如果我們多個指令在更新裝置的GPS位置,可能偏差不是很大,但是效果不佳是顯而易見的。來看個例子:
package net.jcip.examples;
import net.jcip.annotations.*;
/**
* MutableInteger
* <p/>
* Non-thread-safe mutable integer holder
*
* @author Brian Goetz and Tim Peierls
*/
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
將其修改為執行緒安全類的做法,為getter、setter加synchronized同步關鍵字:
package net.jcip.examples;
import net.jcip.annotations.*;
/**
* SynchronizedInteger
* <p/>
* Thread-safe mutable integer holder
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
注:getter也需要同步,否則不能保證其他執行緒讀取到的value是最新的。
安全地釋出物件:
釋出物件 實際上就是擴大物件提供訪問範圍,如public的屬性和方法。安全釋出物件的目的是:維護物件的封裝性和不可變性,防止不正當地使用物件。下面來看一個物件釋出示例:
package net.jcip.examples;
import java.util.*;
/**
* Secrets
*
* Publishing an object
*
* @author Brian Goetz and Tim Peierls
*/
class Secrets {
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
}
class Secret {
}
最常見的釋出物件就是將物件儲存到靜態變數中提供訪問。上面initialize方法例項化儲存物件到knownSecrets的引用,將Secret物件新增到knownSecrets就是一個釋出物件的操作。釋出物件,可以說就是提供物件對外訪問的入口。它可以是一個私有屬性提供共有方法的訪問,這也實現了物件的釋出。但是,下面的示例不推薦大家這樣寫:
package net.jcip.examples;
/**
* UnsafeStates
* <p/>
* Allowing internal mutable state to escape
*
* @author Brian Goetz and Tim Peierls
*/
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] getStates() {
return states;
}
}
雖然我們可以獲取到狀態名稱,但是我們不知道對於使用getStates()方法的執行緒將如何使用它,這是一個執行緒引用的安全問題。所以,封裝使得程式的正確性分析更可行,而且不容易偶然地破壞設計約束。
將物件封裝好了再發布:
使用一個未完全封裝的物件,就破壞了程式的封裝性。不要讓this構造在構造期間逸出。來看下面一個例子:
package net.jcip.examples;
/**
* ThisEscape
* <p/>
* Implicitly allowing the this reference to escape
*
* @author Brian Goetz and Tim Peierls
*/
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
此處的建構函式存在方法的重寫,source.registerListener的實現就是不安全的物件釋出,因為在釋出期間呼叫了registerListener方法。
修正方法:使用工廠方法防止this引用在構造期間逸出。示例如下:
package net.jcip.examples;
/**
* SafeListener
* <p/>
* Using a factory method to prevent the this reference from escaping during construction
*
* @author Brian Goetz and Tim Peierls
*/
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
只要在初始化期間不要進行業務處理就可以實現執行緒安全了,即通過其他方法來啟動執行緒的呼叫。安全地共享物件:
在併發程式中,使用和共享物件的一些最有效的策略如下:
-------------執行緒限制:--------------
一個執行緒限制的物件,通過限制線上程中,而被執行緒獨佔,且只能被佔有它的執行緒修改。
-------------共享只讀(read-only):-------------
一個共享的只讀物件,在沒有額外同步的情況下,可以被多個執行緒併發地訪問,但任何執行緒都不能修改它。共享只讀物件包括可變物件與高效不可變物件。
-------------共享執行緒安全(thread-safe):-------------
一個執行緒安全的物件在內部進行同步,所以其它執行緒無額外同步,就可以通過公共介面隨意地訪問它。
-------------被守護(Guarded):-------------
相關推薦
Java併發程式設計規則:構建執行緒安全的共享物件
構建執行緒安全的共享物件,使其在多執行緒環境下能夠提供安全的訪問。編寫正確的併發程式關鍵在於控制共享、可變的狀態進行訪問管理。synchornized關鍵字既可以阻塞程式,也可以維護操作的原子性,它是一個執行緒安全與非執行緒安全的臨界區標識,通過它我們可以控制物件的記憶體可
JAVA併發程式設計(五):建立執行緒的第三種方式:實現Callable介面
眾所周知建立執行緒的方式有兩種:1.繼承Thread類。2.實現Runnable介面。從jdk1.5開始,提供了另一種建立執行緒的方式。今天我們就來看看這第三種方式:實現Callable介面 一、Callable與Runnable 我們直接來看一個使用C
Java併發程式設計(03):多執行緒併發訪問,同步控制
本文原始碼:[GitHub·點這裡](https://github.com/cicadasmile/java-base-parent) || [GitEE·點這裡](https://gitee.com/cicadasmile/java-base-parent) # 一、併發問題 多執行緒學習的時候,要面
java併發程式設計(一) 執行緒安全(1)
最近想了解併發程式設計,二執行緒安全是它的基礎。所以看了下java相關的執行緒安全知識。 執行緒安全的核心是程式碼正確性(一般是輸出的結果); 首先無狀態的物件是執行緒安全的;因為一個無狀態的物件即不包含其他域;也沒有對其他域的引用; (1)原子性 原子性:即程式碼不
Java併發程式設計規則:不可變物件永遠是執行緒安全的
建立後狀態不能被修改的物件叫作不可變物件。不可變物件天生就是執行緒安全的。它們的常量(變數)是在建構函式中建立的,既然它們的狀態無法被修改,那麼這些常量永遠不會被改變——不可變物件永遠是執行緒安全的。 不可變性的理解: 無論是Java語言規範還是Java儲存模型都沒有對不可
Java併發程式設計規則:原子變數實現執行緒安全
判定規則: 如果一個類中存在變數,並且此變數的操作不是原子操作,那麼這個類就是非執行緒安全的類。線上程產生競爭條件的情況下,多執行緒訪問導致原子性不可保證。 競爭條件產生的原因: 當計算的正確性依賴於執行時中相關的時序或多執行緒的交替時,會產生競爭條件。多執行緒情況下,執行
Java併發程式設計規則:無狀態物件永遠是執行緒安全的
規則說明: 無狀態物件即無狀態類,是指其本身沒有內部變數和外部變數的操作的,在每個使用者訪問的執行緒棧中都是一個各自的例項。 執行緒安全的表現: 一個執行緒對該類的訪問不會影響其他執行緒的訪問結果。
《java併發程式設計實戰》之 執行緒安全性
1.執行緒安全性 當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。 無狀態物件一定是執行緒安全的,何為無狀態,就是類中不包含任何域,也不包含各種其
[讀書筆記][Java併發程式設計實戰]第二章 執行緒安全性
第二章 執行緒安全性 1-什麼是執行緒安全的類? 當多個執行緒訪問某一個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個
(十)java併發程式設計--建立和啟動執行緒(java.lang.Thread 、java.lang.Runnable)
執行緒建立的幾種方式. 建立和啟動一個執行緒 建立一個執行緒. Thread thread = new Thread(); 啟動java執行緒. thread.start(); 這兩個例子並沒有執行執行緒執行體,執行緒將會啟動後然後
學了Java併發程式設計藝術及多執行緒核心程式設計技術,以及最開始學的程式設計思想那本書,今天做些總結
併發Map分析位碼shift預設值是28,對hash值右移28位,取高四位,獲得segments位置,掩碼mask預設值16-1,作一個與值,不知道有何用處,兩個都是不可修改,初始值和併發度有關,一旦確立下來決定了segments陣列大小,包括segments陣列物件不可修改
Java併發程式設計中的多執行緒是怎麼實現的?
眾所周知,在Java的知識體系中,併發程式設計是非常重要的一環,也是面試中必問的題,一個好的Java程式設計師是必須對併發程式設計這塊有所瞭解的。 併發必須知道的概念在深入學習併發程式設計之前,我們需要了解幾個基本的概念。同步和非同步同步和非同步用請求返回呼叫的方式來理解相對簡單。 同步:
Java併發程式設計札記-(六)JUC執行緒池-01概述
前面的例子中總是需要執行緒時就建立,不需要就銷燬它。但頻繁建立和銷燬執行緒是很耗資源的,在併發量較高的情況下頻繁建立和銷燬執行緒會降低系統的效率。執行緒池可以通過重複利用已建立的執行緒降低執行緒建立和銷
Java併發程式設計中四種執行緒池及自定義執行緒使用教程
引言 通過前面的文章,我們學習了Executor框架中的核心類ThreadPoolExecutor ,對於執行緒池的核心排程機制有了一定的瞭解,並且成功使用ThreadPoolExecutor 建立了執行緒池。 而在Java中,除了ThreadPoolExecutor ,Executor框
Java併發程式設計的藝術(四)——執行緒的狀態
執行緒的狀態 初始態:NEW 建立一個Thread物件,但還未呼叫start()啟動執行緒時,執行緒處於初始態。 執行態:RUNNABLE 在Java中,執行態包括就緒態 和 執行態。 就緒態 該狀態下的執行緒已經獲得執行所需的所有資源
java併發程式設計-再談daemon執行緒
守護執行緒:顧名思義是用來做“守護神”的工作,一直守護著使用者執行緒直到使用者執行緒工作完畢(比如:main執行緒結束)。對於守護執行緒我們需要注意兩點:通過呼叫setDaemon(true)方法將執行
Java併發程式設計的藝術(十)——執行緒池(1)
執行緒池的作用 減少資源的開銷 減少了每次建立執行緒、銷燬執行緒的開銷。 提高響應速度 每次請求到來時,由於執行緒的建立已經完成,故可以直接執行任務,因此提高了響應速度。 提高執行緒的可管理性 執行緒是一種稀缺資源,若不加以限制,不僅會佔用大量資源
Java併發程式設計---Executors多工執行緒框架
一.概念 為了更好地控制多執行緒,JDK提供了一套執行緒框架Executor,幫助開發人員有效地進行執行緒控制.他們都在java.util.concurrent包中,是JDK併發包的核
Java併發程式設計(一)執行緒定義、狀態和屬性
一 、執行緒和程序 1. 什麼是執行緒和程序的區別: 執行緒是指程式在執行過程中,能夠執行程式程式碼的一個執行單元。在java語言中,執行緒有四種狀態:執行 、就緒、掛起和結束。 程序是指一段正在執行的程式。而執行緒有時也被成為輕量級的程序,他是程式執
Java 併發程式設計(二)執行緒狀態躍遷
執行緒的狀態: 1、新建狀態(New) 新建立了一個執行緒物件。 示例: Thread t = new Thread(); 2、就緒狀態(Runnable) 執行緒物件建立後,