- 當多個線程對同一個數(shù)據(jù)進行操作的時候勺疼,就會出現(xiàn)線程安全問題。
- 比如銀行轉賬問題:同一個賬戶一邊進行出賬操作(淘寶支付),另一邊進行入賬操作(別人給自己匯款)炭晒,此時會因為線程同步帶來安全性問題。
- 以下舉一個線程安全問題的實例:
兩個線程不停地向屏幕輸出字符串甥角,A線程輸出feifeilover网严,B線程輸出xiaoxin,
所要達到的目的是:屏幕顯示完整的字符串嗤无。
代碼如下:
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() { //線程運行的代碼在Runnable對象里面
@Override
public void run() { //run中while循環(huá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() { //線程運行的代碼在Runnable對象里面
@Override
public void run() { //run中while循環(huán)是為了不停地運行
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
output.output("xiaoxin");
}
}
}).start();//main中啟動兩個線程
}
class Outputer { // 定義一個內部類震束,此類為一個輸出器
public void output(String string) { // 這個方法是為了把字符串的內容打印到屏幕上
int len = string.length();
for (int i = 0; i < len; i++) {
System.out.print(string.charAt(i));// 把字符一個一個的打印到屏幕
}
System.out.println(""); // 換行
}
}
}
注:內部類不能訪問局部變量,為訪問局部變量要加final当犯;
靜態(tài)方法里面不能new內部類的實例對象
-
執(zhí)行后的代碼如下顯示
理想狀態(tài)下我們希望上一個字符串打完以后垢村,在執(zhí)行別的,從執(zhí)行后的結果顯示嚎卫,它沒有等一個字符串全部輸出嘉栓,cpu卻跑去執(zhí)行另一個線程了;
這就是因為線程不同步拓诸,而使兩個線程都在使用同一個對象侵佃。
- 這里先給出一個聲明:
同步(Synchronous) 同步方法調用一旦開始,調用者必須等到方法調用返回后恰响,才能繼續(xù)后繼的行為趣钱。
要從根本上解決上述問題 ,就必須保證兩個線程A胚宦、B在對i輸出操作時完全同步首有。即在線程A寫入時燕垃,線程B不僅不能寫,同時也不能讀井联。因為在線程A寫完之前卜壕,線程B讀取的一定是一個過期數(shù)據(jù)
java中,提供了一個重要的關鍵字synchronized來實現(xiàn)這個功能烙常。它的功能是對同步的代碼加鎖轴捎,使得每一次,只能有一個線程進入同步塊蚕脏,從而保證線程間的安全性(即上面代碼的for語句每次應該只有一個線程可以執(zhí)行)侦副。
Synchrouized關鍵字常用的幾種方法:
1.指定加鎖對象:對給定對象加鎖,進入同步代碼前要獲得給定對象的鎖驼鞭。
class Outputer {
String str = "";
public void output(String string) {
int len = string.length();
synchronized (str) { //加鎖并傳入同一個對象
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}
}//內部類秦驯,是一個輸出器
用Synchronized實現(xiàn)同步互斥,在鎖中一定要是同一個對象挣棕。
前面我們提到的A線程是output對象译隘,B線程是output對象。這兩個使用的是同一個對象洛心,只需在內部類中加入String xxx = “”固耘;獲得Outputer的鎖。
由以上代碼可以看出鎖就是Outputer里面的str词身。Outputer對象在外部看是output厅目,而在內部看就是this。所以代碼可以簡化為:
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("");
}
}
}//內部類偿枕,是一個輸出器
2 . 直接作用于實例對象:相當于對當前實例加鎖璧瞬,進入同步代碼前要獲得當前實例的鎖
方法返回值前加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("");
}
}//內部類渐夸,是一個輸出器
3.直接作用于靜態(tài)方法:相當于對當前類加鎖嗤锉,進入同步代碼前要獲得當前類的鎖。
靜態(tài)同步方法使用的鎖是該方法所在的class文件對象
代碼如下:
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("");
}
}//內部類墓塌,是一個輸出器
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("");
}
注:關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用瘟忱,它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中苫幢,它確保了線程對變量訪問的可見性和排他性访诱。
經(jīng)典面試題:
- 子線程循環(huán)10次,接著主線程循環(huán)100次韩肝,接著又回到子線程循環(huán)10次触菜,接著再回到主線程又循環(huán)100次,如此循環(huán)50次哀峻。
首先涡相,將子線程和主線程中要同步的方法進行封裝哲泊,加上同步關鍵字實現(xiàn)同步。
代碼如下:
package com.java;
public class TraditionalThread {
public static void main(String[] args) {
final Business business = new Business(); //創(chuàng)建一個business對象
new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=50;i++) { //來回循環(huán)50次
business.sub(i);
}
}
}).start();
for(int i=1;i<=50;i++) {
business.main(i);
}
}
} //先起兩個線程催蝗,主線程和子線程
class Business { //定義內部類
public synchronized void sub(int i) { //定義子線程 (加鎖實現(xiàn)同步)
for(int j=1;j<=10;j++) {
System.out.println("sub "+j +","+"loop of " +i);
}
}
public synchronized void main(int i) { //定義主線程(加鎖實現(xiàn)同步)
for(int j=1;j<=100;j++) {
System.out.println("main " + j+","+"loop of " +i);
}
}
}
以上代碼實現(xiàn)了兩個線程的互斥切威。
等待/通知機制
- 一個線程A調用了對象O的wait()方法進入等待狀態(tài),而另一個線程B()調用了對象O的notify()方法丙号,線程A收到通知后從對象O的wait()方法返回先朦,進而執(zhí)行后續(xù)操作。以上兩個線程就是通過對象O來完成交互的犬缨。
wait與notify實現(xiàn)線程間的通信代碼(以上述面試題為例)
class Business { // 定義內部類
private boolean BShould = true;
public synchronized void sub(int i) { // 定義子線程 (加鎖實現(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) { // 定義主線程(加鎖實現(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方法時需要先對調用對象加鎖
- notify方法調用后,等待線程依舊不會從wait()返回遍尺,需要調用notify()的線程釋放鎖之后截酷, 等待線程才能有機會從wait()返回。
- wait()返回的前提是獲得了調用對象的鎖乾戏。
注:此系列博客參照張孝祥的java并發(fā)視頻,以及java高并發(fā)程序等書寫的三热,因為本人小白正在努力學習中鼓择,對知識掌握的特別的膚淺,如果看到我的博文就漾,有什么不對的地方呐能,或者是對我文章有意見的,可以私信給我抑堡,我會一直不斷改進的摆出。