前言
??在之前我們講述了Java的線程模型,理解清楚了過后再我們使用的過程中才能得心應(yīng)手,防止不必要的錯誤出現(xiàn),多線程錯誤是很難復(fù)現(xiàn)的錯誤,一定要小心謹(jǐn)慎的使用。
??同時,這里講的是線程間交互,同步的問題,如果線程間不存在交互,各自用自己的局部變量工作,也不存在這些問題了。
共享變量
假如有一下場景,兩個線程依次對某一個成員變量進行操作,會出現(xiàn)什么問題呢?
public class Main {
static int num;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
num = j;
}
System.out.println(Thread.currentThread().getName() + ": num = " + num);
}
}.start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main, num = " + num);
}
}
我們可以發(fā)現(xiàn)num的最終值是999,而且每個線程的值都是999。我們發(fā)現(xiàn)這里并沒有出現(xiàn)多線程的錯誤,其實是因為在Java里面基本類型的賦值操作是原子性的,上一節(jié)我們講過。那假如吧賦值操作改為非原子性的操作呢?,比如改為num++,會怎么樣呢?
for (int j = 0; j < 1000; j++) {
num ++;
}
//輸出
Thread-0: num = 1889
Thread-1: num = 2000
Thread-2: num = 3000
Thread-4: num = 4000
Thread-3: num = 5000
Thread-5: num = 6000
Thread-9: num = 7000
Thread-8: num = 8046
Thread-7: num = 8046
Thread-6: num = 9046
main, num = 9046
具體數(shù)據(jù)肯定有差別,發(fā)現(xiàn)這里出現(xiàn)了問題,結(jié)果并不是10000,這是由于num++是非原子操作,它包括3個操作:讀取num的值,進行加1操作,把新值寫入num。假如當(dāng)前線程先讀取了num的值放入工作內(nèi)存,然后線程這是被切換到了另一個線程,另一個線程修改了num的值;在回到當(dāng)前線程繼續(xù)執(zhí)行,這是的num就不是最新的值,所以導(dǎo)致出錯。
那我們加上volatile關(guān)鍵字會怎么樣呢?
volatile static int num;
其實不用看結(jié)果我們也能知道volatile也沒用,它不能保證原子性,那么我們該怎么保證同步呢。這就輪到synchronized登場了。
synchronized
我們把Thread的run改成如下形式
new Thread() {
@Override
public void run() {
synchronized (Main.class) {
for (int j = 0; j < 1000; j++) {
num++;
}
System.out.println(Thread.currentThread().getName() + ": num = " + num);
}
}
}.start();
//輸出
Thread-0: num = 1000
Thread-3: num = 2000
Thread-1: num = 3000
Thread-4: num = 4000
Thread-6: num = 5000
Thread-2: num = 6000
Thread-7: num = 7000
Thread-8: num = 8000
Thread-5: num = 9000
Thread-9: num = 10000
main, num = 10000
這就能正確的同步,synchronized實際上是一種鎖機制,括號里面是鎖的內(nèi)容,這里鎖的是當(dāng)前類的.class對象,對象在當(dāng)前進程中是單例的,只有一個。當(dāng)一個線程獲取到該鎖后,其他線程再去嘗試獲取鎖就會等待,直到持有鎖的線程運行完畢,自動釋放鎖后,再去嘗試獲取該鎖。即由synchronized保護的代碼塊每次只能由一個線程運行,這樣就保證了同步性。synchronized可作用于一段代碼或方法,既可以保證可見性,又能夠保證原子性。
線程啟動相關(guān)
在Java中,我們可以利用Thread的子類啟動線程,也可以實現(xiàn)Runnable的接口來啟動線程;Thread本質(zhì)也是實現(xiàn)了Runnable;
public class MyThread extends Thread{
@Override
public void run(){
//TODO
}
}
new MyThread().start();
public class MyRunnable implements Runnable{
@Override
public void run(){
//TODO
}
}
new MyRunnable().start();
當(dāng)然,還可以用ThreadFactory來啟動線程
ThreadFactory factory = Executors.defaultThreadFactory();
factory.newThread(new Runnable() {
@Override
public void run() {
//TODO
}
}).start();
還有一個帶返回的線程Callable+FutureTask
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);
}
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
//獲取返回值
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
注意啟動線程的操作是start()方法,而不是run()方法,以run()啟動的線程實際上是串行執(zhí)行的代碼,即直接執(zhí)行線程對象的run()方法,而不是啟動一個線程
總結(jié)
我們可以看出volatile雖然具有可見性但是并不能保證原子性。
性能方面,synchronized關(guān)鍵字是防止多個線程同時執(zhí)行一段代碼,就會影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized。
但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因為volatile關(guān)鍵字無法保證操作的原子性。
線程其他知識
線程暫停
線程有一個sleep的靜態(tài)方法用于暫停線程,并且會阻塞,不會釋放已經(jīng)持有的鎖。單位:毫秒
Thread.sleep(1000);
線程等待
等待隊列
所有的實例對象都有一個等待隊列,它是在實例的wait方法執(zhí)行后停止操作的線程的隊列。在執(zhí)行完wait方法后,線程便會暫停操作,進入等待隊列。除非發(fā)生以下其中一種情況,否則將一直在等待隊列中休眠。反之將會退出隊列。等待隊列是一個虛擬的概念,它既不是實例中的字段,也不是用于獲取正在實例上等待的線程列表的方法。
· 有其他線程的notify方法來喚醒線程
· 有其他線程的notifyAll方法來喚醒線程
· 有其他線程的interrupt方法來喚醒線程
· wait方法超時
將線程放入等待隊列
Object obj =new Object();
new Thread(){
@Override
public void run(){
synchronized(obj){
.....
obj.wait();//線程將進入等待隊列
.....
}
}
}.start();
假如是鎖當(dāng)前對象,則wait()等同于this.wait()。
public clsss TestWait{
public static void main(String[] args){
TestWait test = new TestWait();
new Thread(){
@Override
public void run(){
test.testWait();
}
}.start();
}
public void testWait(){
synchronized(TestWait.this){
try {
wait();//等同于this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意執(zhí)行wait方法必須持有鎖,線程進入等待隊列必須釋放鎖,這也是跟sleep不同的地方,sleep會阻塞不會釋放鎖。

從等待隊列中取出線程
notify方法會將等待隊列中的一個線程取出。
public class TestWait {
public static void main(String[] args) {
TestWait test = new TestWait();
new Thread() {
@Override
public void run() {
test.testWait();
}
}.start();
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.testNotify();
}
}.start();
}
public void testWait() {
synchronized (TestWait.this) {
try {
System.out.println("進入等待隊列" + System.currentTimeMillis() / 1000);
wait();//等同于this.wait();
System.out.println("從等待隊列中恢復(fù)執(zhí)行" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void testNotify() {
synchronized (TestWait.this) {
notify();//等同于this.notify();
}
}
}
//輸出
進入等待隊列1524497926
從等待隊列中恢復(fù)執(zhí)行1524497927
那么第一個線程確實被喚醒了,并且時間差一秒。

注意B執(zhí)行notify后A不會立即運行,而是要等B運行完后釋放鎖,A這時候去重新獲取鎖后,才能運行。
取出等待隊列的所有線程
notify只能喚醒一個線程,notify會喚醒所有在等待隊列中的線程。其他跟notify一樣。注意notify,notifyAll,wait均需要獲取鎖才能使用,否則會拋出異常java.lang. IllegalMonitorStateException
由于使用notify只能喚醒一個線程,所以處理速度比notifyAll快;但使用notify時,如果處理不好,程序邊可能終止。一般來說,使用notifyAll的代碼比notify要更為健壯
區(qū)別
wait,notify,notifyAll是Object類的方法,而不是Thread類的固有方法;sleep是Thread類的靜態(tài)方法。但由于Oeject是所有類的父類,所以wait,notify,notifyAll也可以通過Thread使用,但不建議。
線程狀態(tài)切換圖
