- 當多個線程對同一個數據進行操作的時候,就會出現線程安全問題。
- 比如銀行轉賬問題:同一個賬戶一邊進行出賬操作(淘寶支付),另一邊進行入賬操作(別人給自己匯款),此時會因為線程同步帶來安全性問題。
- 以下舉一個線程安全問題的實例:
兩個線程不停地向屏幕輸出字符串,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循環是為了不停地運行
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循環是為了不停地運行
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;
靜態方法里面不能new內部類的實例對象
-
執行后的代碼如下顯示
這里寫圖片描述
理想狀態下我們希望上一個字符串打完以后,在執行別的,從執行后的結果顯示,它沒有等一個字符串全部輸出,cpu卻跑去執行另一個線程了;
這就是因為線程不同步,而使兩個線程都在使用同一個對象。
- 這里先給出一個聲明:
同步(Synchronous) 同步方法調用一旦開始,調用者必須等到方法調用返回后,才能繼續后繼的行為。
要從根本上解決上述問題 ,就必須保證兩個線程A、B在對i輸出操作時完全同步。即在線程A寫入時,線程B不僅不能寫,同時也不能讀。因為在線程A寫完之前,線程B讀取的一定是一個過期數據
java中,提供了一個重要的關鍵字synchronized來實現這個功能。它的功能是對同步的代碼加鎖,使得每一次,只能有一個線程進入同步塊,從而保證線程間的安全性(即上面代碼的for語句每次應該只有一個線程可以執行)。
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實現同步互斥,在鎖中一定要是同一個對象。
前面我們提到的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.直接作用于靜態方法:相當于對當前類加鎖,進入同步代碼前要獲得當前類的鎖。
靜態同步方法使用的鎖是該方法所在的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可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處于方法或者同步塊中,它確保了線程對變量訪問的可見性和排他性。
經典面試題:
- 子線程循環10次,接著主線程循環100次,接著又回到子線程循環10次,接著再回到主線程又循環100次,如此循環50次。
首先,將子線程和主線程中要同步的方法進行封裝,加上同步關鍵字實現同步。
代碼如下:
package com.java;
public class TraditionalThread {
public static void main(String[] args) {
final Business business = new Business(); //創建一個business對象
new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=50;i++) { //來回循環50次
business.sub(i);
}
}
}).start();
for(int i=1;i<=50;i++) {
business.main(i);
}
}
} //先起兩個線程,主線程和子線程
class Business { //定義內部類
public synchronized void sub(int i) { //定義子線程 (加鎖實現同步)
for(int j=1;j<=10;j++) {
System.out.println("sub "+j +","+"loop of " +i);
}
}
public synchronized void main(int i) { //定義主線程(加鎖實現同步)
for(int j=1;j<=100;j++) {
System.out.println("main " + j+","+"loop of " +i);
}
}
}
以上代碼實現了兩個線程的互斥。
等待/通知機制
- 一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B()調用了對象O的notify()方法,線程A收到通知后從對象O的wait()方法返回,進而執行后續操作。以上兩個線程就是通過對象O來完成交互的。
wait與notify實現線程間的通信代碼(以上述面試題為例)
class Business { // 定義內部類
private boolean BShould = true;
public synchronized void sub(int i) { // 定義子線程 (加鎖實現同步)
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) { // 定義主線程(加鎖實現同步)
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并發視頻,以及java高并發程序等書寫的,因為本人小白正在努力學習中,對知識掌握的特別的膚淺,如果看到我的博文,有什么不對的地方,或者是對我文章有意見的,可以私信給我,我會一直不斷改進的。