- 當(dāng)多個(gè)線程對(duì)同一個(gè)數(shù)據(jù)進(jìn)行操作的時(shí)候,就會(huì)出現(xiàn)線程安全問(wèn)題。
- 比如銀行轉(zhuǎn)賬問(wèn)題:同一個(gè)賬戶一邊進(jìn)行出賬操作(淘寶支付),另一邊進(jìn)行入賬操作(別人給自己匯款),此時(shí)會(huì)因?yàn)榫€程同步帶來(lái)安全性問(wèn)題。
- 以下舉一個(gè)線程安全問(wèn)題的實(shí)例:
兩個(gè)線程不停地向屏幕輸出字符串,A線程輸出feifeilover,B線程輸出xiaoxin,
所要達(dá)到的目的是:屏幕顯示完整的字符串。
代碼如下:
package com.java;
public class Threadtrodition00 {
public static void main(String[] args) {
new Threadtrodition00().init();
}
private void init() {
final Outputer output = new Outputer();
new Thread(new Runnable() { //線程運(yùn)行的代碼在Runnable對(duì)象里面
@Override
public void run() { //run中while循環(huán)是為了不停地運(yùn)行
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
output.output("feifeilover");
}
}
}).start();
new Thread(new Runnable() { //線程運(yùn)行的代碼在Runnable對(duì)象里面
@Override
public void run() { //run中while循環(huán)是為了不停地運(yùn)行
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
output.output("xiaoxin");
}
}
}).start();//main中啟動(dòng)兩個(gè)線程
}
class Outputer { // 定義一個(gè)內(nèi)部類,此類為一個(gè)輸出器
public void output(String string) { // 這個(gè)方法是為了把字符串的內(nèi)容打印到屏幕上
int len = string.length();
for (int i = 0; i < len; i++) {
System.out.print(string.charAt(i));// 把字符一個(gè)一個(gè)的打印到屏幕
}
System.out.println(""); // 換行
}
}
}
注:內(nèi)部類不能訪問(wèn)局部變量,為訪問(wèn)局部變量要加final;
靜態(tài)方法里面不能new內(nèi)部類的實(shí)例對(duì)象
-
執(zhí)行后的代碼如下顯示
這里寫圖片描述
理想狀態(tài)下我們希望上一個(gè)字符串打完以后,在執(zhí)行別的,從執(zhí)行后的結(jié)果顯示,它沒(méi)有等一個(gè)字符串全部輸出,cpu卻跑去執(zhí)行另一個(gè)線程了;
這就是因?yàn)榫€程不同步,而使兩個(gè)線程都在使用同一個(gè)對(duì)象。
- 這里先給出一個(gè)聲明:
同步(Synchronous) 同步方法調(diào)用一旦開(kāi)始,調(diào)用者必須等到方法調(diào)用返回后,才能繼續(xù)后繼的行為。
要從根本上解決上述問(wèn)題 ,就必須保證兩個(gè)線程A、B在對(duì)i輸出操作時(shí)完全同步。即在線程A寫入時(shí),線程B不僅不能寫,同時(shí)也不能讀。因?yàn)樵诰€程A寫完之前,線程B讀取的一定是一個(gè)過(guò)期數(shù)據(jù)
java中,提供了一個(gè)重要的關(guān)鍵字synchronized來(lái)實(shí)現(xiàn)這個(gè)功能。它的功能是對(duì)同步的代碼加鎖,使得每一次,只能有一個(gè)線程進(jìn)入同步塊,從而保證線程間的安全性(即上面代碼的for語(yǔ)句每次應(yīng)該只有一個(gè)線程可以執(zhí)行)。
Synchrouized關(guān)鍵字常用的幾種方法:
1.指定加鎖對(duì)象:對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼前要獲得給定對(duì)象的鎖。
class Outputer {
String str = "";
public void output(String string) {
int len = string.length();
synchronized (str) { //加鎖并傳入同一個(gè)對(duì)象
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}
}//內(nèi)部類,是一個(gè)輸出器
用Synchronized實(shí)現(xiàn)同步互斥,在鎖中一定要是同一個(gè)對(duì)象。
前面我們提到的A線程是output對(duì)象,B線程是output對(duì)象。這兩個(gè)使用的是同一個(gè)對(duì)象,只需在內(nèi)部類中加入String xxx = “”;獲得Outputer的鎖。
由以上代碼可以看出鎖就是Outputer里面的str。Outputer對(duì)象在外部看是output,而在內(nèi)部看就是this。所以代碼可以簡(jiǎn)化為:
class Outputer {
public void output(String string) {
int len = string.length();
synchronized (this) {
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}
}//內(nèi)部類,是一個(gè)輸出器
2 . 直接作用于實(shí)例對(duì)象:相當(dāng)于對(duì)當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
方法返回值前加synchronized(一般一段代碼中只用一次synchronized,為了防止死鎖)
class Outputer {
public synchronized void output(String string) {
int len = string.length();
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}//內(nèi)部類,是一個(gè)輸出器
3.直接作用于靜態(tài)方法:相當(dāng)于對(duì)當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類的鎖。
靜態(tài)同步方法使用的鎖是該方法所在的class文件對(duì)象
代碼如下:
static class Outputer {
public synchronized void output(String string) {
int len = string.length();
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}//內(nèi)部類,是一個(gè)輸出器
public static synchronized void output3(String string) {
int len = string.length();
for (int i = 0; i < len; i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
注:關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來(lái)進(jìn)行使用,它主要確保多個(gè)線程在同一個(gè)時(shí)刻,只能有一個(gè)線程處于方法或者同步塊中,它確保了線程對(duì)變量訪問(wèn)的可見(jiàn)性和排他性。
經(jīng)典面試題:
- 子線程循環(huán)10次,接著主線程循環(huán)100次,接著又回到子線程循環(huán)10次,接著再回到主線程又循環(huán)100次,如此循環(huán)50次。
首先,將子線程和主線程中要同步的方法進(jìn)行封裝,加上同步關(guān)鍵字實(shí)現(xiàn)同步。
代碼如下:
package com.java;
public class TraditionalThread {
public static void main(String[] args) {
final Business business = new Business(); //創(chuàng)建一個(gè)business對(duì)象
new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=50;i++) { //來(lái)回循環(huán)50次
business.sub(i);
}
}
}).start();
for(int i=1;i<=50;i++) {
business.main(i);
}
}
} //先起兩個(gè)線程,主線程和子線程
class Business { //定義內(nèi)部類
public synchronized void sub(int i) { //定義子線程 (加鎖實(shí)現(xiàn)同步)
for(int j=1;j<=10;j++) {
System.out.println("sub "+j +","+"loop of " +i);
}
}
public synchronized void main(int i) { //定義主線程(加鎖實(shí)現(xiàn)同步)
for(int j=1;j<=100;j++) {
System.out.println("main " + j+","+"loop of " +i);
}
}
}
以上代碼實(shí)現(xiàn)了兩個(gè)線程的互斥。
等待/通知機(jī)制
- 一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B()調(diào)用了對(duì)象O的notify()方法,線程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而執(zhí)行后續(xù)操作。以上兩個(gè)線程就是通過(guò)對(duì)象O來(lái)完成交互的。
wait與notify實(shí)現(xiàn)線程間的通信代碼(以上述面試題為例)
class Business { // 定義內(nèi)部類
private boolean BShould = true;
public synchronized void sub(int i) { // 定義子線程 (加鎖實(shí)現(xiàn)同步)
if (!BShould) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
System.out.println("sub " + j + "," + "loop of " + i);
}
BShould = false;
this.notify();
}
public synchronized void main(int i) { // 定義主線程(加鎖實(shí)現(xiàn)同步)
if (BShould) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
System.out.println("main " + j + "," + "loop of " + i);
}
BShould = true;
this.notify();
}
}
- 在使用wait、notify方法時(shí)需要先對(duì)調(diào)用對(duì)象加鎖
- notify方法調(diào)用后,等待線程依舊不會(huì)從wait()返回,需要調(diào)用notify()的線程釋放鎖之后, 等待線程才能有機(jī)會(huì)從wait()返回。
- wait()返回的前提是獲得了調(diào)用對(duì)象的鎖。
注:此系列博客參照張孝祥的java并發(fā)視頻,以及java高并發(fā)程序等書寫的,因?yàn)楸救诵“渍谂W(xué)習(xí)中,對(duì)知識(shí)掌握的特別的膚淺,如果看到我的博文,有什么不對(duì)的地方,或者是對(duì)我文章有意見(jiàn)的,可以私信給我,我會(huì)一直不斷改進(jìn)的。