非執行緒安全會在多個執行緒對同一個物件中的例項變數進行併發訪問時發生,產生的結果就是髒讀,也就是取到的資料是被更改過的。執行緒安全就是獲得的例項變數的值是經過同步處理的。
方法內的變數是執行緒安全的
方法內的變數是執行緒安全的。非執行緒安全的問題存在於例項變數中,如果是方法內部的私有變數,不存在非執行緒安全問題。例子如下:
class HasMethodPrivateNum {
public void addI(String username){
try {
int num=0;
if(username.equals("a")){
num=100;
System.out.println("a set over");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b set over");
}
System.out.println(username+" num = "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread {
private HasMethodPrivateNum numRef;
public ThreadA(HasMethodPrivateNum numRef){
super();
this.numRef=numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
class ThreadB extends Thread {
private HasMethodPrivateNum numRef;
public ThreadB(HasMethodPrivateNum numRef){
super();
this.numRef=numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
public class Run {
public static void main(String[] args) {
HasMethodPrivateNum numRef=new HasMethodPrivateNum();
ThreadA threadA=new ThreadA(numRef);
threadA.start();
ThreadB threadB=new ThreadB(numRef);
threadB.start();
}
}
複製程式碼
輸出結果:
a set over
b set over
b num = 200
a num = 100
複製程式碼
可見,方法中的變數不存在非線性安全問題,是執行緒安全的。
例項變數非執行緒安全
例項變數是非執行緒安全的。如果多個執行緒共同訪問一個物件中的例項變數,有可能出現非執行緒安全問題。用執行緒訪問的物件中如果有多個例項變數,執行的結果可能出現交叉,如果只有一個例項變數,有可能出現覆蓋的情況。在這種情況下,需要為操作該例項變數的方法加上synchronized關鍵字。在多個執行緒訪問同一個物件中的同步方法時一定是執行緒安全的。
修改上述的程式碼,將第一個類中的addI()方法中的變數作為成員變數放到類中:
class HasSelfPrivateNum {
private int num=0;
synchronized public void addI(String username){
try {
if(username.equals("a")){
num=100;
System.out.println("a set over");
Thread.sleep(2000);
}else{
num=200;
System.out.println("b set over");
}
System.out.println(username+" num = "+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
測試結果如下:
a set over
b set over
b num = 200
a num = 200
複製程式碼
可以發現,得到的結果是存線上程安全問題的。當為addI()方法加上synchronized關鍵字之後,測試結果如下:
a set over
a num = 100
b set over
b num = 200
複製程式碼
可以發現,不存線上程安全問題了。
關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼或方法當作鎖。當多個執行緒訪問的是同一個物件時,哪個執行緒先執行帶關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖,其他執行緒只能等待。但是如果多個執行緒訪問多個物件,則JVM會建立多個鎖。
當一個物件存在同步方法a和非同步方法b,執行緒A和執行緒B分別訪問方法a和方法b時,執行緒A先持有該物件的Lock鎖,但是執行緒B可以非同步呼叫該物件的非同步方法b。但是如果兩個方法都是同步的方法,當A訪問方法a時,已經持有了該物件的Lock鎖,B執行緒此時呼叫該物件的另外的同步方法時,也需要等待,也就是同步。示例程式碼如下:
class MyObject {
synchronized public void methodA(){
try {
System.out.println("begin methodA in thread: "+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end methodA in time:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB(){
try {
System.out.println("begin methodB in thread: "+Thread.currentThread().getName()+" time:"+System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end methodB");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private MyObject object;
public ThreadA(MyObject object){
super();
this.object=object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
class ThreadB extends Thread{
private MyObject object;
public ThreadB(MyObject object){
super();
this.object=object;
}
@Override
public void run() {
super.run();
object.methodB();
}
}
public class Run {
public static void main(String[] args) {
MyObject object=new MyObject();
ThreadA a=new ThreadA(object);
a.setName("A");
ThreadB b=new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
}
複製程式碼
測試結果:
begin methodA in thread: A
begin methodB in thread: B time:1544263806800
end methodB
end methodA in time:1544263811800
複製程式碼
可以看出,執行緒A先得到了object物件的鎖,但是執行緒B仍然非同步呼叫了非同步方法。將methodB()新增了synchronized關鍵字後,測試結果為:
begin methodA in thread: A
end methodA in time:1544264023516
begin methodB in thread: B time:1544264023516
end methodB
複製程式碼
可以看到,A執行緒先得到object的鎖,B執行緒如果此時呼叫objcet中的同步方法需要等待。
髒讀
使用synchronized關鍵字可以實現多個執行緒呼叫同一個方法時,進行同步。雖然賦值時進行了同步,但是在取值時可能會出現髒讀的情況,也就是在讀取例項變數時,該值已經被其他執行緒更改過了。因此,需要在讀取資料的方法也採用同步方法才可以。
鎖重入
synchronized鎖重入:在使用synchronized時,當一個執行緒得到一個物件鎖後,再次請求此物件鎖時是可以再次得到該物件的鎖。也就是自己可以再次獲取自己的內部鎖。當一個執行緒獲得了某個物件的鎖,鎖沒釋放且想要獲取這個物件的鎖的時候還是可以獲取的。如果不可鎖重入,會造成死鎖。示例程式碼:
class Service {
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
service3();
}
synchronized public void service3(){
System.out.println("service3");
}
}
class MyThread extends Thread{
@Override
public void run() {
Service service=new Service();
service.service1();
}
}
public class Run {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
}
}
複製程式碼
測試結果:
service1
service2
service3
複製程式碼
可重入鎖也支援在父子類繼承的環境中。當存在父子類繼承關係時,子類可以通過“可重入鎖”呼叫父類的同步方法。示例程式碼如下:
class Main {
public int i=10;
synchronized public void operateIMainMethod(){
try {
i--;
System.out.println("main print i = "+i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Sub extends Main{
public synchronized void operateISubMethod() {
try {
while(i>0){
i--;
System.out.println("sub print i="+i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Sub sub=new Sub();
sub.operateISubMethod();
}
}
public class Run {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
}
}
複製程式碼
測試結果為:
sub print i=9
main print i = 8
sub print i=7
main print i = 6
sub print i=5
main print i = 4
sub print i=3
main print i = 2
sub print i=1
main print i = 0
複製程式碼
當一個執行緒執行的程式碼出現異常,其所持有的鎖會自動釋放。
同步不具有繼承性
如果父類的方法是同步方法,但是子類重寫了該方法,但是沒有新增synchronized關鍵字,則在呼叫子類的方法時,仍然不是同步方法,需要在子類的方法中新增synchronized關鍵字才可以。
參考資料
- 高洪巖. Java多執行緒程式設計核心技術[M]. 機械工業出版社, 2015.
- github.com/CyC2018/CS-…
- github.com/Snailclimb/…
- crossoverjie.top/JCSprout/#/…