volatile
? Java內(nèi)存模型都是圍繞著原子性预厌、有序性和可見性展開的带饱,為了在適當?shù)膱龊狭诘欤_保線程間的有序性、可見性和原子性辜妓。Java使用了一些特殊的操作或者關鍵字來申明酥泛、告訴虛擬機,在這個地方嫌拣,要尤其注意,不能隨意變動優(yōu)化目標指令呆躲。關鍵字volatile就是其中之一异逐,用于保持線程之間的。
? 但是同時應該注意的是插掂,volatile并不能當鎖用灰瞻,他無法保證共享變量操作的原子性腥例。比如下面的例子,通過volatile是無法保證i++的原子性操作的:
01 static volatile int i=0;
02 public static class PlusTask implements Runnable{
03 @Override
04 public void run() {
05 for(int k=0;k<10000;k++)
06 i++;
07 }
08 }
09
10 public static void main(String[] args) throws InterruptedException {
11 Thread[] threads=new Thread[10];
12 for(int i=0;i<10;i++){
13 threads[i]=new Thread(new PlusTask());
14 threads[i].start();
15 }
16 for(int i=0;i<10;i++){
17 threads[i].join();
18 }
19
20 System.out.println(i);
21 }
? 執(zhí)行上述代碼酝润,如果第6行i++是原子性的燎竖,那么最終的值應該是100000(10個線程各累加10000次)。但實際上要销,上述代碼的輸出總是會小于100000构回。
重點
線程組
可以通過線程組對不同的線程分批管理
比如:
01 public class ThreadGroupName implements Runnable {
02 public static void main(String[] args) {
03 ThreadGroup tg = new ThreadGroup("PrintGroup");
04 Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
05 Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
06 t1.start();
07 t2.start();
08 System.out.println(tg.activeCount());
09 tg.list();
10 }
11
12 @Override
13 public void run() {
14 String groupAndName=Thread.currentThread().getThreadGroup().getName()
15 + "-" + Thread.currentThread().getName();
16 while (true) {
17 System.out.println("I am " + groupAndName);
18 try {
19 Thread.sleep(3000);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 }
24 }
25 }
通過創(chuàng)建一個名為“PrintGroup”的線程組,并將T1和T2兩個線程加入這個組中浑塞。第8借跪、9兩行,展示了線程組的兩個重要的功能酌壕,activeCount()可以獲得活動線程的總數(shù)掏愁,但由于線程是動態(tài)的,因此這個值只是一個估計值卵牍,無法確定精確果港,list()方法可以打印這個線程組中所有的線程信息,對調試有一定幫助辽慕。代碼中第4京腥、5兩行創(chuàng)建了兩個線程,使用Thread的構造函數(shù)溅蛉,指定線程所屬的線程組公浪,將線程和線程組關聯(lián)起來。
線程組還有一個值得注意的方法stop()船侧,它會停止線程組中所有的線程欠气。這看起來是一個很方便的功能,但是它會遇到和Thread.stop()相同的問題(終止業(yè)務邏輯沒有處理完的線程)镜撩,因此使用時也需要格外謹慎预柒。
重點
通過ThreadGroup為線程分組,并且為之按照業(yè)務需要命名袁梗,讓線程的名字更有意義宜鸯,慎用線程組的stop(),這會導致破壞線程業(yè)務邏輯的原子性, 線程組的activeCount()可以獲得活動線程的總數(shù)遮怜,并且可以通過list()方法可以打印這個線程組中所有的線程信息淋袖,對調試有一定幫助
守護線程(Daemon)
? 守護線程是一種特殊的線程,就和它的名字一樣锯梁,它是系統(tǒng)的守護者即碗,在后臺默默地完成一些系統(tǒng)性的服務焰情,比如垃圾回收線程、JIT線程就可以理解為守護線程剥懒。,它會完成這個程序應該要完成的業(yè)務操作初橘。。守護線程要守護的對象已經(jīng)不存在了壁却,那么整個應用程序就自然應該結束批狱。因此,。
01 public class DaemonDemo {
02 public static class DaemonT extends Thread{
03 public void run(){
04 while(true){
05 System.out.println("I am alive");
06 try {
07 Thread.sleep(1000);
08 } catch (InterruptedException e) {
09 e.printStackTrace();
10 }
11 }
12 }
13 }
14 public static void main(String[] args) throws InterruptedException {
15 Thread t=new DaemonT();
16 t.setDaemon(true);
17 t.start();
18
19 Thread.sleep(2000);
20 }
21 }
? 上述代碼第16行盐肃,將線程t設置為守護線程爪膊。這里注意,砸王,否則你會得到一個類似以下的異常推盛,告訴你守護線程設置失敗。但是你的程序和線程依然可以正常執(zhí)行谦铃。只是被當做用戶線程而已耘成。因此,如果不小心忽略了下面的異常信息,你就很可能察覺不到這個錯誤。
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.setDaemon(Thread.java:1367)
at geym.conc.ch2.daemon.DaemonDemo.main(DaemonDemo.java:20)
? 在這個例子中行瑞,由于t被設置為守護線程,系統(tǒng)中只有主線程main為用戶線程师妙,因此在main線程休眠2秒后退出時,整個程序也隨之結束屹培。但如果不把線程t設置為守護線程默穴,main線程結束后,t線程還會不停地打印褪秀,永遠不會結束蓄诽。
重點
設置守護線程必須在線程start()之前設置,否則會得到以下異常,導致守護線程失效媒吗,但是不影響主線程執(zhí)行仑氛。
java.lang.IllegalThreadStateException
線程優(yōu)先級
在Java中,使用1到10表示線程優(yōu)先級蝴猪。一般可以使用內(nèi)置的三個靜態(tài)標量表示:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 1
數(shù)字越大則優(yōu)先級越高调衰,但有效范圍在1到10之間。下面的代碼展示了優(yōu)先級的作用自阱。高優(yōu)先級的線程傾向于更快地完成嚎莉。
01 public class PriorityDemo {
02 public static class HightPriority extends Thread{
03 static int count=0;
04 public void run(){
05 while(true){
06 synchronized(PriorityDemo.class){
07 count++;
08 if(count>10000000){
09 System.out.println("HightPriority is complete");
10 break;
11 }
12 }
13 }
14 }
15 }
16 public static class LowPriority extends Thread{
17 static int count=0;
18 public void run(){
19 while(true){
20 synchronized(PriorityDemo.class){
21 count++;
22 if(count>10000000){
23 System.out.println("LowPriority is complete");
24 break;
25 }
26 }
27 }
28 }
29 }
30
31 public static void main(String[] args) throws InterruptedException {
32 Thread high=new HightPriority();
33 LowPriority low=new LowPriority();
34 high.setPriority(Thread.MAX_PRIORITY);
35 low.setPriority(Thread.MIN_PRIORITY);
36 low.start();
37 high.start();
38 }
39 }
? 上述代碼定義兩個線程,分別為HightPriority設置為高優(yōu)先級沛豌,LowPriority為低優(yōu)先級趋箩。讓它們完成相同的工作,也就是把count從0加到10000000加派。完成后叫确,打印信息給一個提示,這樣我們就知道誰先完成工作了芍锦。這里要注意竹勉,在對count累加前,我們使用synchronized產(chǎn)生了一次資源競爭娄琉。目的是使得優(yōu)先級的差異表現(xiàn)得更為明顯次乓。
? 大家可以嘗試執(zhí)行上述代碼,可以看到孽水,高優(yōu)先級的線程在大部分情況下票腰,都會首先完成任務(就這段代碼而言,試運行多次女气,HightPriority總是比LowPriority快杏慰,但這不能保證在所有情況下,一定都是這樣)炼鞠。
重點
可以通過通過setPriority()設置線程的優(yōu)先級缘滥,從0到10,優(yōu)先級越高越容易搶占資源,但這不是絕對的簇搅,只是說理論上搶占共享資源的幾率更高完域。
synchronized
? Java 內(nèi)置的管程方案(synchronized)使用簡單,synchronized 關鍵字修飾的代碼塊瘩将,在編譯期會自動生成相關加鎖和解鎖的代碼吟税,但是僅支持一個條件變量;而 Java SDK 并發(fā)包實現(xiàn)的管程支持多個條件變量姿现,不過并發(fā)包里的鎖肠仪,需要開發(fā)人員自己進行加鎖和解鎖操作。
? 并發(fā)編程里兩大核心問題——互斥和同步备典,都可以由管程來幫你解決异旧。學好管程,理論上所有的并發(fā)問題你都可以解決提佣,并且很多并發(fā)工具類底層都是管程實現(xiàn)的吮蛹,所以學好管程荤崇,就是相當于掌握了一把并發(fā)編程的萬能鑰匙。
? 關鍵字synchronized的作用是實現(xiàn)線程間的同步潮针。它的工作是對同步的代碼加鎖术荤,使得每一次,只能有一個線程進入同步塊每篷,從而保證線程間的安全性
synchronized的多種用法
- 指定加鎖對象:
- 對給定瓣戚,進入同步代碼前要獲得給定對象的鎖。
- 直接作用于實例方法:
- 相當于對當前焦读,進入同步代碼前要獲得當前實例的鎖子库。
- 直接作用于靜態(tài)方法:
- 相當于對當前,進入同步代碼前要獲得當前類的鎖矗晃。
以上是原書給的分類仑嗅,但是我感覺可以按照鎖對象這個維度來看synchronized的用法:
鎖對象
鎖實例對象
1.修飾普通方法,鎖的是this
此時鎖對象是this喧兄,該類的實例對象无畔。
如果實例對象不是一個,那么保證不了共享資源的安全吠冤。
2.synchronized(this)代碼塊
類鎖浑彰,鎖的是內(nèi)存中唯一存在的class對象
1.修飾靜態(tài)方法
2.synchronized(*.class)代碼塊
synchronized的配套使用
synchronized的使用例子
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
for(int j=0;j<10000000;j++){
//類鎖
synchronized(instance){
i++;
}
}
}
當然,上述代碼也可以寫成如下形式拯辙,兩者是等價的:
01 public class AccountingSync2 implements Runnable{
02 static AccountingSync2 instance=new AccountingSync2();
03 static int i=0;
04 public synchronized void increase(){
05 i++;
06 }
07 @Override
08 public void run() {
09 for(int j=0;j<10000000;j++){
10 increase();
11 }
12 }
13 public static void main(String[] args) throws InterruptedException {
14 Thread t1=new Thread(instance);//鎖的是當前實例郭变,但是兩個線程是同一個鎖對象
15 Thread t2=new Thread(instance);
16 t1.start();t2.start();
17 t1.join();t2.join();
18 System.out.println(i);
19 }
20 }
上述代碼中,synchronized關鍵字作用于一個實例方法涯保。這就是說在進入increase()方法前诉濒,線程必須獲得當前對象實例的鎖。在本例中就是instance對象夕春。在這里未荒,我不厭其煩地再次給出main函數(shù)的實現(xiàn),是希望強調第14及志、15行代碼片排,也就是Thread的創(chuàng)建方式。這里使用Runnable接口創(chuàng)建兩個線程速侈,并且這兩個線程都指向同一個Runnable接口實例(instance對象)率寡,這樣才能保證兩個線程在工作時,能夠關注到同一個對象鎖上去倚搬,從而保證線程安全冶共。
一種錯誤的同步方式如下
01 public class AccountingSyncBad implements Runnable{
02 static int i=0;
03 public synchronized void increase(){
04 i++;
05 }
06 @Override
07 public void run() {
08 for(int j=0;j<10000000;j++){
09 increase();
10 }
11 }
12 public static void main(String[] args) throws InterruptedException {
13 Thread t1=new Thread(new AccountingSyncBad());
14 Thread t2=new Thread(new AccountingSyncBad());
15 t1.start();t2.start();
16 t1.join();t2.join();
17 System.out.println(i);
18 }
19 }
? 上述代碼就犯了一個嚴重的錯誤。雖然在第3行的increase()方法中,申明這是一個同步方法捅僵。但是當鎖對象是當前實例家卖,兩個線程分別分配了不同實例,導致鎖對象不同庙楚,不是一把鎖篡九,線程安全無法保證。
但我們只要簡單地修改上述代碼醋奠,就能使其正確執(zhí)行。那就是使用synchronized的第三種用法伊佃,將其作用于靜態(tài)方法窜司。將increase()方法修改如下:
public static synchronized void increase(){
i++;
? 除了用于線程同步、確保線程安全外航揉,synchronized還可以保證線程間的可見性和有序性塞祈。從可見性的角度上講,synchronized可以完全替代volatile的功能帅涂,只是使用上沒有那么方便议薪。就有序性而言,由于synchronized限制每次只有一個線程可以訪問同步塊媳友,因此斯议,無論同步塊內(nèi)的代碼如何被亂序執(zhí)行,只要保證串行語義一致醇锚,那么執(zhí)行結果總是一樣的哼御。而其他訪問線程,又必須在獲得鎖后方能進入代碼塊讀取數(shù)據(jù)焊唬,因此恋昼,它們看到的最終結果并不取決于代碼的執(zhí)行過程,從而有序性問題自然得到了解決(換言之赶促,被synchronized限制的多個線程是串行執(zhí)行的)液肌。
notice:
什么是管程
所謂管程,指的是管理共享變量以及對共享變量的操作過程鸥滨,讓他們支持并發(fā)嗦哆。翻譯為 Java 領域的語言,就是管理類的成員變量和成員方法爵赵,讓這個類是線程安全的吝秕。
Java 采用的是管程技術,synchronized 關鍵字及 wait()空幻、notify()烁峭、notifyAll() 這三個方法都是管程的組成部分。而管程和信號量是等價的,所謂等價指的是用管程能夠實現(xiàn)信號量约郁,也能用信號量實現(xiàn)管程缩挑。但是管程更容易使用,所以 Java 選擇了管程鬓梅。
管程供置,對應的英文是 Monitor,而不是直譯為“監(jiān)視器”绽快。
如果對管程這種技術思想有興趣芥丧,可以繼續(xù)看我的關于這塊的筆記。