首页 > 编程笔记 > Java笔记 阅读:22

Java synchronized实现线程互斥(附带实例)

多线程的并发操作可以有效地提高程序的效率,但在使用多线程访问同一个资源时,需要特别关注线程安全的问题。

一个共享数据是可以由多个线程一起操作的,但是当一个共享数据在被一个线程操作的过程中,操作并未执行完毕,如果此时另一个线程也参与操作该共享数据,就会导致共享数据存在安全问题。

下面通过一个具体的实例来演示线程安全问题。模拟场景如下:一对夫妻使用相同的银行账户去银行取钱,一人拿着存折,另一人拿着银行卡。假如账户中有 2000 元,丈夫取 1500 元,妻子在同一时间也取 1500 元。如果取钱这段程序没有同步进行,也就是说,这两个操作可以同时进行,假设丈夫执行取钱操作时,程序刚刚判断了余额充足,还没有把钱从余额中扣除,妻子也执行取钱操作,此时妻子也可以取出 1500 元,即两个人从一个账户中取出 3000 元,这就涉及线程安全。

【实例 1 】线程安全问题。
public class Example {
    public static void main(String[] args) {
        Account account = new Account();
        account.withdraw();
    }
}

class Account {
    // 账户余额
    public double balance = 2000;

    /**
     * 取钱测试方法
     * 在该方法中,模拟丈夫和妻子同时取1500元
     */
    public void withdraw() {
        MyThread t1 = new MyThread("丈夫", 1500);
        MyThread t2 = new MyThread("妻子", 1500);
        t1.start();
        t2.start();
    }
}

/**
* 封装取钱操作的自定义线程类
*/
class MyThread extends Thread {
    private double money;
    private String name;

    public MyThread(String name, double money) {
        this.name = name;
        this.money = money;
    }

    public void run() {
        // MyThread是Account类的内部类,故可直接访问balance属性
        if (balance > money) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance = balance - money;
            System.out.println(name + "取钱成功!");
        } else {
            System.out.println(name + "取钱失败!");
        }
    }
}
运行结果为:

妻子取钱成功!
丈夫取钱成功!

上面的实例演示了线程安全问题,也是线程并发操作的问题。两个线程操作同一个账户,账户中只有 2000 元,但为什么丈夫和妻子都能取出 1500 元呢?这是因为多线程在共享同一数据时,未进行线程同步,引发了数据安全问题。下图显示了取钱操作的过程和结果。


图 1 取钱操作的过程和结果

Java线程互斥

在 Java 中,可以通过线程互斥来解决线程安全问题。

线程互斥是指不同线程通过竞争进入临界区(共享的数据和硬件资源),为了防止发生访问冲突,在有限的时间内只允许其中之一独自使用共享资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

Java 中有两种实现线程互斥的方法:一是通过同步(Synchronized)代码块或同步方法实现,二是通过锁(Lock)实现。本节介绍第一种实现方式。

同步代码块(同步监听器)

同步代码块是使用关键字 synchronized 修饰的语句块。使用关键字 synchronized 修饰的语句块会自动被加上内置锁,从而实现同步。

同步代码块的语法格式如下:
synchronized(对象锁){
    // 需要同步访问控制的代码
}
利用关键字 synchronized 就可以为代码加上锁,在该关键字后面的圆括号中所放入的就是对象锁。对象锁可以是一个任意的类的对象,但是所有线程必须共用一个锁。

关键字 synchronized 使用的锁保存在 Java 的对象头中。JDK 提供了多种对象锁实现机制,这些机制的实现原理较为复杂,超出了本文的探讨范围,这里侧重于介绍如何基于关键字synchronized 实现线程同步。

同步方法

与同步代码块不同,同步方法将子线程要允许的代码放到一个方法中,在该方法的名称前面加上关键字 synchronized 即可,这里默认的锁为 this,即当前对象。

在使用时,需要确认多线程访问的是同一个实例的同步方法才能实现同步效果。当使用关键字 synchronized 修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

同步方法的语法格式如下:
访问修饰符 synchronized 返回类型 方法名() {
    // 需要同步访问控制的代码
}
或者:
synchronized 访问修饰符 返回类型 方法名() {
    // 方法体
}
关键字 synchronized 也可以修饰静态方法,此时如果调用该静态方法,就会锁住当前类的 Class 对象。

下面对实例 1 的代码进行重构。首先在 MyThread 类中添加一个新的 Object 类型的属性,命名为 lock。然后在 MyThread 类的构造器中添加一个新的参数,接收外部传入的 lock 对象并为 lock 属性赋值。接下来在 MyThread 类的 run() 方法中添加同步代码块,使用 lock 作为对象锁,限定同一时间仅能有一个线程执行取钱操作。最后修改 Account2 类的 withdraw() 方法,创建一个 byte[] 类型的对象 lockObject,并将其作为参数传入 MyThread 类的构造器中。这里需要注意的是,lockObject 可以是任意类,使用 byte[] 类型主要是因为底层创建对象的步骤更少。

【实例】同步代码块的应用。
public class Example {
    public static void main(String[] args) {
        Account2 account = new Account2();
        account.withdraw();
    }
}

class Account2 {
    // 账户余额
    public double balance = 2000;

    /**
     * 取钱测试方法
     * 在该方法中,模拟丈夫和妻子同时取1500元
     */
    public void withdraw() {
        // 创建多线程共用的锁对象
        byte[] lockObject = new byte[0];
        // 两个线程使用的是同一个锁对象
        MyThread t1 = new MyThread("丈夫", 1500, lockObject);
        MyThread t2 = new MyThread("妻子", 1500, lockObject);
        t1.start();
        t2.start();
    }
}

/**
* 封装取钱操作的自定义线程类
*/
class MyThread extends Thread {
    private double money;
    private String name;
    // 保存当前线程使用的锁对象
    private Object lock;

    public MyThread(String name, double money, Object lock) {
        this.name = name;
        this.money = money;
        this.lock = lock;
    }

    public void run() {
        // 使用同步锁
        synchronized (lock) {
            if (balance > money) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance = balance - money;
                System.out.println(name + "取钱成功!");
            } else {
                System.out.println(name + "取钱失败!");
            }
        }
    }
}
运行结果为:

丈夫取钱成功!
妻子取钱失败!

下图显示了使用线程加锁机制后的取钱操作的过程和结果。


图 2 使用线程加锁机制后的取钱操作的过程和结果

相关文章