程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java多線程及同步實現原理

Java多線程及同步實現原理

編輯:關於JAVA

一. 實現多線程

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()為什麼不會出現我們預想的結果,在下面將講到。

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數據。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved