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();
    }
}

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


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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM