1. 程式人生 > >Java併發程式設計5 —— 併發髒讀問題

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。