【Java並發編程一】線程安全和共享對象


一、什么是線程安全

  當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步及在調用代碼代碼不必作其他的協調,這個類的行為仍然是正確的,那么稱這個類是線程安全的。

  內部鎖

  Java提供了強制性的內置鎖機制:synchronized塊。一個synchronized塊有兩個部分:鎖對象的引用,以及這個鎖保護的代碼塊。執行線程進入synchronized塊之前會自動獲得鎖,無論通過正常控制路徑退出還是從塊中拋出異常,線程都在放棄對synchronized塊的控制時自動釋放鎖。獲得內部鎖的唯一途徑是:進入這個內部鎖保護的同步塊或方法。 
內部鎖在Java中扮演了互斥鎖的角色,意味着至多只有一個線程可以擁有鎖。

  重進入

  內部鎖是可重進入的,這意味着鎖的請求是基於“每線程”,而不是基於“每調用”的,重進入的實現是通過為每個鎖關聯一個請求技術和一個占有它的線程。當計數為0時,認為鎖是未被占有的。線程請求一個未被占有的鎖時,JVM將鎖記錄鎖的占有者且將請求計數值置為1,。如果同一線程再次請求這個鎖,計數 將遞增,每次占用線程退出同步塊,計數值將遞減。直到計數器達到0時,鎖被釋放。

二、共享對象

  synchronized不僅用於原子操作,划定“臨界區”,另外還可以確保當一個線程修改對象的狀態后,其他線程能夠真正看到變化。

  可見性

  當讀與寫發生在不同的線程時,通常不能保證讀線程及時讀取其他線程寫入的值。例如下面這個例子:

public class TestMain
{
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread
    {
        @Override
        public void run()
        {

            while(!ready)
            {
                System.out.println("讀線程");
                Thread.yield();  //暫停當前正在執行的線程對象
                System.out.println(number);
            }
        }
    }
    public static void main(String[] args) throws InterruptedException
    {
        new ReaderThread().start();
        System.out.println("main線程");
        number=42;
        ready=true;

    }
}

  在上面的代碼中,主線程啟動讀進程,然后把number設為42,ready設為true,讀進程進行循環。經測試,該程序運行的結果有下面幾種可能:

  • 打印0

這里寫圖片描述 

  • 打印42

這里寫圖片描述

  • 什么都不打印

這里寫圖片描述

  出現上面的錯誤是因為在沒有同步的情況下,線程運行的順序不同導致了不同的運行結果。有一個簡單的方法來避免這樣的問題:只要數據需要被跨線程共享,就進行恰當的同步。

  上面的例子能夠引起意外的后果:過期數據。過期數據不會發生在全部變量上,也不完全不出現。 
鎖不僅僅是關於同步和互斥的,也是關於內存可見的。為了保證所有的線程都能看到共享的、可變變量的最新值,讀取和寫入線程必須使用公共的鎖進行同步。

  Volatile變量

  volatile變量,確保對一個變量的更新以可預見的方式告知其它的線程。當一個域聲明為volatile類型后,編譯器與運行時會監視這個變量,它是共享的,而且對它的操作不會與其它的內存操作一起被重排序。volatile變量不會緩存在寄存器或者緩存在對其它處理器隱藏的地方。 
  volatile變量的操作不會加鎖,也不會引起執行線程的阻塞,這使得volatile變量相對於sychronized而言是一種輕量級的同步機制。 
  volatile變量通常被當做是標識完成、中斷、狀態的標記使用。它也存在一些限制。volatile變量只能保證可見性,而加鎖可以保證可見性和原子性。 
  只有滿足下面所有的標准后,你才能使用volatile變量:

  • 寫入變量時並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值
  • 變量不需要與其它的狀態量共同參與不變約束
  • 訪問變量時,沒有其他的原因需要加鎖

  ThreadLocal

  ThreadLocal不是一個線程的本地實現版本,它不是一個Thread,而是線程布局變量,為每一個使用該變量的線程都提供了一個變量值的副本。 
  從線程的角度看,每一個線程都保持一個對其線程局部變量副本的隱式引用,只要線程時活動的並且ThreadLocal實例是可訪問的。 
  JVM為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境出現的並發問題提供了一種隔離機制。這種隔離機制與同步機制不同,同步機制才用了“以時間換空間”的方式,而ThreadLocal才用了“以空間換時間”的方式,前者僅提供一份變量,讓不同的線程排隊訪問,后者則為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

  ThreadLocal的實現思路

  查看Thread的源碼,我們可以看到,

public
class Thread implements Runnable
 {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile char  name[];
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    ...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

  Thread類中有一個ThreadLocal.ThreadLocalMap類型的變量threadLocals,就是用它來存儲當前線程變量的副本。ThreadLocalMap是ThreadLocal類的靜態內部類,ThreadLocal類有一個函數public T get():

 public T get() 
 {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
         {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) 
            {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
}

  該函數首先獲取當前線程,然后通過getMap(t)方法獲取ThreadLocalMap類型的map,這個map也就是當前線程的變量threadLocals。接下來獲取key-value鍵值對,如果獲取成功,則返回value值,若map為空,則調用setInitialValue方法返回value。

ThreadLocalMap getMap(Thread t)
 {
     return t.threadLocals;
 }

  下面看一下ThreadLocalMap的實現:

static class ThreadLocalMap
  {
       static class Entry extends WeakReference<ThreadLocal<?>> 
       {
            Object value;
            Entry(ThreadLocal<?> k, Object v) 
            {
                super(k);
                value = v;
            }
       }
        ....
}

  ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作為鍵值。 
  下面是各對象之間的引用關系圖,實線表示強引用,虛線表示弱引用: 
  這里寫圖片描述 
  綜上,在每個線程Thread內部有一個ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前的ThreadLocal變量,value為變量副本。 
  初始化時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。 
  然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。例子: 

public class TestThreadLocal 
{
    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>()
    {
        @Override
        protected Integer initialValue()
        {
            return 0;
        }
    };
    public static void main(String[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            new Thread(new MyThread(i)).start();
        }
    }
    static class MyThread implements Runnable
    {
        private int index;

        public MyThread(int index)
        {
            this.index = index;
        }
        public void run()
        {
            System.out.println("線程" + index + "的初始value:" + value.get());
            for (int i = 0; i < 10; i++) 
            {
                value.set(value.get() + i);
            }
            System.out.println("線程" + index + "的累加value:" + value.get());
        }
    }
}

  運行結果:

運行結果 
可以看到,各個線程的value值是相互獨立的,本線程的累加操作不會影響到其他線程的值,真正達到了線程內部隔離的效果。

三、參考資料

    1. 深入研究java.lang.ThreadLocal類
    2. Java並發編程:深入剖析ThreadLocal
    3. [Java並發包學習七]解密ThreadLocal


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM