1. 程式人生 > >Java併發程式設計規則:構建執行緒安全的共享物件

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) 執行緒物件建立後,