線程的狀態
有說5種的,有說6種的
5種的,從操作系統層面來講
- 初始狀態:也就是語言層面創建了線程對象,還未與操作系統線程關聯。Java中也就是new了一個線程,還未調用。
- 可運行狀態:(就緒狀態)也就是該線程已經被創建,也就是java中調用了start()方法,已經與操作系統關聯了,已經可以被CPU調度了。
- 運行狀態:指線程被CPU調度了,獲取了CPU的時間片,已經在執行代碼中了(注意:當CPU的時間片用完,也就是CPU自己的調度,切換線程上下文,該線程就會從運行狀態重新到可運行狀態,等待下一次被CPU調度,獲取時間片)
- 阻塞狀態:比如sleep了,wait了啥的,就會到阻塞狀態,此時CPU就不會分時間片給這些線程。而是會選擇分給那些可運行狀態的線程。等sleep完了,再回到可運行狀態
- 終止狀態:代碼運行完畢生命周期結束
6種狀態的說法,根據Java Api層面描述
根據Thread.State枚舉,有六種狀態,new、runnable、blocked、waiting、time_waiting、terminated
源碼:
public enum State {/*** Thread state for a thread which has not yet started.* 尚未啟動的線程的線程狀態。*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.* 可運行線程的線程狀態。處于可運行狀態的線程正在Java虛擬機中執行,* 但它可能正在等待來自操作系統(如處理器)的其他資源。*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.* 等待監視器鎖的線程的線程狀態。處于阻塞狀態的線程正在等待監視器鎖* 進入同步blockmethod或在調用后重新進入同步blockmethod*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* 等待線程的線程狀態。線程由于調用以下方法之一而處于等待狀態:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.* 處于等待狀態的線程正在等待另一個線程執行特定的操作** For example, a thread that has called <tt>Object.wait()</tt>* on an object is waiting for another thread to call* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on* that object. A thread that has called <tt>Thread.join()</tt>* is waiting for a specified thread to terminate.* 例如,一個線程在一個對象上調用了<tt> object. wait()<tt>,* 正在等待另一個線程在該對象上調用<tt> object. notify()<tt>或<tt> object. notifyall ()<tt>。* 調用了<tt> thread .join()<tt>的線程正在等待指定的線程終止。*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* 指定等待時間的等待線程的線程狀態。線程處于定時等待狀態,因為調用了以下方法之一,并指定了正等待時間:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.* 終止線程的線程狀態。線程已完成執行。*/TERMINATED;}
- NEW 線程剛被創建,但是還沒有調用 start() 方法
- RUNNABLE 當調用了 start() 方法之后,注意,Java API 層面的 RUNNABLE 狀態涵蓋了 操作系統 層面的
【可運行狀態】、【運行狀態】和【阻塞狀態】(由于 BIO[也就是io讀取文件的阻塞,java也是認為在運行。而不像sleep這樣的阻塞] 導致的線程阻塞,在 Java 里無法區分,仍然認為
是可運行) - BLOCKED , WAITING , TIMED_WAITING 都是 Java API 層面對【阻塞狀態】的細分,后面會在狀態轉換一節
詳述 - TERMINATED 當線程代碼運行結束
運行狀態、可運行狀態、阻塞狀態,在java中都屬于runnable狀態,因為不管你怎么樣,反正就是可以被CPU調度的狀態
六種狀態的例子
public static void main( String[] args ) throws InterruptedException {Thread t1 = new Thread() {@Overridepublic void run() {log.debug("running...");}};t1.setName("t1");Thread t2 = new Thread() {@Overridepublic void run() {while (true) {//runnable:分到時間片,未分到時間片,io阻塞}}};t2.setName("t2");t2.start();Thread t3 = new Thread() {@Overridepublic void run() {log.debug("running...");}};t3.setName("t3");t3.start();Thread t4 = new Thread() {@Overridepublic void run() {synchronized (JUCStudy.class) {try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}};t4.setName("t4");t4.start();Thread t5 = new Thread() {@Overridepublic void run() {try {t2.join();//要等待t2,但是t2死循環,或者是不知道時間,所以就是waiting,沒有時間的等待} catch (InterruptedException e) {e.printStackTrace();}}};t5.setName("t5");t5.start();Thread t6 = new Thread() {@Overridepublic void run() {synchronized (JUCStudy.class) {//因為t4在用鎖,t6就拿不到,就會到blocked狀態try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}}};t6.setName("t6");t6.start();log.debug("t1 state {}",t1.getState());//NEW (新建線程對象,只是new了,還沒start,也無法被CPU分配時間片)log.debug("t2 state {}",t2.getState());//RUNNABLE (可運行狀態,能被CPU分配時間片就是可運行狀態)log.debug("t3 state {}",t3.getState());//TERMINATED (線程正常執行結束)log.debug("t4 state {}",t4.getState());//TIMED_WAITING (睡眠等有時間的等待)log.debug("t5 state {}",t5.getState());//WAITING (使用了join方法等,需要等待其他線程結束,不知道時間的等待)log.debug("t6 state {}",t6.getState());//BLOCKED (需要對象鎖,而對象鎖被其他線程用了,還未釋放鎖,陷入阻塞)}
共享模型
正常來講,單線程訪問共享資源是沒有問題的,因為他們不會交替訪問。但是多線程交替訪問共享資源,就有可能出現類似"臟讀"的情況,比如線程一要計算用共享資源從1累加到100,結果還沒算出來就沒有時間片了,而線程二來讀取這個共享資源,就會出現數據錯誤。
一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區
static int counter = 0;
static void increment()
// 臨界區
{counter++;
}
static void decrement()
// 臨界區
{counter--;
}
ok,上面是臨界區對共享資源的一種競爭,要怎么解決這樣的問題呢?
synchronized 解決方案
應用之互斥
為了避免臨界區的競態條件發生,有多種手段可以達到目的
- 阻塞式的解決方案: synchronized、Lock
- 非阻塞式的解決方法:原子變量
本次課使用阻塞式的解決方案:synchronized,來解決上述問題,即俗稱的【對象鎖】,它采用互斥的方式讓同一
時刻至多只有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖
的線程可以安全的執行臨界區內的代碼,不用擔心線程上下文切換
注意
雖然 java 中互斥和同步都可以采用 synchronized 關鍵字來完成,但它們還是有區別的:
- 互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼
- 同步是由于線程執行的先后、順序不同、需要一個線程等待其它線程運行到某個點
synchonized語法
synchronized (對象){//臨界區:也就是要對共享資源讀取的代碼區域
}
拿了所鎖之后,不論有沒有時間片,都不會釋放鎖,只有整個臨界區運行我完畢了才釋放鎖。釋放之后會喚醒其他所有阻塞、等待鎖釋放的線程,至于哪個線程來爭得到這把鎖,就由CPU來決定了
例子
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room) {counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);
}
- 如果把 synchronized(obj) 放在 for 循環的外面,如何理解?-- 原子性(原來是鎖住了++和–的代碼,也就是要么加一次要么減一次,而放在外面就變成了,一次性先加完,再一次性減完)
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 會怎樣運作?–鎖對象 (肯定要同一把鎖啊,不同鎖,我等你干什么)
- 如果 t1 synchronized(obj) 而 t2 沒有加會怎么樣?如何理解?-- 鎖對象 (不用鎖我就執行咯,你有所沒鎖關我啥事,鎖要多個地方用,只給一個臨界區上鎖沒啥意義)
鎖的面向對象的改進
說白了就是把鎖弄在對象里面
里面的方法加鎖,鎖就是對象自己
/*** Hello world!*/
@Slf4j
public class JUCStudy {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.increment();}}, "t1");Thread t2 = new Thread(() -> {for (int j = 0; j < 5000; j++) {room.decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("count: {}", room.get());}
}
class Room {int value = 0;public void increment() {synchronized (this) {value++;}}public void decrement() {synchronized (this) {value--;}}public int get() {synchronized (this) {return value;}}
}
進一步優化,synchroized其他語法
class Test{public synchronized void test() {}
}
等價于
class Test{public void test() {synchronized(this) {}}
}
class Test{public synchronized static void test() {}
}
等價于
class Test{public static void test() {synchronized(Test.class) {}}
}
不加synchronized是無法保證方法的原子性的
網上所謂的"線程八鎖"
其實沒啥意思,就8個例子,都很簡單
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {log.debug("1");}public synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}public void c() {log.debug("3");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();new Thread(()->{ n1.c(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n1.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
@Slf4j(topic = "c.Number")
class Number{public static synchronized void a() {sleep(1);log.debug("1");}public static synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{ n1.a(); }).start();new Thread(()->{ n2.b(); }).start();
}
變量的線程安全分析
成員變量和靜態變量是否線程安全?
- 如果它們沒有共享,則線程安全
- 如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
局部變量是否線程安全?
- 局部變量是線程安全的
- 但局部變量引用的對象則未必
- 如果該對象沒有逃離方法的作用訪問,它是線程安全的
- 如果該對象逃離方法的作用范圍,需要考慮線程安全
先看一個成員變量的例子
@Slf4j
public class JUCStudy {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {ThreadUnsafe test = new ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}
}class ThreadUnsafe {ArrayList<String> list = new ArrayList<>();public void method1(int loopNumber) {for (int i = 0; i < loopNumber; i++) {// { 臨界區, 會產生競態條件method2();method3();// } 臨界區}}private void method2() {list.add("1");}private void method3() {list.remove(0);}
}
說白了就是兩個線程,一起去操作同一個對象里的成員變量,這不很明顯,這個成員變量就相當于是共享變量了,一起對其進行讀寫操作,肯定會出現線程安全問題,這里是一個線程執行方法還沒加完,另一個線程就來執行減的操作,就會出現IndexOutOfBoundsException,下標越界異常。
很明顯,就是因為無論是method2還是method3都是對同一個變量進行操作,而且還是同一個對象。
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748)
常見的線程安全類
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的類
這里說它們是線程安全的是指,多個線程調用它們同一個實例的某個方法時,是線程安全的。也可以理解為:
(說白了就是上述說的安全類,new一個對象后里面的方法都是加鎖的,所以就線程安全了嘛,見下面put()方法源碼:)
public static void main(String[] args) {Hashtable table = new Hashtable();new Thread(()->{table.put("key", "value1");}).start();new Thread(()->{table.put("key", "value2");}).start();
}
put()方法源碼
public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null;}
- 它們的每個方法是原子的
- 但注意它們多個方法的組合不是原子的,見后面分析
重點來了!!!->但注意它們多個方法的組合不是原子的
線程安全類方法的組合
分析下面代碼是否線程安全?(不安全)
Hashtable table = new Hashtable();
// 線程1,線程2
if( table.get("key") == null) {table.put("key", value);
}
這不很明顯不安全嘛,很好理解,兩個方法里面的代碼都是線程安全的,這是因為給兩個方法都上了鎖,但是兩個方法外面那一層并沒有加鎖,所以這兩個方法一起用,多個線程來執行的話,哪個線程先執行哪個方法的順序就成了問題。所以要想他們的組合也是保持原子性,那肯定就得在外層也加個鎖嘍
不可變類線程安全性
String、Integer 等都是不可變類,因為其內部的狀態不可以改變,因此它們的方法都是線程安全的
或許有疑問,String 有 replace,substring 等方法【可以】改變值啊,那么這些方法又是如何保證線程安全的呢?說白了,其實這些類的方法,里面都是創建了新的String、Integer對象,所以內部其實就是不可變的,變了其實就是new了新對象,所以是說他們的內容方法都是線程安全的。
例子
背景:servlet實例是運行在tomcat里面,所以多個現場是共用這一個servlet的
public class MyServlet extends HttpServlet {// 是否安全?不安全,安全的是HashTableMap<String,Object> map = new HashMap<>();// 是否安全?安全String S1 = "...";// 是否安全?安全---不可變即安全final String S2 = "...";// 是否安全?不安全Date D1 = new Date();// 是否安全?不安全,只是說這個對象不變,但是里面的屬性還是可能改變的。就跟前面有提到過的,一個對象的外部安全,但是里面的屬性能變。final Date D2 = new Date();public void doGet(HttpServletRequest request, HttpServletResponse response) {// 使用上述變量}
}
Monitor概念
Java對象頭
簡單了解一下,里面有mark word和klass word等,一般是這兩個,里面有字節,指向對應的地址,就知道java類對應到底是什么類。
Monitor(synchorized底層原理的鎖)
Monitor翻譯:監視器、管程
底層原理全是概念,好復雜,頂多用來應對面試,不想記。
輕量級鎖
輕量級鎖的應用場景:如果一個對象有多線程訪問,但多線程訪問的時間是錯開的(也就是沒有競爭),那么可以使用輕量級鎖來優化
輕量級鎖對使用者是透明的,即語法仍是synchronized
假設有兩個方法同步塊,利用同一個對象加鎖
static final Object obj = new Object();public static void method1(){synchronized (obj){//同步塊Amethod2();}}public static void method2() {synchronized (obj){//同步塊B}
}
聽不懂…
鎖膨脹(指的是一個過程,由輕量級鎖變成重量級鎖)
如果在嘗試加輕量級鎖的過程中,CAS操作無法成功,這是一種情況就是有其他線程為此對象加上了輕量級鎖(有競爭),這是需要進行鎖膨脹,將輕量級鎖變為重量級鎖。也就是最簡單意義上的鎖。
static Object obj = new Object();
public static void method1() {synchronized (obj) {//同步塊}
}
聽不懂…
自旋優化
重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時,當前線程就可以避免阻塞
聽不懂…
偏向鎖
看不懂…
上面說看不懂的,難的都是理論知識,頂多應付面試。實際項目開發用不上。需要的可以自己去搜相關知識。