線程安全定義
線程安全是一個(gè)非常重要的話題。Java通過 Thread 提供多線程環(huán)境,從相同的Object共享對(duì)象變量創(chuàng)建的多個(gè)線程,當(dāng)線程用于讀取和更新共享數(shù)據(jù)時(shí),這可能導(dǎo)致數(shù)據(jù)不一致。
線程安全
數(shù)據(jù)不一致的原因是因?yàn)楦氯魏巫侄沃挡皇窃舆^程,它需要三個(gè)步驟;
- 首先讀取當(dāng)前值
- 第二個(gè)讀取必要的操作以獲取更新的值
- 第三個(gè)將更新的值分配給字段引用
package coreofjava.javathread.threadsafety;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
// 等待線程結(jié)束
t1.join();
t2.join();
System.out.println("正在處理 count = " + pt.getCount());
}
}
class ProcessingThread implements Runnable {
private int count;
@Override
public void run() {
for (int i = 1; i < 5; i++) {
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
try {
Thread.sleep(i * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上述例子中,count增加了四次,有兩個(gè)線程,所以值應(yīng)該是8,但實(shí)際中是6或7或8
count++導(dǎo)致了數(shù)據(jù)損壞
讓線程安全
- 同步是 java 中最簡單和最廣泛使用的線程安全工具
- 使用java.util.concurrent.atomic包中的Atomic Wrapper類,如AtomicInteger
- 使用java.util.locks包中的鎖
- 使用線程安全集合類
- 使用帶有變量的volatile關(guān)鍵字使每個(gè)線程從內(nèi)存中讀取數(shù)據(jù),而不是從線程緩存中讀取
Java 同步
JVM保證同步代碼一次只能由一個(gè)線程執(zhí)行
synchronized 用于創(chuàng)建同步,內(nèi)部使用Object、Class上的鎖來確保只有一個(gè)線程正在執(zhí)行同步代碼
- 同步在鎖定或解鎖資源時(shí)起作用,任何線程進(jìn)入同步代碼前,必須獲取對(duì)象的鎖定,當(dāng)代碼執(zhí)行結(jié)束時(shí),解鎖可以被其他線程鎖定的資源。同時(shí),其他線程處于等待狀態(tài)以鎖定同步資源
- 2種方式使用synchronized, 一種是一個(gè)完整的方法同步,二是創(chuàng)建 synchronized 塊
- 方法同步時(shí),會(huì)鎖定 Object,若方法是靜態(tài)的,會(huì)鎖定Class,因此最好使用synchronized塊來鎖定需要同步的方法的位移部分。
- 在創(chuàng)建synchronized塊時(shí),需要提供將獲取鎖的資源,可以是任意類或類的任意字段
- synchronized(this) 將在進(jìn)入同步塊之前鎖定對(duì)象
- 應(yīng)該使用最低級(jí)別的鎖定,若有多個(gè) synchronized塊,并且其中一個(gè)鎖了Object,則其他同步塊也將無法由其他線程執(zhí)行,當(dāng)鎖定一個(gè)Object,它會(huì)獲取Object的所有字段。
- Java同步提供了性能成本的數(shù)據(jù)完整性,因此因此只有在絕對(duì)必要時(shí)才使用
- Java同步僅在同一個(gè) JVM中工作,如果需要在多個(gè)JVM中鎖定某些資源,將無法工作,可能需要處理一些全局鎖定機(jī)制
- Java同步可能會(huì)導(dǎo)致死鎖
- Java synchronized 關(guān)鍵字不能用于構(gòu)造函數(shù)和變量
- 最好創(chuàng)建一個(gè)用于同步塊的虛擬私有對(duì)象,它的引用就不能被任何其他代碼更改,若過正在同步Object setter方法,則可以通過某些其他代碼更改其引用,并執(zhí)行 synchronized 塊
- 不應(yīng)該使用包含常量池的對(duì)象。 例如,String 不應(yīng)該被用在同步方法中,如果任何其他代碼給String加了鎖,盡管兩個(gè)代碼不相關(guān),它將要求要求鎖住來自String池同一個(gè)引用對(duì)象,這將導(dǎo)致互相鎖住。
下面代碼我們需要進(jìn)行改進(jìn)來使得線程安全
package coreofjava.javathread.threadsafety;
public class HackerCode {
public synchronized void doSomething() {
System.out.println("Doing");
}
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
正在嘗試鎖定 myObject 示例,一旦獲得鎖定,就永遠(yuǎn)不會(huì)釋放它,這導(dǎo)致doSomething() 方法在等待鎖定時(shí)阻塞,這將導(dǎo)致系統(tǒng)死鎖并導(dǎo)致拒絕服務(wù) (Dos)
Not safe example
package coreofjava.javathread.threadsafety;
import java.util.Arrays;
public class NotSafe {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1", "2", "3", "4", "5", "6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1 = new Thread(hmp, "t1");
Thread t2 = new Thread(hmp, "t2");
Thread t3 = new Thread(hmp, "t3");
long start = System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Time taken = " + (System.currentTimeMillis()-start));
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable {
private String[] strArr = null;
public HashMapProcessor(String[] m) {
this.strArr = m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for (int i = 0; i < strArr.length; i++) {
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] + ":" + name;
}
private void processSomething(int index) {
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序輸出
Time taken = 15004
[1:t1, 2:t1:t3:t2, 3:t1, 4:t3, 5:t1:t3, 6:t1:t2:t3]
為何不是 1:t1:t2:t3, 2:t1,t2,t3, ...., 而是以上結(jié)果?
這是因?yàn)镾tring數(shù)組值因共享數(shù)據(jù)而沒有同步導(dǎo)致的損壞,我們將更改代碼將使得線程安全
改進(jìn)代碼如下
package coreofjava.javathread.threadsafety;
import java.util.Arrays;
public class Safe extends NotSafe {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1", "2", "3", "4", "5", "6"};
SafeHashMapProcessor hmp = new SafeHashMapProcessor(arr);
Thread t1 = new Thread(hmp, "t1");
Thread t2 = new Thread(hmp, "t2");
Thread t3 = new Thread(hmp, "t3");
long start = System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("Time taken = " + (System.currentTimeMillis()-start));
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class SafeHashMapProcessor implements Runnable{
private final Object lock = new Object();
private String[] strArr = null;
SafeHashMapProcessor(String[] m) {
this.strArr = m;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
public String[] getMap() {
return strArr;
}
private void processArr(String name) {
for (int i = 0; i < strArr.length; i++) {
processSomething(i);
addThreadName(i, name);
}
}
private void processSomething(int index) {
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void addThreadName(int i, String name) {
synchronized (lock) {
strArr[i] = strArr[i] + ":" + name;
}
}
}
通過改進(jìn),程序運(yùn)行輸出為
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]