Java經典面試題(其二)——Java線程同步方式和線程本地變量

Java經典面試題(其二)——Java線程同步方式和線程本地變量

實現線程同步的幾種方式

1.爲何要使用同步?

Java允許多線程併發控制,當多個線程同時操作一個可共享資源變量時(如數據的增刪改查),將會導致數據不準確,相互之間產生衝突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。

2.同步的方式

1>.同步方式

即有synchronized關鍵字修改的方法。由於Java的每個對象都有一個內置鎖,當用此關鍵字修飾時,內置鎖會保護整個方法。在調用該方法前,需要獲取內置鎖,否則就處於阻塞狀態。


// 代碼如:

public synchronized void save(){}

// 注:synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個表。

2>.同步代碼塊

即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。

// 代碼如:

synchronized(Object){
}

// 注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。

package com.xhj.thread;

/**
 * 線程同步的運用
 * 
 * @author XIEHEJUN
 * 
 */
public class SynchronizedThread {

    class Bank {

        private int account = 100;

        public int getAccount() {
            return account;
        }

        /**
         * 用同步方法實現
         * 
         * @param money
         */
        public synchronized void save(int money) {
             account += money;
        }

        /**
         * 用同步代碼塊實現
         * 
         * @param money
         */
        public void save1(int money) {
            synchronized (this) {
                account += money;
            }
        }
    }

    class NewThread implements Runnable {
        private Bank bank;

        public NewThread(Bank bank) {
            this.bank = bank;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                // bank.save1(10);
                bank.save(10);
                System.out.println(i + "賬戶餘額爲:" + bank.getAccount());
            }
        }

    }

    /**
     * 建立線程,調用內部類
     */
    public void useThread() {
        Bank bank = new Bank();
        NewThread new_thread = new NewThread(bank);
        System.out.println("線程1");
        Thread thread1 = new Thread(new_thread);
        thread1.start();
        System.out.println("線程2");
        Thread thread2 = new Thread(new_thread);
        thread2.start();
    }

    public static void main(String[] args) {
        SynchronizedThread st = new SynchronizedThread();
        st.useThread();
    }
}
    

3>. 使用特殊域變量(volatile)實現線程同步

a.volatile關鍵字爲域變量的訪問提供了一種免鎖機制;

b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新;

c.因此每次使用該域就要重新計算,而不是使用寄存器中的值;

d.volatile不會提供任何原子操作,它也不能用來修改final類型的變量。

// 代碼實例

class Bank {
    //需要同步的變量加上volatile
    private volatile int account = 100;

    public int getAccount() {
        return account;
    }
    //這裏不再需要synchronized 
    public void save(int money) {
        account += money;
    }
}

// 多線程中的非同步問題主要在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。

4>.使用重入鎖實現線程同步

在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和塊具有相同的基本行爲和語義,並且擴展了其能力。

ReenreantLock類的常用方法有:

ReentrantLock() : 創建一個ReentrantLock實例

lock() : 獲得鎖

unlock() : 釋放鎖

class Bank {

    private int account = 100;
    //需要聲明這個鎖
    private Lock lock = new ReentrantLock();
    public int getAccount() {
        return account;
    }
    //這裏不再需要synchronized 
    public void save(int money) {
        lock.lock();
        try{
            account += money;
        }finally{
            lock.unlock();
        }

    }
}

// 注:關於Lock對象和synchronized關鍵字的選擇:

// a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助用戶處理所有與鎖相關的代碼。

// b.如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因爲它能簡化代碼

// c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖

5>.使用局部變量實現線程同步

如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。

ThreadLocal類型的常用方法
() : ThreadLocal() : 創建䘝線程本地變量
get() : 返回此線程局部變量的當前線程副本中的值

initialValue() : 返回此線程局部變量的當前線程的“初始值”
set(T value) : 將此線程局部變量的當前線程副本中的值設置爲value

// 例如:在上面例子的基礎上,修改後的代碼爲:

 public class Bank{
    //使用ThreadLocal類管理共享變量account
    private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
           return 100;
        }
    };
    public void save(int money){
        account.set(account.get()+money);
    }
    public int getAccount(){
        return account.get();
    }
}

// 注: ThreadLocal與同步機制

// a.ThreadLocal與同步機制都是爲了解決多線程中相同變量的訪問衝突問題。

// b.前者採用以"空間換時間“的方法,後者採用以”時間換空間“的方式。

threadlocal原理及常用應用場景

1.對ThreadLocal的理解

ThreadLocal,很多地方叫做線程本地變量,也有叫線程本地存儲,其實意思差不多。可能很多朋友鬥志ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

// 我們還是先來看一個例子:

class ConnectionManager {  
   
    private static Connection connect = null;  
   
    public static Connection openConnection() {  
        if(connect == null){  
            connect = DriverManager.getConnection();  
        }  
        return connect;  
    }  
   
    public static void closeConnection() {  
        if(connect!=null)  
            connect.close();  
    }  
} 

  假設有這樣一個數據庫連接管理類,這段代碼在單線程中使用是沒有問題的,但是如果在多線程中使用呢?很明顯,在多線程中使用會存在線程安全問題:第一,這裏面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那麼必然在調用connect的地方需要使用到同步來保障線程安全,因爲很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉連接。

  所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。

  這樣將會大大影響程序執行效率,因爲一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。

  那麼大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上沒有依賴關係的,即一個線程不需要關係其他線程是否對這個connect進行了修改的。

  到這裏,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫連接,然後在方法調用完畢再釋放這個連接。比如下面這樣:

class ConnectionManager {  
   
    private Connection connect = null;  
   
    public Connection openConnection() {  
        if(connect == null){  
            connect = DriverManager.getConnection();  
        }  
        return connect;  
    }  
   
    public void closeConnection() {  
        if(connect!=null)  
            connect.close();  
    }  
}  
   
class Dao{  
    public void insert() {  
        ConnectionManager connectionManager = new ConnectionManager();  
        Connection connection = connectionManager.openConnection();  
   
    //使用connection進行操作  
   
        connectionManager.closeConnection();  
     }  
}

  這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那麼線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且驗證影響程序執行性能。由於在方法中需要頻繁地開啓和關閉數據庫連接,這樣不僅驗證影響程序執行效率,還可能導致服務器壓力巨大。

  但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的佔用會比不使用ThreadLocal要大。

  1. 深入解析ThreadLocal類

  在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。先了解一下ThreadLocal類提供的幾個方法:

// get()方法是與用來獲取ThreadLocal在當前線程中保存的變量副本

public T get(){}

// set()用來設置當前線程中變量的副本

public void set(T value){}

// remove()用來移除當前線程中變量的副本

public void remove(){}

// initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。

protected T initialValue(){}

發佈了68 篇原創文章 · 獲贊 119 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章