1. 程式人生 > 其它 >Java實現多執行緒【同步】的三種方式

Java實現多執行緒【同步】的三種方式

多執行緒之間對同一共享資源進行操作,容易出現執行緒安全問題,解決方案就是把共享資源加鎖,從而實現執行緒同步,使任意時刻只能有一個執行緒操作共享資源。Java 有 3 種方式可以實現執行緒同步,為了更清晰的描述方案,我以兩個視窗賣火車票為例進行介紹 3 種執行緒同步的方案。本篇部落格目的在於總結 Java 多執行緒同步的知識點,以便在平時工作中用到的時候,可以快速上手。


方案一、採用同步程式碼塊

同步程式碼塊格式:

//需要確保多個執行緒使用的是同一個鎖物件
synchronized (鎖物件) {
    多條語句操作共享資料的程式碼
}

程式碼演示:

public class Ticket implements Runnable {
    //火車票的總數量
    private int ticket = 50;
    //鎖物件
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            //同步程式碼塊:多個執行緒必須使用同一個鎖物件
            synchronized (obj) {
                if (ticket <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                    ticket = ticket - 1;
                    System.out.println(Thread.currentThread().getName() +
                                              "正在賣票,還剩下 " + ticket + " 張票");
                }
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        /*
        不能採用這種方式,因為這樣相當於每個執行緒使用不同的物件,沒有共享資源
        Ticket ticket1 = new Ticket();
        Ticket ticket2 = new Ticket();

        Thread t1 = new Thread(ticket1);
        Thread t2 = new Thread(ticket2);*/

        //例項化一個物件,讓所有執行緒都使用這一個物件
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);

        t1.setName("視窗一");
        t2.setName("視窗二");

        t1.start();
        t2.start();
    }
}

同步程式碼塊:這種實現方案允許一個類中存在多個鎖物件。

如果想讓多個執行緒即使訪問多個不同的程式碼塊,也要統一排隊等待的話,可以讓多個程式碼塊使用同一個鎖物件。

如果想讓多個執行緒訪問不同的程式碼塊互不影響,但是訪問同一個程式碼塊需要排隊等待的話,可以讓多個程式碼塊分別使用不同的鎖物件。


方案二、採用同步方法

同步方法的格式:

//同步方法的鎖物件是其所在類的例項化物件本身 this
修飾符 synchronized 返回值型別 方法名 (方法引數) {
    方法體
}

//同步靜態方法的鎖物件是其所在的類的 類名.Class
修飾符 static synchronized 返回值型別 方法名 (方法引數) {
    方法體
}

同步方法的程式碼演示:

public class Ticket implements Runnable {
    private static int ticketCount = 50;

    @Override
    public void run() {
        while (true) {
            //這裡先休眠 100 毫秒,為了讓多個執行緒都有機會搶奪共享資源
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //使用同步方法
            boolean result = synchronizedMthod();
            if (result) {
                break;
            }
        }
    }

    //同步方法的鎖物件就是 this 本身
    private synchronized boolean synchronizedMthod() {
        if (ticketCount <= 0) {
            return true;
        } else {
            ticketCount = ticketCount - 1;
            System.out.println(Thread.currentThread().getName() +
                                  "正在賣票,還剩下 " + ticketCount + " 張票");
            return false;
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //例項化一個物件,讓所有執行緒都使用這一個物件
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket,"視窗一");
        Thread t2 = new Thread(ticket,"視窗二");

        t1.start();
        t2.start();
    }
}

同步靜態方法的程式碼演示:

//為了證明同步靜態方法的鎖物件是其所在的類的 類名.Class
//這裡針對兩個視窗執行緒,分別採用不同的同步方式來證明
//視窗一執行緒,採用同步靜態方法
//視窗二執行緒,採用同步程式碼塊,但是使用的是當前類的 類名.Class 作為鎖物件
//最終可以發現【視窗一執行緒】和【視窗二執行緒】能夠實現執行緒同步
public class Ticket implements Runnable {
    private static int ticketCount = 50;

    @Override
    public void run() {
        while (true) {
            //視窗一執行緒,使用同步靜態方法
            if ("視窗一".equals(Thread.currentThread().getName())) {
                //同步方法
                boolean result = synchronizedMthod();
                if (result) {
                    break;
                }
            }

            //視窗二執行緒,使用同步程式碼塊,但是鎖物件是當前類的 類名.Class
            if ("視窗二".equals(Thread.currentThread().getName())) {
                //同步程式碼塊
                synchronized (Ticket.class) {
                    if (ticketCount <= 0) {
                        break;
                    } else {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticketCount--;
                        System.out.println(Thread.currentThread().getName() +
                                              "正在賣票,還剩下 " + ticketCount + " 張票");
                    }
                }
            }
        }
    }

    //同步靜態方法
    private static synchronized boolean synchronizedMthod() {
        if (ticketCount <= 0) {
            return true;
        } else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() +
                                  "正在賣票,還剩下 " + ticketCount + " 張票");
            return false;
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //例項化一個物件,讓所有執行緒都使用這一個物件
        Ticket mr = new Ticket();

        Thread t1 = new Thread(mr, "視窗一");
        Thread t2 = new Thread(mr, "視窗二");

        t1.start();
        t2.start();
    }
}

同步方法:這種方案會導致同一個例項物件中的所有的同步方法的鎖物件都是 this ,因此多個執行緒即使訪問該例項物件中不同的同步方法時,也必須統一排隊等待。

同步靜態方法:這種方案導致同一個類中所有的同步靜態方法的鎖物件都是當前的 類名.Class ,因此多個執行緒即使訪問該類中不同的同步靜態方法時,也必須統一排隊等待。


方案三、採用 Lock 鎖物件例項

JDK5以後提供了一個新的鎖物件 Lock,但是 Lock 是介面不能直接例項化,因此必須採用它的實現類 ReentrantLock 來實現執行緒同步。ReentrantLock 有兩個方法:

方法名 說明
void lock() 對多執行緒要訪問的共享資原始碼加鎖
void unlock() 對多執行緒要訪問的共享資原始碼解鎖

程式碼演示:

public class Ticket implements Runnable {
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //這裡先休眠 100 毫秒,為了讓多個執行緒都有機會搶奪共享資源
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock.lock(); //加鎖

            if (ticket <= 0) {
                break;
            } else {
                ticket--;
                System.out.println(Thread.currentThread().getName() +
                                       "正在賣票,還剩下 " + ticket + " 張票");
            }

            lock.unlock(); //解鎖
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        //例項化一個物件,讓所有執行緒都使用這一個物件
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket,"視窗一");
        Thread t2 = new Thread(ticket,"視窗二");

        t1.start();
        t2.start();
    }
}

這種方案,跟同步程式碼塊一樣,一個類中可以存在多個鎖物件。只不過需要自己手動進行加鎖和解鎖。


到此為止,三種執行緒同步的方案已經介紹完畢,每種方案各有優缺點,大家可以根據實際需要,選擇使用不同的方案。