2七、多線程 同步的原理 多線程
一. 實現多線程 1. 虛假的多線程
例1: 函數
public class TestThread
{
int i=0, j=0;
public void go(int flag)
{
while(true)
{
try{ Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
if(flag==0) i++;
System.out.println("i=" + i);
}
else
{
j++;
System.out.println("j=" + j);
}
}
}
public static void main(String[] args)
{
new TestThread().go(0);
new TestThread().go(1);
}
} 優化
上面程序的運行結果爲:
i=1 i=2 i=3 。。。
結果將一直打印出I的值。咱們的意圖是當在while循環中調用sleep()時,另外一個線程就將起動,打印出j的值,但結果卻並非這樣。關於sleep()爲何不會出現咱們預想的結果,在下面將講到。 this
探討Java多線程及其同步的實現 spa
2. 實現多線程
經過繼承class Thread或實現Runnable接口,咱們能夠實現多線程
2.1 經過繼承class Thread實現多線程
class Thread中有兩個最重要的函數run()和start()。
1) run()函數必須進行覆寫,把要在多個線程中並行處理的代碼放到這個函數中。
2) 雖然run()函數實現了多個線程的並行處理,但咱們不能直接調用run()函數,而是經過調用start()函數來調用run()函數。在調用start()的時候,start()函數會首先進行與多線程相關的初始化(這也是爲何不能直接調用run()函數的緣由),而後再調用run()函數。
例2: 線程
public class TestThread extends Thread
{
private static int threadCount = 0;
private int threadNum = ++threadCount;
private int i = 5;
public void run()
{
while(true)
{
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
System.out.println("Thread " + threadNum +
" = " + i);
if(--i==0) return;
}
}
public static void main(String[] args)
{
for(int i=0; i<5; i++)
new TestThread().start();
}
} 對象
運行結果爲:
Thread 1 = 5
Thread 2 = 5
Thread 3 = 5
Thread 4 = 5
Thread 5 = 5
Thread 1 = 4
Thread 2 = 4
Thread 3 = 4
Thread 4 = 4
Thread 1 = 3
Thread 2 = 3
Thread 5 = 4
Thread 3 = 3
Thread 4 = 3
Thread 1 = 2
Thread 2 = 2
Thread 5 = 3
Thread 3 = 2
Thread 4 = 2
Thread 1 = 1
Thread 2 = 1
Thread 5 = 2
Thread 3 = 1
Thread 4 = 1
Thread 5 = 1
從結果可見,例2能實現多線程的並行處理。
**:在上面的例子中,咱們只用new產生Thread對象,並無用reference來記錄所產生的Thread對象。根據垃圾回收機制,當一個對象沒有被reference引用時,它將被回收。可是垃圾回收機制對Thread對象「不成立」。由於每個Thread都會進行註冊動做,因此即便咱們在產生Thread對象時沒有指定一個reference指向這個對象,實際上也會在某個地方有個指向該對象的reference,因此垃圾回收器沒法回收它們。
繼承
3) 經過Thread的子類產生的線程對象是不一樣對象的線程 接口
class TestSynchronized extends Thread
{
public TestSynchronized(String name)
{
super(name);
}
public synchronized static void prt()
{
for(int i=10; i<20; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
public synchronized void run()
{
for(int i=0; i<3; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
}
public class TestThread
{
public static void main(String[] args)
{
TestSynchronized t1 = new TestSynchronized("t1");
TestSynchronized t2 = new
TestSynchronized("t2");
t1.start();
t1.start(); //(1)
//t2.start(); (2) }} 資源
運行結果爲:
t1 : 0
t1 : 1
t1 : 2
t1 : 0
t1 : 1
t1 : 2
因爲是同一個對象啓動的不一樣線程,因此run()函數實現了synchronized。若是去掉(2)的註釋,把代碼(1)註釋掉,結果將變爲:
t1 : 0
t2 : 0
t1 : 1
t2 : 1
t1 : 2
t2 : 2
因爲t1和t2是兩個對象,因此它們所啓動的線程可同時訪問run()函數。
2.2 經過實現Runnable接口實現多線程
若是有一個類,它已繼承了某個類,又想實現多線程,那就能夠經過實現Runnable接口來實現。
1) Runnable接口只有一個run()函數。
2) 把一個實現了Runnable接口的對象做爲參數產生一個Thread對象,再調用Thread對象的start()函數就可執行並行操做。若是在產生一個Thread對象時以一個Runnable接口的實現類的對象做爲參數,那麼在調用start()函數時,start()會調用Runnable接口的實現類中的run()函數。
例3.1:
public class TestThread implements Runnable
{
private static int threadCount = 0;
private int threadNum = ++threadCount;
private int i = 5;
public void run()
{
while(true)
{
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
System.out.println("Thread " + threadNum +
" = " + i);
if(--i==0) return;
}
}
public static void main(String[] args)
{
for(int i=0; i<5; i++) new Thread(new
TestThread()).start(); //(1)
}
}
運行結果爲:
Thread 1 = 5
Thread 2 = 5
Thread 3 = 5
Thread 4 = 5
Thread 5 = 5
Thread 1 = 4
Thread 2 = 4
Thread 3 = 4
Thread 4 = 4
Thread 4 = 3
Thread 5 = 4
Thread 1 = 3
Thread 2 = 3
Thread 3 = 3
Thread 4 = 2
Thread 5 = 3
Thread 1 = 2
Thread 2 = 2
Thread 3 = 2
Thread 4 = 1
Thread 5 = 2
Thread 1 = 1
Thread 2 = 1
Thread 3 = 1
Thread 5 = 1
例3是對例2的修改,它經過實現Runnable接口來實現並行處理。代碼(1)處可見,要調用TestThread中的並行操做部分,要把一個TestThread對象做爲參數來產生Thread對象,再調用Thread對象的start()函數。
3) 同一個實現了Runnable接口的對象做爲參數產生的全部Thread對象是同一對象下的線程。
例3.2:
package mypackage1;
public class TestThread implements Runnable
{
public synchronized void run()
{
for(int i=0; i<5; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
public static void main(String[] args)
{
TestThread testThread = new TestThread();
for(int i=0; i<5; i++)
//new Thread(testThread, "t" + i).start(); (1)
new Thread(new TestThread(), "t" +
i).start(); (2) }}
運行結果爲:
t0 : 0
t1 : 0
t2 : 0
t3 : 0
t4 : 0
t0 : 1
t1 : 1
t2 : 1
t3 : 1
t4 : 1
t0 : 2
t1 : 2
t2 : 2
t3 : 2
t4 : 2
t0 : 3
t1 : 3
t2 : 3
t3 : 3
t4 : 3
t0 : 4
t1 : 4
t2 : 4
t3 : 4
t4 : 4
因爲代碼(2)每次都是用一個新的TestThread對象來產生Thread對象的,因此產生出來的Thread對象是不一樣對象的線程,因此全部Thread對象均可同時訪問run()函數。若是註釋掉代碼(2),並去掉代碼(1)的註釋,結果爲:
t0 : 0
t0 : 1
t0 : 2
t0 : 3
t0 : 4
t1 : 0
t1 : 1
t1 : 2
t1 : 3
t1 : 4
t2 : 0
t2 : 1
t2 : 2
t2 : 3
t2 : 4
t3 : 0
t3 : 1
t3 : 2
t3 : 3
t3 : 4
t4 : 0
t4 : 1
t4 : 2
t4 : 3
t4 : 4
因爲代碼(1)中每次都是用同一個TestThread對象來產生Thread對象的,因此產生出來的Thread對象是同一個對象的線程,因此實現run()函數的同步。
二. 共享資源的同步
1. 同步的必要性
例4:
class Seq
{
private static int number = 0;
private static Seq seq = new Seq();
private Seq() {}
public static Seq getInstance()
{
return seq;
}
public int get()
{
number++;
//(a)
return number;
//(b)
}
}
public class TestThread
{
public static void main(String[] args)
{
Seq.getInstance().get();
//(1)
Seq.getInstance().get();
//(2)
}
}
上面是一個取得序列號的單例模式的例子,但調用get()時,可能會產生兩個相同的序列號:
當代碼(1)和(2)都試圖調用get()取得一個惟一的序列。當代碼(1)執行完代碼(a),正要執行代碼(b)時,它被中斷了並開始執行代碼(2)。一旦當代碼(2)執行完(a)而代碼(1)還未執行代碼(b),那麼代碼(1)和代碼(2)就將獲得相同的值。
2. 經過synchronized實現資源同步
2.1 鎖標誌
2.1.1 每一個對象都有一個標誌鎖。當對象的一個線程訪問了對象的某個synchronized數據(包括函數)時,這個對象就將被「上鎖」,因此被聲明爲synchronized的數據(包括函數)都不能被調用(由於當前線程取走了對象的「鎖標誌」)。只有當前線程訪問完它要訪問的synchronized數據,釋放「鎖標誌」後,同一個對象的其它線程才能訪問synchronized數據。
2.1.2 每一個class也有一個「鎖標誌」。對於synchronized
static數據(包括函數)能夠在整個class下進行鎖定,避免static數據的同時訪問。
例5:
class Seq
{
private static int number = 0;
private static Seq seq = new Seq();
private Seq() {}
public static Seq getInstance(){ return seq; }
public synchronized int get()
{
//(1)
number++;
return number;
}
}
例5在例4的基礎上,把get()函數聲明爲synchronized,那麼在同一個對象中,就只能有一個線程調用get()函數,因此每一個線程取得的number值就是惟一的了。
例6:
class Seq
{
private static int number = 0;
private static Seq seq = null;
private Seq() {}
synchronized public static Seq getInstance()
{
//(1)
if(seq==null) seq = new Seq();
return seq;
}
public synchronized int get()
{
number++;
return number;
}
}
例6把getInstance()函數聲明爲synchronized,那樣就保證經過getInstance()獲得的是同一個seq對象。
2.2 non-static的synchronized數據只能在同一個對象的純種實現同步訪問,不一樣對象的線程仍可同時訪問。
例7:
class TestSynchronized implements Runnable
{
public synchronized void run()
{
//(1)
for(int i=0; i<10; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
/*(2)*/
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
}
public class TestThread
{
public static void main(String[] args)
{
TestSynchronized r1 = new TestSynchronized();
TestSynchronized r2 = new TestSynchronized();
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r2, "t2"); //(3) //
Thread t2 = new Thread(r1, "t2"); (4)
t1.start();
t2.start();
}
}
運行結果爲:
t1 : 0
t2 : 0
t1 : 1
t2 : 1
t1 : 2
t2 : 2
t1 : 3
t2 : 3
t1 : 4
t2 : 4
t1 : 5
t2 : 5
t1 : 6
t2 : 6
t1 : 7
t2 : 7
t1 : 8
t2 : 8
t1 : 9
t2 : 9
雖然咱們在代碼(1)中把run()函數聲明爲synchronized,但因爲t1、t2是兩個對象(r1、r2)的線程,而run()函數是non-static的synchronized數據,因此仍可被同時訪問(代碼(2)中的sleep()函數因爲在暫停時不會釋放「標誌鎖」,由於線程中的循環很難被中斷去執行另外一個線程,因此代碼(2)只是爲了顯示結果)。
若是把例7中的代碼(3)註釋掉,並去年代碼(4)的註釋,運行結果將爲:
t1 : 0
t1 : 1
t1 : 2
t1 : 3
t1 : 4
t1 : 5
t1 : 6
t1 : 7
t1 : 8
t1 : 9
t2 : 0
t2 : 1
t2 : 2
t2 : 3
t2 : 4
t2 : 5
t2 : 6
t2 : 7
t2 : 8
t2 : 9
修改後的t1、t2是同一個對象(r1)的線程,因此只有當一個線程(t1或t2中的一個)執行run()函數,另外一個線程才能執行。
2.3 對象的「鎖標誌」和class的「鎖標誌」是相互獨立的。
例8:
class TestSynchronized extends Thread
{
public TestSynchronized(String name)
{
super(name);
}
public synchronized static void prt()
{
for(int i=10; i<20; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
public synchronized void run()
{
for(int i=0; i<10; i++)
{
System.out.println(Thread.currentThread().getName()
+ " : " + i);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
}
public class TestThread
{
public static void main(String[] args)
{
TestSynchronized t1 = new
TestSynchronized("t1");
TestSynchronized t2 = new
TestSynchronized("t2");
t1.start();
t1.prt(); //(1)
t2.prt(); //(2)
}
}
運行結果爲:
main : 10
t1 : 0
main : 11
t1 : 1
main : 12
t1 : 2
main : 13
t1 : 3
main : 14
t1 : 4
main : 15
t1 : 5
main : 16
t1 : 6
main : 17
t1 : 7
main : 18
t1 : 8
main : 19
t1 : 9
main : 10
main : 11
main : 12
main : 13
main : 14
main : 15
main : 16
main : 17
main : 18
main : 19
在代碼(1)中,雖然是經過對象t1來調用prt()函數的,但因爲prt()是靜態的,因此調用它時不用通過任何對象,它所屬的線程爲main線程。
因爲調用run()函數取走的是對象鎖,而調用prt()函數取走的是class鎖,因此同一個線程t1(由上面可知其實是不一樣線程)調用run()函數且還沒完成run()函數時,它就能調用prt()函數。但prt()函數只能被一個線程調用,如代碼(1)和代碼(2),即便是兩個不一樣的對象也不能同時調用prt()。
3. 同步的優化
1) synchronized block
語法爲:synchronized(reference){ do this }
reference用來指定「以某個對象的鎖標誌」對「大括號內的代碼」實施同步控制。
例9:
class TestSynchronized implements Runnable
{
static int j = 0;
public synchronized void run()
{
for(int i=0; i<5; i++)
{
//(1)
System.out.println(Thread.currentThread().getName()
+ " : " + j++);
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
}
public class TestThread
{
public static void main(String[] args)
{
TestSynchronized r1 = new TestSynchronized();
TestSynchronized r2 = new TestSynchronized();
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r1, "t2");
t1.start();
t2.start();
}
}
運行結果爲:
t1 : 0
t1 : 1
t1 : 2
t1 : 3
t1 : 4
t2 : 5
t2 : 6
t2 : 7
t2 : 8
t2 : 9
上面的代碼的run()函數實現了同步,使每次打印出來的j老是不相同的。但實際上在整個run()函數中,咱們只關心j的同步,而其他代碼同步與否咱們是不關心的,因此能夠對它進行如下修改:
class TestSynchronized implements Runnable
{
static int j = 0;
public void run()
{
for(int i=0; i<5; i++)
{
//(1)
synchronized(this)
{
System.out.println(Thread.currentThread().getName()
+ " : " + j++);
}
try
{
Thread.sleep(100);
}
catch(InterruptedException e)
{
System.out.println("Interrupted");
}
}
}
}
public class TestThread
{
public static void main(String[] args)
{
TestSynchronized r1 = new TestSynchronized();
TestSynchronized r2 = new TestSynchronized();
Thread t1 = new Thread(r1, "t1");
Thread t2 = new Thread(r1, "t2");
t1.start();
t2.start();
}
}
運行結果爲:
t1 : 0
t2 : 1
t1 : 2
t2 : 3
t1 : 4
t2 : 5
t1 : 6
t2 : 7
t1 : 8
t2 : 9
因爲進行同步的範圍縮小了,因此程序的效率將提升。同時,代碼(1)指出,當對大括號內的println()語句進行同步控制時,會取走當前對象的「鎖標誌」,即對當前對象「上鎖」,不讓當前對象下的其它線程執行當前對象的其它synchronized數據。