在Java并发编程札记-(一)基础-05线程安全问题一文中已经学习了什么是线程安全以及实现线程安全的方法。今天就来学习下其中的一种方法——隐式锁synchronized。

Java中每个对象都有且只有一个内置锁。通过synchronized修饰代码片段,可以在其上加锁。当任务运行到对象的被synchronized修饰的代码片段时,任务可以获取到对象的锁。当获取到对象的锁后,其他任务在锁被释放前就不能再进入该对象的synchronized代码片段。当任务执行完synchronized代码片段或者抛出异常,会自动释放锁。

synchronized是Java中的关键字,是一种同步锁。它可以修饰以下几种代码片段:

  1. 方法。作用范围是整个方法,作用的对象是调用这个方法的对象;
  2. 代码块。作用范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  3. 静态方法。作用范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 类。作用范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
synchronized修饰方法

synchronized修饰方法很简单,只需要在方法声明中添加synchronized关键字。格式如下

public synchronized void method(){
    //方法体
}

稍许修改下Java并发编程札记-(一)基础-05线程安全问题中的“例1:火车票订票系统-线程不安全版”中的代码,用synchronized修饰售票的方法,就可以使这个例子变为线程安全的。
例1:火车票订票系统-线程不安全版

public class SellTickets {

    public static void main(String[] args) {
        TicketsWindow tw = new TicketsWindow();
        Thread t1 = new Thread(tw, "一号窗口");
        Thread t2 = new Thread(tw, "二号窗口");
        t1.start();
        t2.start();
    }
}

