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要大。
- 深入解析ThreadLocal類
在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。先了解一下ThreadLocal類提供的幾個方法:
// get()方法是與用來獲取ThreadLocal在當前線程中保存的變量副本
public T get(){}
// set()用來設置當前線程中變量的副本
public void set(T value){}
// remove()用來移除當前線程中變量的副本
public void remove(){}
// initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法,下面會詳細說明。
protected T initialValue(){}