1、線程安全與鎖
線程安全的本質,在于 存在了共享的可變狀態 status, 在多線程共同操作狀態變量時,當計算的正確性依賴于運行時相關的時序(比如i++,先取出i,再+1,然后賦值),就會出現競爭(競態)
無狀態對象永遠是線程安全的.
所以線程安全 三步驟:a、無共享狀態;b、共享狀態不可變;c、共享狀態同步
要做到狀態同步,就必須要通過鎖 or volatile;本章節先討論鎖,被鎖保護的代碼塊可以認為是一間屋子,只有唯一的鎖可以打開,即同時只能只有一個線程進入。其他進入的線程必須拿到這把鎖。這樣就保證了數據的安全性。
I、內部鎖 synchronized
三種用法:a、鎖住變量 (這個變量就是鎖);b、鎖住方法 (擁有這個方法的對象是鎖) ;c、鎖住靜態方法 (這個類的Class對象是鎖)
每個java對象都自帶隱式鎖(也稱為監視器鎖)
可重入 - 每個線程可以對同一個鎖多次獲取,比如 下面這個例子 a 獲取一次,b還要再獲取,獲取時計數\退出時減少(像不像早期的垃圾回收)
public synchronized a(){
b();
}
public synchronized b(){
}
II、顯式鎖 ReentrantLock
ReentrantLock + Condition 替換了原先的 synchronized + object
Condition 實現了 資源的 競爭與釋放 ; Lock 實現了 代碼的隔離
Lock lock = new ReentrantLock();
Waits = lock.newConidition();
Lock.lock();
Try{
Waits.await() – 等同于 原先的鎖wait
}finnaly{
Lock.unlock
}
顯示鎖有什么好處?
a、tryLock( xxx ) -- 可以設置超時,無法獲取的時候,這樣可以避免死鎖
b、lockInterruptibly(); -- 可以響應中斷的鎖
c、性能提升? -- java6 以后的版本已經沒有性能優勢了
d、讀寫分離
顯式鎖里面,有專門的 ReentrantReadWriteLock 讀寫分離鎖。好吧,這里又看到類似數據庫的設計思路了。
讀鎖可以多線程同時進入,是共享的。寫鎖只能單線程進入,是排他的
e、公平
初始化 ReentrantLock 時,可以指定是否是公平的。何解? 因為線程調度喚醒線程時,是隨機的,有可能先來的線程未獲取到,這就不公平了。指定公平性,可以按等待順序獲取鎖
有啥缺點?
每次都需要顯式手工finnaly 關閉
內部鎖 or 顯式鎖?
如何選擇兩者。jdk 推薦 默認使用 內部鎖,因為簡單、方便;如果有特殊需求的,如上顯示鎖描述的好處。
III、死鎖
a、出現死鎖的條件:
互斥 - 資源只能被一個線程占有
請求與保持 - 占住的不想放,想要的非要
不可剝奪 - 占住的不能搶走
循環等待 - 只要有人放手就解開了,就是沒人放手
b、如果預防死鎖?
排好隊 - 如果非要獲取多個鎖,那所有的獲取鎖方式都要 按照一定順序
中斷(搶奪) - 顯式鎖可以中斷某個鎖
超時(放棄) - 顯式鎖可以設置超時放棄鎖
c、如何檢測死鎖?
打印堆棧
win( Ctrl + Break ) linux (kill -3)
Jmap+jhat+jstatck
監視工具
JConsole | Visual VM
IV、使用鎖的建議
a、如何選擇內部鎖和顯式鎖,默認內部鎖,特殊情況比如定時、中斷使用顯式。(參見上文描述)
b、盡量減少同步的范圍、 不要在 方法上加鎖,而是在方法內部
c、多個鎖按順序獲取,可以把獲取的地方統一隔離,放在一起,通過對象hash值identityHashCode安排順序
2、線程安全與對象(非阻塞式實現)
同步是 保護 對象(數據) 的一種方式,除了用鎖這種同步方式,還有其他效率更高的方式。
I、volatile - 共享變量
系統會監視這個變量,對它的操作不會和其他內存操作重排序,也就是說對volatile 變量的操作當成是原子操作;所有的修改其他線程都可見,也就不需要加鎖同步了。
volatile int a;
對volatile的使用有一些限制,主要是自增類的操作不能使用(比如i++)、只能單獨使用、不能組合使用
II、CAS 與原子類型 -- CAS = 比較并設置CompareAndSet
實現思路很簡單,就是當修改的值和 現在內存的值比較,如果一致了,就修改生效;如果不一致就一直循環檢查,直到生效為止。
public final long getAndDecrement() {
while (true) {
long current = get();
long next = current - 1;
if (compareAndSet(current, next))
return current;
}
}
volatile和 CAS 都是CPU層面提供的底層技術,不是語言本身提供的技術,其中CAS的實現還需要調用 JNI 本地化實現。
volatile + CAS 構成了java的原子類型庫 java\util\concurrent\atomic
a、基本類型的原子包裝器 AtomicInteger | AtomicLong | AtomicBoolean
b、數組類型 AtomicIntegerArray ...
c、對象類型 AtomicReference (包裝一個類) | 域更新器 AtomicReferenceFieldUpdater(利用反射,更細粒度的操作)
-- java自身的cocurrent類庫也有使用原子類型的,比如ConcurrentLinkedQueue 就使用了 域更新器實現 具體鏈表 字段的更新
該如何選擇 鎖 or 原子類型+volatile?
一般需要用在控制變量、標記值等等應用場景應該選用 原子類型這些非阻塞的同步方式;如果有大段代碼復雜邏輯需要保護,則采用同步方式。
III、封閉線程Ad-hoc 與ThreadLocal
把數據封閉在線程內部,就不存在共享狀態的問題,也就是線程安全了,最典型的應用就是 ThreadLocal,網上對ThreadLocal很多理解都有誤區
a、每個線程綁定一個 ThreadLocal.ThreadLocalMap 對象,線程內部的局部變量。
b、存放對象 set(T) 實際上就是 把 ThreadLocal 對象作為key ,T 作為value put 到 線程的map對象中
c、獲取對象 get,實際上就是把 this自身的對象 作為key 從 線程map中取出數據
幾個關鍵問題
a、多個線程的 ThreadLocal 對象雖然是同一個,但Map 是線程自己的,所以在調用 set get 時,獲取的是自己Map存儲的 value,實現了數據封閉
b、設置的value 必須是新的對象或者基本類型,否則如果設置的 是同一個 對象的引用的話,取出來還是同一個引用,就達到不到隔離的效果。
c、如果需要 在所有線程共享一個初始數據,可以 繼承ThreadLocal,擴展實現 initialValue 方法,這樣所有線程都能看到初始化數據
private static ThreadLocal> threadLog = new ThreadLocal>(){
protected List initialValue(){
return new ArrayList();
}
};
總結:ThreadLocal本質上是一個 空間換時間的 實現,通過在多個線程中 存儲不同的拷貝,實現了線程安全
3、線程生命周期
I、五種狀態
新建 - 兩種方法創建多線程:繼承java.lang.Thread,覆蓋run方法 | 實現java.lang.Runnable接口,實現run方法
就緒 - Thread.start - 對應run 方法
運行 - 線程搶到CPU開始運行
阻塞 - 遇到 鎖、休眠、IO等情況線程處于阻塞態 (等待、休眠)
終止 - 正常結束,被中斷 Thread.interrrupt
II、改變狀態
a、sleep - 當前線程休眠
b、yield - 當前線程讓渡CPU給同級別的線程
c、wait - 阻塞等待其他資源的鎖
d、join - 傻傻等其他線程結束后再結束
sleep vs wait
sleep 只是線程休眠,并不會釋放占有的資源,wait會釋放所有的資源;wait 是資源上的方法,而休眠是線程方法
sleep vs yield
sleep 線程切入休眠狀態,等待一定時間才喚醒搶占CPU;yield 只是暫時讓渡CPU
在構造函數中啟動線程是危險的,因為可能構造函數還未構造完成,如果回調或者其他就會出現異常。避免在構造函數啟動線程。
III、線程優先級
調度器根據線程優先級作為參考,決定多長時間調度該線程 ;Thread類中定義了默認3個級別 1 MIN ,5 NORMAL ,10 MAX
4、線程間通訊
嚴格來說 join 方法也算是一種線程間通訊,一個線程等待另一個線程處理結束,才繼續。
線程間通訊,不像進程間通訊Socket、共享內存、系統級別信號量,線程間因為共享資源,所以通訊方式大都是通過共享的資源。
傳統方式
I、經典通訊方式 wait/notify/notifyAll
使用wait/notify的正確姿勢
線程A
synchronized(list){
while(list.size == 0)
list.wait();
}
線程B
synchronized(list){
list.add(xxx);
list.notifyAll();
}
a、wait 方法一般都要寫在synchronized 的循環里
b、synchronized 鎖住的對象和 wait的對象一般是同一個對象
c、使用notifyAll 而不是 notify
wait 有無參數? 有參數只是多了一個喚醒條件,喚醒后和wait一樣繼續搶占資源
notify 和 notifyAll ? notifyAll喚醒所有線程,只有一個線程搶到,notify只隨機喚醒一個線程
線程屏障
II、閉鎖 CountDownLatch 和 關卡 CyclicBarrier
a、從功能上看,兩者很類似,都是用作阻塞的線程控制。
b、從語義上看,略有區別,閉鎖等待的是事件、關卡等待的是線程,兩者都是在await等待;閉鎖通過 countDown 這個事件、觸發等待的線程,而關卡需要互相等待、等所有線程處理完成后、觸發等待的線程。
c、更大的區別則是:CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得阻塞的線程數量。isBroken方法用來知道阻塞的線程是否被中斷。
III、信號量 Semaphore
和操作系統的信號量概念一樣,允許一個資源被被操作使用的次數
Semaphore s = new Semaphore(100);
申請 s.acquire()
釋放 s.release()
以上三種使用方法,wait/閉鎖/信號量 某種程度上都屬于資源申請;而閉鎖,則是單純的線程等待控制。下面介紹另一種方式,數據交換.
IV、Exchanger 數據交換
只能兩個線程進行數據交換
Exchange exc = new Exchange();
A線程執行 IN = exc.exchange(OUT) ,此時A線程阻塞
B線程執行 OUT = exc.exchange(IN) ,此時A、線程解除阻塞,雙方數據發生了交換
5、線程池
線程池一般有兩種實現方式:一種是 任務池,各個線程循環去池里取任務;一種是 線程池,提交任務后,從池里取出空閑線程處理。
I、ThreadPoolExecutor
任務隊列BlockingQueue - 使用了阻塞隊列作為提交的任務,接受繼承 Runnable 的任務
線程池HashSet - 最少corePoolSize 最大 maximumPoolSize 空閑時間 keepAliveTime ;這三個參數決定 線程數目在 最小和最大之間彈性處理(當 > 最少,且 > 空閑時間,釋放一部分空閑線程)
線程工廠ThreadFactory - 提供創建線程的方法 (還是為了擴展性, 給應用更多的創建線程前后的處理工作),Executors框架提供了一些線程工廠的實現
工作線程Worker - 實現了Runnable ,綁定了一個線程(ThreadFactory 創建線程時,傳入),執行提交的任務外,還有beforeExecute afterExecute 的鉤子函數
隊列管理
ThreadPoolExecutor 的隊列管理是開放出來由外界提供 BlockingQueue的實現,具體參見下面 并發框架的 Executors 提供的一些默認隊列
飽和策略
當有限隊列充滿后(即無法把任務提交到 BlockingQueue中去),就要有飽和策略。ThreadPoolExecutor的飽和策略,可以通過調用setRejectedExecutionHandler來修改
中止(abort)
遺棄(discard)
遺棄最舊的(discard-oldest)
調用者運行(caller-runs)
II、Executors/ExecutorServie(Executor) 框架
a、Executor 框架 -- 其實就是一個 Command模式
接口非常簡單,把一個Runnable線程作為參數
void execute(Runnable command);
一般情況下,不需要自己定義一個線程,而是提交任務給Executor框架
b、ExecutorService 框架 -- Active Object 模式(主動對象模式)
調用線程 方法的調用 和被調用線程的 方法執行 進行解耦
被調用線程內部綁定一個 線程對象,即ThredPoolExecutor 的Worker,自己管理線程的狀態
獲取被調用線程的引用
ExecutorService service = Executors.newFixedThreadPool(NTHREADS);
提交
service.execute(task);
service.submit(task);
還可以管理executor的生命周期
service.shutdown();
c、隊列
使用無限隊列 LinkedBlockingQueue (對于ThreadPoolExecutor 來說這些隊列都是有限的,可以把指定容量的隊列給他,但Executors 默認的都是無限未指定容量的隊列)
newFixedThreadPool -- 定長的線程池
newSingleThreadExecutor -- 單線程化的 executor
使用同步移交隊列 SynchronousQueue (沒有內存的隊列,直接把隊列從 提交者 給 消費者,據說等待性能更好?少了數據結構的操作開銷?)
newCachedThreadPool -- 可緩存的線程池
周期線程池
newScheduledThreadPool - 定長線程池,支持定時和周期的任務執行
Timer和TimerTask 的改進版本
原先的Timer存在一些缺陷,Timer調度是基于絕對時間,對系統時間改變很敏感。(ScheduledThreadPoolExecutor 支持相對時間);所有timer任務在一個線程執行,如果耗時操作導致其他任務延遲,會出現一些不可預料的錯誤Timer中如果timetask執行失敗,整個timer的任務都失敗
// 定時器
public class Timer {
public static final int TIME_DEFAULT = 15 * 60;
protected ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
protected int period;
protected Runnable runnable;
public Timer(Runnable runnable){
this(TIME_DEFAULT, runnable);
}
public Timer(int period, Runnable runnable){
this.period = period;
this.runnable = runnable;
}
public void start(){
service.scheduleAtFixedRate(runnable, 0, period, TimeUnit.SECONDS);
}
public void destroy(){
service.shutdown();
}
// 絕對定時器,從下一個粒度整點開始
public static class AbsoluteTimer extends Timer{
public AbsoluteTimer(Runnable runnable) {
super(runnable);
}
public AbsoluteTimer(int period, Runnable runnable) {
super(period, runnable);
}
@Override
public void start(){
service.scheduleAtFixedRate(runnable, Utils.getNextWholeTime(new Date(), period), period, TimeUnit.SECONDS);
}
}
}
d、線程工廠
DefaultThreadFactory
PrivilegedThreadFactory
III、任務管理
任務的執行,很容易操作,直接Thread 的start,或者提交到 任務執行框架 Executor執行
任務取消和停止,如何控制? 怎么知道任務執行的結果? 怎么隔離一個耗時任務 ?
取消的方法一:通過共享一個變量標志來進行任務的協調。- cancellation requested
中斷的方法二:通過interrupt 中斷方法,取消一個線程的執行
獲取返回值
通過 局部變量,或者返回函數 -- 不可行,因為主線程和子線程并沒有先后順序的約束,受限于CPU、IO等復雜因素,結果是未知的
輪詢 -- 可行,采用類似阻塞方式,不停的去檢查是否完成,但多做了很多無用功,占用了CPU
回調 -- 正解
Future/FutureTask + Callable - 增強版的任務執行
Callable 提供 Runnable的增強功能 可以支持返回值
非阻塞的模型:Promise,Future 和 Callback 典型架構,Future 是異步執行方式
下面是一個簡版的 任務執行框架,可以把任務提交給Executors框架,通過 Future 來
// 執行任務
static interface Function{
Out action() throws Exception ;
}
// 提交任務框架, 任何一次性執行的, 有可能延遲的都可以使用
private static Out submit(final Function in){
final ExecutorService exec = Executors.newSingleThreadExecutor();
Future future = exec.submit(new Callable(){
@Override
public Out call() throws Exception {
return in.action();
}
});
Out result = null;
try {
result = future.get(STREAM_TIMEOUT_PER_TIME, TimeUnit.SECONDS);
} catch (Exception e) {
log.error(String.format("Task Thread %d Get submit error : %s ", Thread.currentThread().getId(), e.getMessage()), e.getCause());
}finally{
exec.shutdown();
}
return result;
}
Fork/Join 框架?
IV、CompletionService:Executor 和 BlockingQueue的組合
整合了 Executor 和 BlockingQueue的功能。調用時,把Callable任務提交給他,同時像quue一樣取出take和poll方法,返回的是一個Future
CompletionService 和 直接調用ExecutorService 區別在于,如果ExecutorService 啟動多個任務,獲取返回值時,多個Future需要管理起來,現在CompletionService直接用BlockingQueque對Future進行管理。
V、其他
線程增加 UncaughtExceptionHandler 捕獲未知的線程異常,通知框架和應用
6、其他
JVM線程關閉 時,會有一個鉤子函數
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
// 在這里做一些清理的工作。
}
});
守護線程 - 精靈線程 (dameon)
后臺服務線程,和JVM線程同在
用戶線程和守護線程區別不大,用戶線程隨時可以退出,守護線程一直到虛擬機的生命周期。 沒有用戶線程,守護線程和JVM一起退出。
一個完整的實際使用案例 - 這個可以用 JDK7的 Fork/Join 改寫
// 下載調度線程
public class DownLoadTask {
public int MAX_THREAD ;
public int MAX_RECORD_PER_THREAD ;
public static final int DEFAULT_TIME_OUT = 120000;
private FileWriter writer;
private List list;
private ExecutorService downPool ;
private PriorityBlockingQueue container;
private CyclicBarrier oneTimes;
private AtomicInteger interrupt = new AtomicInteger(0);
private AtomicInteger timeout = new AtomicInteger(0);
private static Logger log = Logger.getLogger(DownLoadTask.class);
public DownLoadTask(List list, FileWriter writer){
this.list = list;
this.writer = writer;
}
public void setMAX_THREAD(int MAX_THREAD) {
this.MAX_THREAD = MAX_THREAD;
}
public void setMAX_RECORD_PER_THREAD(int MAX_RECORD_PER_THREAD) {
this.MAX_RECORD_PER_THREAD = MAX_RECORD_PER_THREAD;
}
public void run(){
int ONE_SORT_TOTAL_NUM = MAX_THREAD * MAX_RECORD_PER_THREAD;
downPool = Executors.newFixedThreadPool(MAX_THREAD);
container = new PriorityBlockingQueue(ONE_SORT_TOTAL_NUM);
int recordsSize = list.size();
int repeatCount = recordsSize / ONE_SORT_TOTAL_NUM;
int lastRecords = recordsSize % ONE_SORT_TOTAL_NUM;
int lastRecordsWorkerNum = (int) ceil((double)lastRecords / (double) MAX_RECORD_PER_THREAD);
int lastRecordsLastWorkNum = lastRecords % MAX_RECORD_PER_THREAD;
log.info("repeatCount size:"+ repeatCount);
log.info("lastRecords size:"+ lastRecords);
log.info("lastRecordsWorkerNum:"+ lastRecordsWorkerNum);
log.info("lastRecordsLastWorkNum:"+ lastRecordsLastWorkNum);
if(repeatCount==0){
oneTimes = new CyclicBarrier(lastRecordsWorkerNum + 1, new Combine());
}else if(repeatCount > 0){
oneTimes = new CyclicBarrier(MAX_THREAD + 1, new Combine());
}
int i = 0;
int count = 0;
int index = 0;
List listfile = new ArrayList();
for(int j=0;j
SortFileData sortfile = new SortFileData();
sortfile.setFilestruct(list.get(j));
sortfile.setIndex(j);
listfile.add(sortfile);
i++;
if(listfile.size()== MAX_RECORD_PER_THREAD){
downPool.submit(new Worker(index, listfile));
index++;
if(i % ONE_SORT_TOTAL_NUM == 0 ){
count++;
waitForComplete();
if(count == repeatCount && lastRecords != 0){
oneTimes = new CyclicBarrier(lastRecordsWorkerNum + 1, new Combine());
}
}
listfile = new ArrayList();
}
}
if (lastRecordsLastWorkNum>0){
downPool.submit(new Worker(index, listfile));
}
if(lastRecordsWorkerNum != 0){
waitForComplete();
}
log.info(String.format("result [interrup = %d, timeout = %d]", interrupt.get(), timeout.get()));
}
// 線程阻塞
public void waitForComplete(){
try {
oneTimes.await(DEFAULT_TIME_OUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.error("Thread interrupted.", e.getCause());
interrupt.incrementAndGet();
} catch (BrokenBarrierException e) {
log.info("CyclicBarrier reset", e.getCause());
} catch (TimeoutException e) {
log.error("Thread timeout.", e.getCause());
timeout.incrementAndGet();
}
}
// 用于排序的查詢結果
static class SortFileData implements Comparable{
int index;
byte[] data;
private FileStruct filestruct;
public void setIndex(int index) {
this.index = index;
}
public void setData(byte[] data) {
this.data = data;
}
public FileStruct getFilestruct() {
return filestruct;
}
public int getIndex(){
return index;
}
public byte[] getData(){
return data;
}
public void setFilestruct(FileStruct filestruct) {
this.filestruct = filestruct;
}
@Override
public int compareTo(SortFileData o) {
SortFileData that = o;
if(this.getIndex() > that.getIndex()) return 1;
else if(this.getIndex() < that.getIndex()) return -1;
return 0;
}
}
//工作線程
class Worker implements Runnable{
private FileStruct struct;
private int index;
private List listfile;
private DsuQuery query;
Worker(int index, List listfile){
this.index = index;
this.listfile = listfile;
}
public void setQuery(DsuQuery query) {
this.query = query;
}
@Override
public void run() {
query = new DsuQuery();
for (SortFileData file:listfile) {
byte[] data = Decode.decode(query.queryOnce(file.getFilestruct()),file.getFilestruct());
//byte[] data =new byte[0];
file.setData(data);
container.offer(file);
}
waitForComplete();
}
}
//合并線程,取出數據,并合并寫入文件
class Combine implements Runnable{
@Override
public void run() {
log.info("container size is "+ container.size());
while(!container.isEmpty()){
SortFileData result = container.poll();
writer.write(result.data);
//log.info("start write size is "+ result.data.length);
}
oneTimes.reset();
}
}
public static void main(String[] args) throws IOException {
List list = new ArrayList();
for(int i = 0 ; i < 14; i++){
FileStruct f = new FileStruct();
f.fileName = "test";
f.start = i;
f.length = 1;
f.timestab=i;
list.add(f);
}
LocalFileWriter filewriter = new LocalFileWriter(1);
DownLoadTask down=new DownLoadTask(list,filewriter);
down.run();
filewriter.close();
}
}