非線程安全會在多個線程對同一個對象中的實例變量進行併發訪問時發生,產生的結果就是髒讀,也就是取到的數據是被更改過的。線程安全就是得到的實例變量的值是通過同步處理的。java
方法內的變量是線程安全的。非線程安全的問題存在於實例變量中,若是是方法內部的私有變量,不存在非線程安全問題。例子以下:git
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();
}
}
複製代碼
輸出結果:github
a set over
b set over
b num = 200
a num = 100
複製代碼
可見,方法中的變量不存在非線性安全問題,是線程安全的。編程
實例變量是非線程安全的。若是多個線程共同訪問一個對象中的實例變量,有可能出現非線程安全問題。用線程訪問的對象中若是有多個實例變量,運行的結果可能出現交叉,若是隻有一個實例變量,有可能出現覆蓋的狀況。在這種狀況下,須要爲操做該實例變量的方法加上synchronized關鍵字。在多個線程訪問同一個對象中的同步方法時必定是線程安全的。安全
修改上述的代碼,將第一個類中的addI()方法中的變量做爲成員變量放到類中:bash
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會建立多個鎖。ide
當一個對象存在同步方法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關鍵字才能夠。
參考資料