Java 线程 5 - 线程同步和线程通信

参考:

Java 线程 0 - 前言


下面学习 Java 中线程同步,线程通信的概念和使用


主要内容:

  1. 为什么线程需要同步
  2. ReentrantLockCondition
  3. synchronized

为什么线程需要同步

竞争条件:线程共享进程资源,当多线程对同一个对象进行访问时,根据各线程访问进程的次序,可能会得到一个错误的结果

《Java核心技术 卷I 14.5 同步》中给出了一个银行账户的例子

首先定义 Bank 类,保存银行账户信息:

public class Bank {

    private double[] accounts = null;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];

        for (int i = 0; i < n; i++) {
            accounts[i] = initialBalance;
        }
    }

    public void transfer(int from, int to, double amount) throws InterruptedException {
        if (accounts[from] < amount) return;
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    }

    public double getTotalBalance() {
        double sum = 0;

        for (double a : accounts) {
            sum += a;
        }

        return sum;
    }

    public int size() {
        return accounts.length;
    }
}

构造函数初始化银行账户数以及每个账户金额

方法 transfer 用于转账操作

方法 getTotalBalance 返回当前银行总金额

方法 size 返回账户数

然后创建类 TransferRunnable 实现接口 Runnable,在 run 方法中每隔不定时间,执行银行账户转账操作:

public class TransferRunnable implements Runnable {

    private Bank bank;
    private int fromAccount;
    private double maxAmount;
    private int DELAY = 10;

    public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
        this.bank = bank;
        this.fromAccount = fromAccount;
        this.maxAmount = maxAmount;
    }

    @Override
    public void run() {
        try {
            while (true) {
                int toAccount = (int) (bank.size() * Math.random());
                double amount = maxAmount * Math.random();
                bank.transfer(fromAccount, toAccount, amount);
                Thread.sleep((int) (DELAY * Math.random()));
            }
        } catch (InterruptedException e) {

        }
    }
}

最后创建测试类,定义账户数和初始余额,并创建多个线程:

public class BankTest {

    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;

    public static void main(String[] args) {
        Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
        int i;
        for (i = 0; i < NACCOUNTS; i++) {
            TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
            Thread t = new Thread(r);
            t.start();
        }
    }

}

因为线程实现的是账户之间的转账操作,所以不论执行多少次,银行账户总金额应该保持不变,不过在运行上述程序后,总金额会发生变化

原因:多线程同时对账户进行操作时,并没有实现原子操作,无法保证 原子性

解决思路:对关键代码块加锁,保证当前执行此临界区的线程仅有一个,使用条件对象来管理处于临界区的线程

锁和条件对象的作用如下:

  1. 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
  2. 锁可以管理试图进入被保护代码段的线程
  3. 锁可以拥有一个或多个相关的条件对象
  4. 每个条件对象管理那些已经进入被保护的代码段但不能运行的线程

解决方案有 2 种:

  1. 使用 ReentrantLock 显式加锁
  2. 使用 synchronized 关键字

ReentrantLockCondition

ReentrantLock 用于线程同步操作,Condition 用于线程通信操作

ReentrantLock

Java SE 5.0 开始引入了 ReentrantLock 类,基本使用格式如下:

private Lock lock = new ReentrantLock();

public void lockDemo() {
    lock.lock();
    try {
        critical section
    } finally {
        lock.unlock();
    }
}

定义一个 ReentrantLock 对象,在关键代码块前后进行 加锁解锁 操作

Note:使用 try-finally 结构可以保证及时解锁

线程 A 获得了锁,在运行临界区时失去了处理器资源,线程 B 开始运行,因为线程 A 拥有此锁,所以线程 B 会被阻塞,直到线程 A 运行完成后,解锁为止

锁具有可重入性。线程可以访问多个被同一个锁保护的临界区,在锁内部保存一个计数器,计算对锁重复访问的次数,所以每次加锁(lock)操作后,必须对应一个解锁(unlock)操作。当计数器为 0 时,线程释放该锁

假定类中含有锁,对于不同的类对象而言,其锁并不相同,线程访问不同的类对象并不会对同一锁进行请求,所以不会造成阻塞操作

Condition

一个锁对象可以有多个条件对象(Condition),使用方法 newCondition 即可产生一个新的条件对象

private ReentrantLock lock = new ReentrantLock();
private Condition condition =  lock.newCondition();

当线程在临界区中需要满足某一条件才能继续执行时,调用方法 await,该线程即进入该条件对象的等待集

public void lockDemo() {
    lock.lock();
    try {

        while (...)
            condition.await();

    } finally {
        lock.unlock();
    }
}

调用 await 方法后,该线程进入 等待 状态,必须有另一个线程调用同一条件上的 signalAll 方法,才能唤醒所有等待该条件的线程

/**
 * Wakes up all waiting threads.
 *
 * <p>If any threads are waiting on this condition then they are
 * all woken up. Each thread must re-acquire the lock before it can
 * return from {@code await}.
 *
 * <p><b>Implementation Considerations</b>
 *
 * <p>An implementation may (and typically does) require that the
 * current thread hold the lock associated with this {@code
 * Condition} when this method is called. Implementations must
 * document this precondition and any actions taken if the lock is
 * not held. Typically, an exception such as {@link
 * IllegalMonitorStateException} will be thrown.
 */
void signalAll();

也可以调用 signal 方法随机唤醒一个等待线程,不过这样容易造成死锁

修改上面的 Bank 类,对关键代码块进行同步和通信操作:

public class Bank {

    private double[] accounts = null;
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];

        for (int i = 0; i < n; i++) {
            accounts[i] = initialBalance;
        }

        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    public void transfer(int from, int to, double amount) throws InterruptedException {
        bankLock.lock();
        try {
            while (accounts[from] < amount)
                sufficientFunds.await();
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" %10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            sufficientFunds.signalAll();
        } finally {
            bankLock.unlock();
        }
    }

    public double getTotalBalance() {
        bankLock.lock();
        try {
            double sum = 0;

            for (double a : accounts) {
                sum += a;
            }

            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    public int size() {
        return accounts.length;
    }
}

synchronized

Java 1.0 开始,每个对象都有一个内部锁。方法使用关键字 synchronized 声明,即使用内部锁保护整个方法

内部锁仅包含单个条件对象,使用 wait 方法来添加线程到等待集中,使用方法 notify / notifyAll 释放等待集中的线程

修改 Bank 类代码如下:

public class Bank {

    private double[] accounts = null;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];

        for (int i = 0; i < n; i++) {
            accounts[i] = initialBalance;
        }
    }

    public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
        while (accounts[from] < amount)
            wait();
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        notifyAll();
    }

    public synchronized double getTotalBalance() {
        double sum = 0;

        for (double a : accounts) {
            sum += a;
        }

        return sum;
    }

    public int size() {
        return accounts.length;
    }
}

客户端锁定

参考:java synchronized锁对象,但是当对象引用是null的时候,锁的是什么?

因为每个对象都包含一个内部锁,所以也可使用如下方式同步:

synchronized (obj) {
  critical section  
}

使用 obj 对象的锁来保护临界区(obj 对象不能为空

猜你喜欢

转载自blog.csdn.net/u012005313/article/details/78410856