class TicketsWindow implements Runnable {
    private int tickets = 1;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "还剩余票:" + tickets + "张");
                tickets--;
                System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
            } else {
                System.out.println(Thread.currentThread().getName() + "余票不足,暂停出售!");
                try {
                    Thread.sleep(1000 * 60 * 5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果为:

一号窗口还剩余票:1张
二号窗口还剩余票:1张
一号窗口卖出一张火车票,还剩0张
二号窗口卖出一张火车票,还剩-1张
一号窗口余票不足,暂停出售!
二号窗口余票不足,暂停出售!

余票只有一张,但最后卖出了两张火车票。这明显不是我们想要的结果。
当线程t1,即“一号窗口”执行run方法进行售票时,t2“二号窗口”也在执行run方法进行售票。下面是两个线程的执行顺序。

  1. 一号窗口读出某班次的火车票余票A,设A=1;
  2. 二号窗口读出同一班次的火车票余票B,当然也为1;
  3. 一号窗口判断出余票A=1>0,卖出一张火车票,修改余票A←A-1,A为0,把A写回数据库;
  4. 二号窗口判断出余票B=1>0,也卖出一张火车票,修改余票B←B-1,B为-1;

例2:火车票订票系统-synchronized修饰方法线程安全版
现在给run()方法添加synchronized修饰符,即将public void run()改为public synchronized void run(),再次运行,会发现结果变为:

一号窗口还剩余票:1张
一号窗口卖出一张火车票,还剩0张
一号窗口余票不足,暂停出售!

这是因为synchronized为run方法加了锁,当线程t1,即“一号窗口”执行run方法时,就获取到了对象tw的锁,所以线程t2“二号窗口”就无法执行run方法了,这样就不会线程t1所做的修改就不会被覆盖,结果自然是正确的了。

synchronized修饰代码块

synchronized不仅可以修饰方法,还可以修饰代码块。这样synchronized的使用变得灵活许多,因为也许一个方法中只有一部分代码需要同步,如果此时对整个方法进行同步,会影响执行效率。格式如下

synchronized(synObject) {
    //方法体
}

synObject可以是this,代表获取当前对象的锁;也可以是类中的一个属性对象,代表获取该属性对象的锁。
例3:火车票订票系统-synchronized修饰代码块线程安全版

@Override
public synchronized void run() {
    while (true) {
        if (tickets > 0) {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + "还剩余票:" + tickets + "张");
                tickets--;
                System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "余票不足,暂停出售!");
            try {
                Thread.sleep(1000 * 60 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

一个对象有一个锁,多个对象是有多个锁的,如果多个线程访问多个对象,怎么实现同步呢?这时就需要了解如何使多个对象共享一个锁。

synchronized修饰静态方法

synchronized还可以修饰静态方法。为什么要把静态方法和方法区分开呢?众所周知,静态方法属于类不属于对象,因此synchronized修饰静态方法锁定的是这个类的所有对象。格式如下

public synchronized static void method() {
    //方法体
}

例4:火车票订票系统-synchronized修饰静态方法线程安全版

public class SellTickets {

    public static void main(String[] args) {
        TicketsWindow tw1 = new TicketsWindow();
        TicketsWindow tw2 = new TicketsWindow();
        Thread t1 = new Thread(tw1, "一号窗口");
        Thread t2 = new Thread(tw2, "二号窗口");
        t1.start();
        t2.start();
    }
}

class TicketsWindow implements Runnable {
    private static int tickets = 1;

    @Override
    public synchronized void run() {
        sellTicket();
    }
    public synchronized static void sellTicket() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "还剩余票:" + tickets + "张");
                --tickets;
                System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
            } else {
                System.out.println(Thread.currentThread().getName() + "余票不足,暂停出售!");
                try {
                    Thread.sleep(1000*60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这个例子和前面的例子的最大区别是有两个任务tw1和tw2,但在t1和t2并发执行时却保持了线程同步。这是因为run中调用了静态方法sellTicket,而静态方法是属于类TicketsWindow的,所以tw1和tw2共用了类TicketsWindow的锁。

synchronized修饰类

synchronized还可以修饰类。与synchronized修饰静态方法效果相同,锁定的是这个类的所有对象。格式如下

class ClassName {
    public void method() {
        synchronized(ClassName.class) {
        //方法体
        }
    }
}

例5:火车票订票系统-synchronized修饰类线程安全版

public class SellTickets {

    public static void main(String[] args) {
        TicketsWindow tw1 = new TicketsWindow();
        TicketsWindow tw2 = new TicketsWindow();
        Thread t1 = new Thread(tw1, "一号窗口");
        Thread t2 = new Thread(tw2, "二号窗口");
        t1.start();
        t2.start();
    }
}

class TicketsWindow implements Runnable {
    private int tickets = 1;

    @Override
    public synchronized void run() {
        synchronized (SyncThread.class) {
            while (true) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "还剩余票:" + tickets + "张");
                    --tickets;
                    System.out.println(Thread.currentThread().getName() + "卖出一张火车票,还剩" + tickets + "张");
                } else {
                    System.out.println(Thread.currentThread().getName() + "余票不足,暂停出售!");
                    try {
                        Thread.sleep(1000 * 60);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

和例2相同,两个任务tw1和tw2能在t1和t2并发执行时保持了线程同步,是因为tw1和tw2共用了类TicketsWindow的锁。

注意事项

将域设置为private
在使用并发时,要将域设置为private,否则synchronized就不能阻止其他任务直接访问域,这样可能会产生不可预知的结果。

一个任务可以多次获得对象的锁
如果一个任务在同一个对象上调用了第二个方法,后者又调用了同一个对象上的第三个方法,这个任务就会多次获取这个对象的锁。每当任务执行所有的方法,锁才被完全释放。

class PrintClass {
    public synchronized void func1() {
        System.out.println("func1()");
        func2();
    }

    public synchronized void func2() {
        System.out.println("func2()");
        func3();
    }

    public synchronized void func3() {
        System.out.println("func3()");
    }
}

class MyThread extends Thread {
    public void run() {
        PrintClass pc = new PrintClass();
        pc.func1();
    }
}

public class Demo {

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
    }
}

打印结果为:

func1()
func2()
func3()

每个访问临界资源的方法都必须被同步
如果在你的类中有超过一个方法在处理临界数据,那么必须同步所有的方法。如果只同步一个方法,其他方法可以忽略这个锁。所以,每个访问临界资源的方法都必须被同步。

异常自动释放锁
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

本文就讲到这里,想了解更多内容请参考:

  • Java并发编程札记-目录

END.