文章目錄
線程操作的定義
1、如果一個程序沒有數據競爭,那麼程序的所有執行看起來都是順序一致的,各自按照線程內語義執行即可,JMM對此不需要有額外的描述了。
2、線程間操作指:一個程序執行的操作可被其他線程感知或被其他線程直接影響。
線程間操作:
1、write 要寫的變量以及要寫的值。
2、read 要讀的變量以及可見的寫入值(由此,我們可以確定可見的值)。
3、lock 要鎖定的管程(監視器monitor)。
4、unlock 要解鎖的管程。
5、外部操作(socket等等…)
6、啓動和終止
注意: 所有線程間操作,都存在可見性問題,JMM需要對其進行規範。
對於同步的規則定義
- 對於監視器 m 的解鎖與所有後續操作對於 m 的加鎖同步
- 對 volatile 變量 v 的寫入,與所有其他線程後續對 v 的讀同步
- 對於每個屬性寫入默認值(0, false,null)與每個線程對其進行的操作同步
- 啓動線程的操作與線程中的第一個操作同步
- 線程 T12的最後操作與線程 T1 發現線程 T2 已經結束同步。( isAlive ,join可以判斷線程是否終結)
- 如果線程 T1 中斷了 T2,那麼線程 T1 的中斷操作與其他所有線程發現 T2 被中斷了同步,通過拋出 InterruptedException 異常,或者調用 Thread.interrupted 或 Thread.isInterrupted
Happens-before先行發生原則
happens-before 關係用於描述兩個有衝突的動作之間的順序,如果一個action happends before 另一個action,則第一個操作被第二個操作可見 。
具體的虛擬機實現,有必要確保以下原則的成立:
- 某個線程中的每個動作都 happens-before 該線程中該動作後面的動作。
- 某個管程上的 unlock 動作 happens-before 同一個管程上後續的 lock 動作。
- 對某個 volatile 字段的寫操作 happens-before 每個後續對該 volatile 字段的讀操作。
- 在某個線程對象上調用 start()方法 happens-before 被啓動線程中的任意動作。
- 如果在線程t1中成功執行了t2.join(),則t2中的所有操作對t1可見。
- 如果某個動作 a happens-before 動作 b,且 b happens-before 動作 c,則有 a happens-before c.
補充: 當程序包含兩個沒有被 happens-before 關係排序的衝突訪問時,就稱存在數據競爭。遵守了這個原則,也就意味着有些代碼不能進行重排序,有些數據不能緩存!
final在JMM中的處理
1、final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。僞代碼示例:f = new finalDemo(); 讀取到的 f.x 一定最新,x爲final字段。
public class Demo2Final {
final int x;
int y;
static Demo2Final f;
public Demo2Final(){
x = 3;
y = 4;
}
static void writer(){
f = new Demo2Final();
}
static void reader(){
if (f!=null){
int i = f.x; //一定讀到正確構造版本
int j = f.y; //可能會讀到 默認值0
System.out.println("i=" + i + ", j=" +j);
}
}
}
在多線程中,調用reader方法,f.x一定能讀到在構造函數中的正確賦值,但是f.y卻不一定。
2、如果在構造函數中設置字段後發生讀取,則會看到該final字段分配的值,否則它將看到默認值;僞代碼示例:public finalDemo(){ x = 1; y = x; }; y會等於1;
public class Demo3Final {
final int x;
int y;
static Demo2Final f;
public Demo3Final(){
x = 3;
//#### 重點 語句 #####
y = x; //因爲x被final修飾了,所以可讀到y的正確構造版本
}
static void writer(){
f = new Demo2Final();
}
static void reader(){
if (f!=null){
int i = f.x; //一定讀到正確構造版本
int j = f.y; //也能讀到正確構造版本
System.out.println("i=" + i + ", j=" +j);
}
}
}
在多線程中,reader方法都能讀到x和y的正確值,因爲x被final修飾了,所以可讀到y的正確構造版本。
3、讀取該共享對象的final成員變量之前,先要讀取共享對象。僞代碼示例: r = new ReferenceObj(); k = r.f ; 這兩個操作不能重排序。
4、通常static final是不可以修改的字段 。然而System.in,System.out和System.err是static final字段,遺留原因,必須允許通過set方法改變,我們將這些字段稱爲寫保護,以區別於普通final字段;
Word Tearing字節處理
Java虛擬機實現的一個考慮因素是,每個字段和數組元素被認爲是不同的;對一個字段或元素的更新,不得與任何其他字段或元素的讀取或更新交互。特別是,分別更新字段數組的相鄰元素的兩個線程不得干擾或交互。
有些處理器(尤其是早期的 Alphas 處理器)沒有提供寫單個字節的功能。在這樣的處理器上更新 byte 數組,若只是簡單地讀取整個內容,更新對應的字節,然後將整個內容再寫回內存,將是不合法的。
這個問題有時候被稱爲“字分裂(word tearing)”,更新單個字節有難度的處理器,就需要尋求其它方式來解決問題。
因此,編程人員需要注意,儘量不要對byte[]中的元素進行重新賦值,更不要在多線程程序中這樣做。
volatile關鍵字
多個線程同時訪問一個共享的變量的時候,每個線程的工作內存有這個變量的一個拷貝,變量本身還是保存在共享內存中。
Violate修飾的字段,對這個變量的訪問必須要從共享內存刷新一次。最新的修改寫回共享內存。可以保證字段的可見性。
注意:
Violate修飾的字段絕對不是線程安全的,沒有操作的原子性。
適用場景:
- 一個線程寫,多個線程讀;
- volatile變量的變化很固定
示例代碼:
public class VolatileThread implements Runnable {
private volatile int a= 0;
@Override
public void run() {
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
}
}
public class VolatileTest {
public static void main(String[] args) {
VolatileThread volatileThread = new VolatileThread();
Thread t1 = new Thread(volatileThread);
Thread t2 = new Thread(volatileThread);
Thread t3 = new Thread(volatileThread);
Thread t4 = new Thread(volatileThread);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
輸出結果:
Thread-2----3
Thread-3----4
Thread-1----3
Thread-0----3
Thread-2----5
Thread-3----8
Thread-0----8
Thread-1----8
可以看到變量a雖然用volatile修飾,是線程共享的變量,但是並不是線程安全的,輸出結果是不可預測的。
理解volatile特性的一個好方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。
假設有多個線程分別調用上面程序的3個方法,這個程序在語義上和下面程序等價。
volatile寫的內存語義如下:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀的內存語義如下:
當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
synchronized關鍵字
可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。
例如在上面的VolatileThread示例代碼中,在run方法裏面加上synchronized鎖,那麼就可以保證運行結果,修改後的代碼爲:
public class VolatileThread implements Runnable {
private volatile int a= 0;
@Override
public void run() {
synchronized (this){
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a=a+1;
System.out.println(Thread.currentThread().getName()+"----"+a);
}
}
}
VolatileTest運行結果:
Thread-0----1
Thread-0----2
Thread-3----3
Thread-3----4
Thread-2----5
Thread-2----6
Thread-1----7
Thread-1----8
所以可以知道synchronized鎖住的代碼,同一時間只能有一個線程運行。
Synchronized的類鎖和對象鎖,本質上是兩把鎖,Synchronized加在靜態方法上就是類鎖,加在非靜態的上面就是對象鎖,類鎖實際鎖的是每一個類的class對象。對象鎖鎖的是當前對象實例。
示例代碼:
package com.dongnaoedu.syn;
import com.dongnaoedu.threadstate.SleepUtils;
/**
* 動腦學院-Mark老師
* 創建日期:2017/11/28
* 創建時間: 20:45
* 類鎖和實例鎖
*/
public class InstanceAndClass {
//測試類鎖
private static class TestClassSyn extends Thread{
@Override
public void run() {
System.out.println("TestClass is going...");
synClass();
}
}
//測試類鎖
private static class TestClassSyn2 extends Thread{
@Override
public void run() {
System.out.println("TestClass2 is going...");
synClass2();
}
}
//測試對象鎖
private static class TestInstanceSyn extends Thread{
private InstanceAndClass instanceAndClass;
public TestInstanceSyn(InstanceAndClass instanceAndClass) {
this.instanceAndClass = instanceAndClass;
}
@Override
public void run() {
System.out.println("TestInstance is going..."+instanceAndClass);
instanceAndClass.synInstance();
}
}
//測試對象鎖
private static class TestInstance2Syn implements Runnable{
private InstanceAndClass instanceAndClass;
public TestInstance2Syn(InstanceAndClass instanceAndClass) {
this.instanceAndClass = instanceAndClass;
}
@Override
public void run() {
System.out.println("TestInstance2 is going..."+instanceAndClass);
instanceAndClass.synInstance2();
}
}
//鎖對象的方法
private synchronized void synInstance(){
SleepUtils.second(3);
System.out.println("synInstance is going...");
SleepUtils.second(3);
System.out.println("synInstance ended");
}
//鎖對象的方法
private synchronized void synInstance2(){
SleepUtils.second(3);
System.out.println("synInstance2 going...");
SleepUtils.second(3);
System.out.println("synInstance2 ended");
}
//鎖類的方法
private static synchronized void synClass(){
SleepUtils.second(5);
System.out.println("synClass going...");
SleepUtils.second(5);
}
//鎖類的方法
private static synchronized void synClass2(){
SleepUtils.second(1);
System.out.println("synClass2 going...");
SleepUtils.second(1);
}
public static void main(String[] args) {
InstanceAndClass instanceAndClass = new InstanceAndClass();
Thread t1 = new TestClassSyn();
Thread t4 = new TestClassSyn2();
Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass));
Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass));
t2.start();
t3.start();
SleepUtils.second(1);
t1.start();
t4.start();
}
}
輸出結果:
TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance ended
synClass going...
synInstance2 going...
synInstance2 ended
synClass2 going...
從輸出結果可以看出,類鎖和對象鎖是兩個不同的鎖,並且當一個對象在運行一個加了該對象鎖的方法時,其他線程不能用該對象運行加了該對象鎖的其他方法。如上例代碼中,一個線程運行着synInstance方法,在synInstance運行結束之前,那麼其他線程就不能通過instanceAndClass對象調用synInstance2或者synInstance方法。
補充: 即使是同一個方法,加了對象鎖,如果不是同一個對象去調用,那麼是不會互斥的,因爲synchronized鎖的不是同一個對象,如下面,把main方法改成如下,那麼t2和t3是互不干擾的,不會互斥。
把TestInstance2Syn 在run方法中調用的方法改成synInstance,讓兩個線程都去調用synInstance方法。
private static class TestInstance2Syn implements Runnable{
private InstanceAndClass instanceAndClass;
public TestInstance2Syn(InstanceAndClass instanceAndClass) {
this.instanceAndClass = instanceAndClass;
}
@Override
public void run() {
System.out.println("TestInstance2 is going..."+instanceAndClass);
instanceAndClass.synInstance();
}
}
public static void main(String[] args) {
InstanceAndClass instanceAndClass1 = new InstanceAndClass();
InstanceAndClass instanceAndClass2 = new InstanceAndClass();
Thread t1 = new TestClassSyn();
Thread t4 = new TestClassSyn2();
Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass1));
Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass2));
t2.start();
t3.start();
SleepUtils.second(1);
t1.start();
t4.start();
}
運行結果:
TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@64669643
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@7e437185
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance is going...
synClass going...
synInstance ended
synInstance ended
synClass2 going...
從結果上看,t2和t3是可以併發執行的。所以即使加了synchronized的對象方法,不同對象去調用是不會互斥的,是可以併發執行的。
等待和通知機制
等待方原則:
1、獲取對象鎖
2、如果條件不滿足,調用對象的wait方法,被通知後依然要檢查條件是否滿足
3、條件滿足以後,才能執行相關的業務邏輯
wait方法導致當前線程等待,加入該對象的等待集合中,並且放棄當前持有的對象鎖。
格式:
Synchronized(對象){
While(條件不滿足){
對象.wait()
}
業務邏輯處理
}
通知方原則:
1、獲得對象的鎖;
2、改變條件;
3、通知所有等待在對象的線程
notify/notifyAll方法喚醒一個或所有正在等待這個對象鎖的線程。
格式:
Synchronized(對象){
業務邏輯處理,改變條件
對象.notify/notifyAll
}
示例代碼:
public class BlockingQueueWN<T> {
private List queue = new LinkedList<>();
private final int limit;
public BlockingQueueWN(int limit) {
this.limit = limit;
}
//入隊
public synchronized void enqueue(T item) throws InterruptedException {
while(this.queue.size()==this.limit){
wait();
}
//將數據入隊,可以肯定有出隊的線程正在等待
if (this.queue.size()==0){
notifyAll();
}
this.queue.add(item);
}
//出隊
public synchronized T dequeue() throws InterruptedException {
while(this.queue.size()==0){
wait();
}
if (this.queue.size()==this.limit){
notifyAll();
}
return (T)this.queue.remove(0);
}
}
public class BqTest {
public static void main(String[] args) {
BlockingQueueWN bq = new BlockingQueueWN(10);
Thread threadA = new ThreadPush(bq);
threadA.setName("Push");
Thread threadB = new ThreadPop(bq);
threadB.setName("Pop");
threadB.start();
threadA.start();
}
//推數據入隊列
private static class ThreadPush extends Thread{
BlockingQueueWN<Integer> bq;
public ThreadPush(BlockingQueueWN<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
int i = 20;
while(i>0){
try {
Thread.sleep(1000);
System.out.println(" i="+i+" will push");
bq.enqueue(i--);
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
}
//取數據出隊列
private static class ThreadPop extends Thread{
BlockingQueueWN<Integer> bq;
public ThreadPop(BlockingQueueWN<Integer> bq) {
this.bq = bq;
}
@Override
public void run() {
while(true){
try {
System.out.println(Thread.currentThread().getName()
+" will pop.....");
Integer i = bq.dequeue();
System.out.println(" i="+i.intValue()+" alread pop");
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
}
}
注意:
1、雖然會wait自動解鎖,但是對順序有要求, 如果在notify被調用之後,纔開始wait方法的調用,線程會永遠處於WAITING狀態。
2、這些方法只能由同一對象鎖的持有者線程調用,也就是寫在同步塊裏面,否則會拋出IllegalMonitorStateException異常。
join方法
線程A,執行了thread.join(),線程A等待thread線程終止了以後,A在join後面的語句纔會繼續執行.
示例代碼:
public class JoinTest {
static class CutInLine implements Runnable{
private Thread thread;
public CutInLine(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
//在被插隊的線程裏,調用一下插隊線程的join方法
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" will work");
}
}
public static void main(String[] args) {
Thread previous = Thread.currentThread();
for(int i=0;i<10;i++){
Thread thread =
new Thread(new CutInLine(previous),String.valueOf(i));
System.out.println(previous.getId()+" cut in the thread:"+thread.getName());
thread.start();
previous = thread;
}
}
}
運行結果:
1 cut in the thread:0
12 cut in the thread:1
13 cut in the thread:2
14 cut in the thread:3
15 cut in the thread:4
16 cut in the thread:5
17 cut in the thread:6
18 cut in the thread:7
19 cut in the thread:8
20 cut in the thread:9
0 will work
1 will work
2 will work
3 will work
4 will work
5 will work
6 will work
7 will work
8 will work
9 will work
這裏在main方法啓動的10個線程中,每個線程都是得在上一個線程執行完成(終止)之後,纔會執行自己的輸出。
park/unpark機制
線程調用park則等待“許可”,處於等待狀態,unpark方法爲指定線程提供“許可(permit)”,線程繼續運行。
補充:調用unpark之後,再調用park,線程會直接運行。
提前調用的unpark不疊加,連續多次調用unpark後,第一次調用park後會拿到“許可”直接運行,後續調用會進入等待。
代碼示例:
package com.dongnao.concurrent.period2;
import java.util.concurrent.locks.LockSupport;
public class Demo9_ParkUnpark {
public static void main(String args[]) throws Exception {
Demo9_ParkUnpark demo = new Demo9_ParkUnpark();
demo.test1_normal();
//demo.test2_DeadLock();
}
public static Object iceCream = null;
/** 正常的park/unpark */
public void test1_normal() throws Exception {
//開啓一個線程,代表小朋友
Thread consumerThread = new Thread(new Runnable() {
@Override
public void run() {
while (iceCream == null) { // 若沒有冰激凌
System.out.println("沒有冰激凌,小朋友不開心,等待...");
LockSupport.park();
}
System.out.println("小朋友買到冰激凌,開心回家");
}
});
consumerThread.start();
Thread.sleep(3000L); // 3秒之後
iceCream = new Object(); //店員做了一個冰激凌
LockSupport.unpark(consumerThread); //通知小朋友
System.out.println("通知小朋友");
}
/** 死鎖的park/unpark */
public void test2_DeadLock() throws Exception {
//開啓一個線程,代表小朋友
Thread consumerThread = new Thread(new Runnable() {
@Override
public void run() {
if (iceCream == null) { // 若沒有冰激凌
System.out.println("沒有冰激凌,小朋友不開心,等待...");
synchronized (this) { // 若拿到鎖
LockSupport.park(); //執行park
}
}
System.out.println("小朋友買到冰激凌,開心回家");
}
});
consumerThread.start();
Thread.sleep(3000L); // 3秒之後
iceCream = new Object(); //店員做了一個冰激凌
synchronized (this) { // 爭取到鎖以後,才能恢復consumerThread
LockSupport.unpark(consumerThread);
}
System.out.println("通知小朋友");
}
}
僞喚醒
注意:
之前代碼中用 if 語句來判斷,是否進入等待狀態,這樣的做法是錯誤的!
官方建議應該在循環中檢查等待條件,原因是處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。
僞喚醒是指線程並非因爲notify、notifyall、unpark等api調用而意外喚醒,是更底層原因導致的。