首頁  >  文章  >  Java  >  實例解析Java中的synchronized關鍵字與執行緒安全問題

實例解析Java中的synchronized關鍵字與執行緒安全問題

高洛峰
高洛峰原創
2017-01-05 14:47:481074瀏覽

首先來回顧synchronized的基本使用:

synchronized代碼塊,被修飾的代碼成為同步語句塊,其作用的範圍是調用這個代碼塊的對象,我們在用synchronized關鍵字的時候,能縮小代碼段的範圍就盡量縮小,能在程式碼段上加同步就不要再整個方法上加同步。這叫做減小鎖的粒度,使程式碼更大程度的並發。

synchronized方法,被修飾的方法成為同步方法,其作用範圍是整個方法,作用物件是呼叫這個方法的物件。

synchronized靜態方法,修飾一個static靜態方法,其作用範圍是整個靜態方法,作用物件是這個類別的所有物件。

synchronized類,其作用範圍是Synchronized後面括號括起來的部分synchronized(className.class),作用的物件是這個類別的所有物件。

synchronized()  ()中是鎖住的對象, synchronized(this)鎖住的只是對象本身,同一個類別的不同對象調用的synchronized方法並不會被鎖住,而synchronized(className.class)實現了全域鎖的功能,所有這個類別的物件都呼叫這個方法都受到鎖的影響,此外()還可以加入一個特定的對象,實作給具體物件加鎖。

synchronized (object) {
 //在同步代码块中对对象进行操作 
}

synchronized關鍵字與線程安全
以為用了synchronized關鍵字包住了程式碼就可以線程同步安全了。測試了下。發現是完全的錯了。 synchronized必須正確的使用才是真正的線程安全。 。 。雖然知道這種寫法,一直以為卻因為懶而用了錯誤的方法。
看來基礎還沒打好。仍需複習加強!工作上犯這種錯誤是不可原諒的,要知道使用synchronized關鍵字的地方都是資料敏感的!汗一把。 。 。
先貼代號:

package com; 
  
public class ThreadTest { 
 public static void main(String[] args) { 
  MyThread m1 = new MyThread(1); 
  MyThread m2 = new MyThread(2); 
  m1.start(); 
  m2.start(); 
 } 
} 
  
final class MyThread extends Thread { 
 private int val; 
  
 public MyThread(int v) { 
  val = v; 
 } 
 //这种做法其实是非线程安全的 
 public synchronized void print1(int v) { 
  for (int i = 0; i < 100; i++) { 
   System.out.print(v); 
  } 
 } 
  
 public void print2(int v) { 
  //线程安全 
  synchronized (MyThread.class) { 
   for (int i = 0; i < 100; i++) { 
    System.out.print(v); 
   } 
  } 
 } 
  
 public void run() { 
  print1(val); 
  // print2(val); 
 } 
}

還是為了偷懶,汗一把。 。 。程式設計師總是懶的吧。能少寫就少寫。我把MyThread寫成了一個匿名的最終的內部類,方便呼叫。它用了最直接的繼承Thread來實作一個執行緒類,定義需要運行的run()方法。
先註解了print2()方法,看看print1()的結果如何。 print1()是一個使用了synchronized關鍵字定義的方法,我一直以為這樣也可以實現線程安全。殊不知,我錯了。
我們來直接運行main()方法。控制台列印結果如下:

1212111121212121212121212121212121212121222222212121212。。。

是一連串1和2交叉列印的結果。而我main方法中是先運行m1再運行m2的,顯示沒有做到執行緒同步!

MyThread m1 = new MyThread(1); 
MyThread m2 = new MyThread(2); 
m1.start(); 
m2.start();

接下來我們註解掉run方法中的print1(),運行print2();
控制台印如下:

11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222

線程果然是安全了,一直以為也知道這種寫法,但由於這種寫法代碼稍微多點就沒怎麼考慮,今天才意識到這種錯誤。看來有時候不懶還是有好處的。打好基礎很重要。糾正的長期以來的一個錯誤。

下面我們來看看具體原因。

synchronized關鍵字可以作為函數的修飾符,也可作為函數內的語句,也就是平常說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變數、object reference(物件參考)、static函數和class literals(類別名稱字面常數)身上。
在進一步闡述之前,我們需要先明確幾點:
A.無論synchronized關鍵字加在方法上或對像上,它所取得的鎖都是對象,而不是把一段程式碼或函式當作鎖――而且同步方法很可能還會被其他執行緒的對象存取。
B.每個物件只有一個鎖(lock)與之相關聯。
C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。
接著來討論synchronized用到不同地方對程式碼產生的影響:
假設P1、P2是同一個類別的不同對象,這個類別中定義了以下幾種情況的​​同步區塊或同步方法,P1、P2就都可以調用它們。
1. 把synchronized當作函數修飾符時,範例程式碼如下:

Public synchronized void methodAAA() 
{ 
//…. 
}

這也就是同步方法,那這時synchronized鎖定的是哪個物件呢?它鎖定的是呼叫這個同步方法物件。也就是說,當一個物件P1在不同的執行緒中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個物件所屬的Class所產生的另一物件P2卻可以任意呼叫這個被加了synchronized關鍵字的方法。
上邊的範例程式碼等同於下列程式碼:

public void methodAAA() 
{ 
synchronized (this) // (1) 
{ 
//….. 
} 
}

(1)處的this指的是什麼呢?它指的就是呼叫這個方法的對象,如P1。可見同步方法實質是將synchronized作用於object reference。 ――那個拿到了P1物件鎖的線程,才可以呼叫P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程式也可能在這種情形下擺脫同步機制的控制,造成數據混亂!

2.同步區塊,範例程式碼如下:

public void method3(SomeObject so) 
{ 
synchronized(so) 
{ 
//….. 
} 
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable 
{ 
private byte[] lock = new byte[0]; // 特殊的instance变量 
Public void methodA() 
{ 
synchronized(lock) { //… } 
} 
//….. 
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:

Class Foo 
{ 
public synchronized static void methodAAA() // 同步的static 函数 
{ 
//…. 
} 
public void methodBBB() 
{ 
synchronized(Foo.class) // class literal(类名称字面常量) 
} 
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
小结如下:
搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。
还有一些技巧可以让我们对共享资源的同步访问更加安全:
1.定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
2.如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全,因为当外界对象通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。

总结一些synchronized注意事项:

当两个并发线程访问同一个对象中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。两个线程间是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。(两个线程使用的是同一个对象)

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞(同上,两个线程使用的是同一个对象)。

更多实例解析Java中的synchronized关键字与线程安全问题相关文章请关注PHP中文网!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn