多线程之间对同一共享资源进行操作,容易出现线程安全问题,解决方案就是把共享资源加锁,从而实现线程同步,使任意时刻只能有一个线程操作共享资源。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();
    }
}

这种方案,跟同步代码块一样,一个类中可以存在多个锁对象。只不过需要自己手动进行加锁和解锁。


到此为止,三种线程同步的方案已经介绍完毕,每种方案各有优缺点,大家可以根据实际需要,选择使用不同的方案。