在java多線程併發操作中,如果不加任何的同步控制,有可能會出現一些錯誤的情況。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
一次運行結果:
Thread-1-->10
Thread-2-->9
Thread-0-->8
Thread-3-->9
Thread-4-->7
Thread-0-->6
Thread-2-->4
Thread-1-->5
Thread-3-->6
Thread-4-->3
Thread-3-->1
Thread-2-->-1
Thread-0-->2
Thread-1-->0
Thread-4-->-2
可以看到一些數字打印了兩遍,一些打印了一遍,更爲要命的是還打印出來一些負數。如果學習過操作系統併發的知識的話,這個問題其實不難理解。JVM爲每個線程分配時間片,並選擇了一個線程將處理機分配給他。但是這個線程執行執行到sleep()時進入了休眠狀態(也就是阻塞態),這時JVM就會再選擇一個線程分配處理機資源。設想如果當n=1時,有三個線程都進入了都執行到Thread.sleep(10),當這三個線程被喚醒使就會去執行打印語句部分。因爲他們都已經經歷了n>0條件,所以都會執行打印語句,而且執行n--,就可能出現上述出現-1,-2的情況。打印兩次也是這樣,當一個線程執行完打印n,還沒來得及對n進行減一,就被剝奪了處理機。另一個線程此時又打印了一次n。這樣的情況在併發編程中是不允許的,就比如買火車票,上述情況就如同有的票被賣給兩個人,沒有票了還在售賣。
java中提供了同步關鍵字:synchrnoized(同步)來解決多線程併發操作中的由於共享資源導致的資源衝突。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
運行結果:
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
在方法前加一個synchronized關鍵字,該方法就變成了一個同步方法,就不會出現上一個那樣錯誤的情況了。但是又有一個奇怪的情況:在主線程中我開啓了五個線程去執行task10這個任務,爲什麼只有打印出來的線程名只有一個Thread-0呢。這裏就得說明一下synchronized同步的機制了。在java中每個對象都有一個對象鎖,一個對象的對象鎖同一時間只能由一個線程獲取。一個線程從獲取該對象鎖的時刻起到該線程釋放該對象鎖止,其他線程是無法取得該對象鎖的。如果線程無法獲取該對象的對象鎖,那麼這些線程是無法去執行該對象的同步方法的。例子的運行結果就是因爲:Thread-0取得了task10這個對象的對象鎖,他就一直佔用着task10對象的對象鎖。另外四個線程只能乾巴巴的看着,進不了被同步的方法裏去。
我在同步方法之前和之後又加了兩個普通的輸出語句,來說明對象鎖對於同步代碼塊的鎖定。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName() + "hello");
method();
System.out.println(Thread.currentThread().getName()+"hello");
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
運行結果:
Thread-1hello
Thread-3hello
Thread-0hello
Thread-4hello
Thread-2hello
Thread-1-->10
Thread-1-->9
Thread-1-->8
Thread-1-->7
Thread-1-->6
Thread-1-->5
Thread-1-->4
Thread-1-->3
Thread-1-->2
Thread-1-->1
Thread-1hello
Thread-2hello
Thread-4hello
Thread-0hello
Thread-3hello
可以看到確實開啓了五個線程,而且同步方法裏的代碼只有一個線程在執行(而且這次是Thread-1佔用了對象鎖),執行完同步方法裏的代碼後,其他線程這時又登場了。
我們再來看一種情況。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new MyTask10());
thread.start();
}
}
}
Thread-0-->10
Thread-3-->10
Thread-2-->10
Thread-1-->10
Thread-4-->10
Thread-1-->9
Thread-2-->9
Thread-3-->9
Thread-0-->9
Thread-4-->9
Thread-3-->8
Thread-2-->8
Thread-1-->8
Thread-0-->8
Thread-4-->8
Thread-1-->7
Thread-0-->7
Thread-3-->7
Thread-2-->7
Thread-4-->7
Thread-1-->6
Thread-2-->6
Thread-0-->6
Thread-3-->6
Thread-4-->6
Thread-1-->5
Thread-3-->5
Thread-0-->5
Thread-2-->5
Thread-4-->5
Thread-1-->4
Thread-0-->4
Thread-2-->4
Thread-3-->4
Thread-4-->4
Thread-1-->3
Thread-2-->3
Thread-0-->3
Thread-3-->3
Thread-4-->3
Thread-1-->2
Thread-0-->2
Thread-2-->2
Thread-3-->2
Thread-4-->2
Thread-1-->1
Thread-0-->1
Thread-2-->1
Thread-3-->1
Thread-4-->1
這種情況其實更好理解,因爲五個線程所執行的是不同的任務,所以就不存在共享資源的問題。因爲這五個線程每次所執行的是五個不同的MyTask10對象的method方法,每線程各自完成各自的任務,互不影響,如果從對象鎖的角度來說是因爲五個對象都有各自的對象鎖,一個線程佔用一個對象的對象鎖也不會影響另外一個線程去使用另一個對象的對象鎖。即便沒有synchrnoized來實現同步也不會出現一些錯誤的結果(就是打印出0,-1,-2的情況),沒有資源共享時也沒有必要同步。
同步靜態方法:
這裏我將method方法改爲了static方法,static方法又稱類方法,static修飾的方法或者變量在內存中只有一個副本,不管創建多少個類的實例,都只有一個副本。
package com.lql.thread;
public class MyTask10 implements Runnable {
private static int n = 10;
public MyTask10() {
}
public static synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new MyTask10());
thread.start();
}
}
}
運行結果:
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
上邊說的都是同步方法,java還提供了另外一種細粒度的實現線程同步的機制——同步代碼塊。
package com.lql.thread;
public class MyTask11 implements Runnable {
private int n = 10;
private String name = "";
public MyTask11(){
}
public void method(){
System.out.println(Thread.currentThread().getName() +"hello");
synchronized (this) {
while(n > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"-->" + n--);
}
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask11 task11 = new MyTask11();
Thread thread = new Thread(task11);
Thread thread2 = new Thread(task11);
thread.start();
thread2.start();
}
}
Thread-0helloThread-1hello
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
同步方法所能實現的同步,同步代碼塊也都能實現。同步方法有時同步的範圍太大,有時只是一小部分的代碼是臨界區,如果將整個方法都整成同步方法並不好,這與併發的理念是相悖的。所以就有了同步代碼塊。
synchrnized(this)括號中的對象指定當前線程獲取哪個對象的對象鎖,因爲對象的鎖是唯一的,所以一個線程獲取了對象鎖,在該線程釋放鎖之前,其他線程也就無法再取得該對象鎖,在一個線程執行完同步塊代碼後,才釋放該對象鎖。synchrnized()括號裏可以使任何一個對象,因爲一個確定的對象他的對象鎖是唯一的。只要一個線程取得了對象鎖,其他線程都無法進入同步塊。
總結以上所有內容:
1.當併發編程共享資源時,要使用同步來確保同一時刻只有一個線程進入臨界區。
2.java中每個對象都有一個對象鎖,synchronized就是基於對象鎖來實現同步,一個線程佔用了對象鎖其他線程就必須等待上一個線程釋放了對象鎖才能進入被同步的臨界區。
3.同步靜態方法時,線程獲取的對象鎖是類對象(MyTask.class)的對象鎖。無論定義多少個對象,被同步的靜態方法都無動於衷。哪個線程有了類對象的對象,就讓哪個線程進到同步區。
4.同步塊是一種細粒度實現同步的方法,同步塊指定執行到同步塊的線程獲取哪個對象的對象鎖。由於一個確定對象的對象鎖唯一,所以能夠實現只有一個線程進入同步區代碼執行。
以上是個人的理解,如有錯誤或不當歡迎批評指正。