Java併發程式設計5 —— 併發髒讀問題
兩個執行緒同時修改共享資料容易產生執行緒安全問題。如果是在資料庫中,一條資料由很多個欄位組成,假設一個執行緒要去修改其中幾個欄位的值,與此同時有另一個執行緒想要讀取這條資料,那麼會產生什麼問題呢。
下面的程式碼中,Student類有學號id、姓名name、專業名dprt三個屬性。某同學申請轉專業,需要修改他的學號和專業名。修改學號和專業名需要1秒鐘完成,同時有另一個執行緒在查他的資訊,查詢速度很快。
import static java.lang.Thread.sleep; public class DirtyRead { public static void main(String[] args) { Student student = new Student(105, "Peter", "Physics"); SetInfo setInfo = new SetInfo(student); GetInfo getInfo = new GetInfo(student); Thread set = new Thread(setInfo); Thread get = new Thread(getInfo); set.start(); get.start(); } } class SetInfo implements Runnable{ private Student student; public SetInfo(Student student){ this.student = student; } @Override public void run() { student.setInfo(205, "Math"); } } class GetInfo implements Runnable{ private Student student; public GetInfo(Student student){ this.student = student; } @Override public void run() { student.getInfo(); } } class Student{ private int id; private String name; private String dprt; public Student(int id, String name, String dprt){ this.id = id; this.name = name; this.dprt = dprt; System.out.println("init: " + toString()); } public void setInfo(int id,String dprt){ this.id = id; try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.dprt = dprt; System.out.println("setInfo finished: " + toString()); } public void getInfo(){ System.out.println("getInfo: " + toString()); } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", dprt='" + dprt + '\'' + '}'; } }
輸出
init: Student{id=105, name='Peter', dprt='Physics'}
getInfo: Student{id=205, name='Peter', dprt='Physics'}
setInfo finished: Student{id=205, name='Peter', dprt='Math'}
不難看出,getInfo的方法在setInfo結束前就執行了,並且get到的資料是錯誤的。學號已經改了,但是專業名還沒改,仍然是轉系之前的專業名。這條資料是錯誤的,是不存在的,也就是“髒資料”。這也就產生了“髒讀”問題。
所以在設計併發時要考慮到全域性的併發,既要實現同步寫,又要實現同步讀。如果把Student類的setInfo方法定義為Synchronized,那是不是就可以使整個setInfo方法原子化,等setInfo執行完才能去執行getInfo方法呢。然而並不是這樣。因為在setInfo方法上加鎖相當於是加了一把物件鎖,而物件鎖僅對Synchronized方法有效。也就是說,getInfo方法不是Synchronized的,所以getInfo和setInfo並不互斥,在setInfo執行過程中仍然可以執行getInfo方法去讀資料。此時互斥的是多個執行緒同時執行setInfo方法。
為了解決上述髒讀問題,我們需要把setInfo和getInfo都定義為Synchronized,使物件鎖同時對二者生效,兩個執行緒分別執行這兩個方法時互斥。
import static java.lang.Thread.sleep; public class DirtyRead { public static void main(String[] args) { Student student = new Student(105, "Peter", "Physics"); SetInfo setInfo = new SetInfo(student); GetInfo getInfo = new GetInfo(student); Thread set = new Thread(setInfo); Thread get = new Thread(getInfo); set.start(); get.start(); } } class SetInfo implements Runnable{ private Student student; public SetInfo(Student student){ this.student = student; } @Override public void run() { student.setInfo(205, "Math"); } } class GetInfo implements Runnable{ private Student student; public GetInfo(Student student){ this.student = student; } @Override public void run() { student.getInfo(); } } class Student{ private int id; private String name; private String dprt; public Student(int id, String name, String dprt){ this.id = id; this.name = name; this.dprt = dprt; System.out.println("init: " + toString()); } public synchronized void setInfo(int id,String dprt){ this.id = id; try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.dprt = dprt; System.out.println("setInfo finished: " + toString()); } public synchronized void getInfo(){ System.out.println("getInfo: " + toString()); } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", dprt='" + dprt + '\'' + '}'; } }
輸出
init: Student{id=105, name='Peter', dprt='Physics'}
setInfo finished: Student{id=205, name='Peter', dprt='Math'}
getInfo: Student{id=205, name='Peter', dprt='Math'}
此時get到的資料已經是完全被修改後的了,是一條正確的資料。而且從輸出順序上也能看出來,第一次讀到髒資料時是先get在finish set,而此時是先finish set再get。