日常開發(fā)中往往會遇到多個線程循環(huán)操作某個對象中的某個方法或者間接處理某個對象,如果當前某個對象被多個線程同時操作的話,勢必會造成同步的問題,即線程A操作了對象object從而改變了該object的值,此刻線程A獲取到的object的值還是之前的值。那么類似這種情況下,我們通常就要用如下幾種方式去解決該問題。
1、synchronized 關(guān)鍵字
2、Lock
3、ReadWriteLock
4、其他方式

(一)synchronized
是java中表示同步代碼快的關(guān)鍵字。可以放在方法修飾符前,比如private synchronized void test(){},也可以放在方法內(nèi)部,修飾某一段特定的代碼。synchronized有一個地方需要注意,就是在給普通方法加鎖與給靜態(tài)方法加鎖機制是不一樣的。synchronized在靜態(tài)方法上表示調(diào)用前要獲得類的鎖,而在非靜態(tài)方法上表示調(diào)用此方法前要獲得對象的鎖,synchronized可作用于instance變量、object reference(對象引用)、static函數(shù)和class literals(類名稱字面常量)身上。。
如下代碼所示:
public class SynTest {
private static String a="a";
//等同于方法test2
public synchronized void test1(String b){ //調(diào)用前要取得SynTest 實例化后對象的鎖
System.out.println(a+b);
}
public void test2(String b){
synchronized (this) {//取得SynTest 實例化后對象的鎖
System.out.println(a+b);
}
}
//等同于方法test4
public synchronized static void test3(String b){//調(diào)用前要取得SynTest .class類的鎖
System.out.println(b+a);
}
public static void test4(String b){
synchronized (SynTest .class) { //取得SynTest .class類的鎖
System.out.println(a+b);
}
注意:
1、test1和test2中synchronized 鎖定的是SynTest的對象即該類的實例化對象,也可認為是object reference(對象引用), test3和test4中synchronized 鎖定的是SynTest對象所屬的類(Class,而不再是由這個Class產(chǎn)生的某個具體對象了),SynTest.class和synTest.getClass作用于同步鎖是不一樣的,不能用synTest.getClass()來達到鎖這個Class的目的。synTest指的是由SynTest類產(chǎn)生的對象。
2、對synchronized(this)的一些理解
一、當兩個并發(fā)線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內(nèi)只能有一個線程得到執(zhí)行。另一個線程必須等待當前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。
二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
三、尤其關(guān)鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
四、當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結(jié)果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
3、synchronized 方法的缺陷:若將一個大的方法聲明為synchronized 將會大大影響效率,典型地,若將線程類的方法 run() 聲明為synchronized ,由于在線程的整個生命期內(nèi)它一直在運行,因此將導致它對本類任何 synchronized 方法的調(diào)用都永遠不會成功。當然我們可以通過將訪問類成員變量的代碼放到專門的方法中,將其聲明為synchronized ,并在主方法中調(diào)用來解決這一問題,但是 Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。
4、synchronized 塊:通過 synchronized關(guān)鍵字來聲明synchronized 塊。語法如下:
synchronized(syncObject) {
//允許訪問控制的代碼
}
synchronized 塊是這樣一個代碼塊,其中的代碼必須獲得對象 syncObject (如前所述,可以是類實例或類)的鎖方能執(zhí)行,具體機制同前所述。由于可以針對任意代碼塊,且可任意指定上鎖的對象,故靈活性較高。
5、如果只是想單純的鎖某段代碼塊,當有明確的對象作為鎖是可以的,如果沒有明確的對象作為鎖,則可以創(chuàng)建一個特殊的變量。如下:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance變量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
注:零長度的byte數(shù)組對象創(chuàng)建起來將比任何對象都經(jīng)濟――查看編譯后的字節(jié)碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。
(二)Lock
是java.util.concurrent.locks包下的接口,Lock實現(xiàn)提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作,因為Lock可以鎖定任意一段代碼
public class LockTest {
public static void main(String[] args) {
final Outputter output = new Outputter();
new Thread() {
public void run() {
output.output("Alex-Jerry");
};
}.start();
new Thread() {
public void run() {
output.output("AI-Curry");
};
}.start();
}
}
class Outputter {
private Lock lock = new ReentrantLock();// 鎖對象
public void output(String name) {
// TODO 線程輸出方法
lock.lock();// 得到鎖
try {
for(int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 釋放鎖
}
}
}
這樣就實現(xiàn)了和sychronized一樣的同步效果,需要注意的是,用sychronized修飾的方法或者語句塊在代碼執(zhí)行完之后鎖自動釋放,而用Lock需要我們手動釋放鎖,所以為了保證鎖最終被釋放(發(fā)生異常情況),要把互斥區(qū)放在try內(nèi),釋放鎖放在finally內(nèi)。
(三)ReadWriteLock
前兩種方式雖然解決了讀和寫、寫和寫之間的互斥,但是讀和讀之間同樣也是互斥的(不同得到線程可以同步執(zhí)行讀?。?,嚴重影響了效率,這個時候就該ReadWriteLock出場了。
class RWLock{
private int data;// 共享數(shù)據(jù)
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到寫鎖
try {
System.out.println(Thread.currentThread().getName() + "準備寫入數(shù)據(jù)");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "寫入" + this.data);
} finally {
rwl.writeLock().unlock();// 釋放寫鎖
}
}
public void get() {
rwl.readLock().lock();// 取到讀鎖
try {
System.out.println(Thread.currentThread().getName() + "準備讀取數(shù)據(jù)");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "讀取" + this.data);
} finally {
rwl.readLock().unlock();// 釋放讀鎖
}
}
}
測試:
public class ReadWriteLockTest {
public static void main(String[] args) {
final RWLock data = new RWLock();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
}).start();
}
}
}
部分測試輸出結(jié)果如下:
Thread-4準備讀取數(shù)據(jù)
Thread-3準備讀取數(shù)據(jù)
Thread-5準備讀取數(shù)據(jù)
Thread-5讀取18
Thread-4讀取18
Thread-3讀取18
Thread-2準備寫入數(shù)據(jù)
Thread-2寫入6
Thread-2準備寫入數(shù)據(jù)
Thread-2寫入10
Thread-1準備寫入數(shù)據(jù)
Thread-1寫入22
(四)其他方式
線程同步的問題,最上面說的只是一種問題,往往導致同步問題的原因有多種。如線程A和線程B,線程B要等到線程A執(zhí)行完畢之后才能進行操作,或者子線程執(zhí)行結(jié)束才能讓主線程去操作等等,這種情況下,可解決的方案很多,如:使用FutureTask+Callable代替Runnable去執(zhí)行異步任務(FutureTask 的 get 方法,能夠阻塞主線程,直到子線程初始化完成,才繼續(xù)執(zhí)行主線程邏輯),使用handler機制,操作線程的wait、notify、join、yield等等。基于這些情況,下篇文章將會逐個去分析和解決。