Java 多线程(二)

并发

本文将通过一个实现了 ReentrantLock 和 Condition 的 Bank 类来梳理 Java 的并发加锁机制,该类源码来自《Java 核心技术卷1》,该 Bank 类模拟一个有若干账户的银行,并提供转账交易方法:

import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author leon
 * @date 2019-04-05 09:19
 */
public class Bank {

    private final double[] accounts;//创建银行账户
    private Lock bankLock; //创建 lock 对象
    private Condition sufficicentFunts; // 创建 conditio 对象

    /**
     * 
     * @param n 账户数量
     * @param initialBalance 初始存款
     */
    public Bank(int n,double initialBalance){

        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        bankLock = new ReentrantLock();
        sufficicentFunts = bankLock.newCondition();

    }


    public void transfer(int from,int to,double amount) throws InterruptedException {

        bankLock.lock(); //加锁

        try {

            while (accounts[from] < amount) {
                sufficicentFunts.await(); //条件对象调用 await 方法,该线程被阻塞,并放弃锁
            }

            accounts[from] -= amount;
            accounts[to] += amount;

            sufficicentFunts.signalAll(); //解除其他进程的阻塞


        }finally {

            bankLock.unlock(); //解锁
        }


    }


    public double getTotalBalance(){

        bankLock.lock(); //同一个对象可以两次加锁
        try{

            double sum = 0;

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

            return sum;

        }finally {

            bankLock.unlock();

        }

    }
    
}

加锁机制

加锁是为了避免多线程对共享数据的讹误,在此例中即当一个进程对一个 bank 对象调用 transfer 方法时,但是 transfer 方法并没有执行完,另一个进程又调用该对象的 transfer 就会导致结果讹误,最终银行所有账户的总存款要么变多要么减少,这显然是不安全的。所以当多个线程并发访问时,需要对线程提供加锁机制。Java 提供两种加锁机制:synchronized 关键字和 ReentrantLock 类。

ReentrantLock的结构如下:

myLock.lock();
try{
	...
}finally{
	mylock.unlock();
}

这一结构确保只有一个进程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock 语句,如果其他线程调用 lock ,它们将被阻塞,直到第一个线程释放锁对象。
解锁操作写在 finally 子句之内是当临界区代码抛出异常,锁必须被释放。这点至关重要,否则其他线程将永远阻塞。

需要注意的一点是,上例中每个 Bank 对象都有自己的 ReentrantLock 对象。如果两个线程访问同一个 Bank 对象,那么锁将以串行方式提供服务,即第一个线程释放锁,第二个线程才能开始运行。

线程可以重复获得已经持有的锁,例如一个 Bank 对象的线程获得 bankLock 对象,当它的 transfer 方法调用 getTotalBalance 方法时,也会封锁 bankLock 对象,此时该 bankLock 对象的持有计数(hold count)为 2。持有计数(hold count) 是为了追踪 bankLock 对象的嵌套调用,即当 getTotalBalance 方法退出时,持有计数(hold count)变为 1,当 transfer 方法退出时变为 0,然后线程释放锁。综上也说明同一个锁对象是可以重入的。

条件对象

引入条件对象是为了解决如下情况:当一些线程只有在满足某些条件时才能执行临界区代码时,从而发生的某些问题。例如本例中:

 public void transfer(int from,int to,double amount) throws InterruptedException {

        bankLock.lock(); //加锁

        try {

            while (accounts[from] < amount) {
                //sufficicentFunts.await(); //条件对象调用 await 方法,该线程被阻塞,并放弃锁
            }

            accounts[from] -= amount;
            accounts[to] += amount;

            //sufficicentFunts.signalAll(); //解除其他进程的阻塞


        }finally {

            bankLock.unlock(); //解锁
        }


    }

若没有引入条件对象(被注释掉的内容),则当一个线程获得锁后发现账户余额不足,那么它将会等待其他线程向该账户注入资金,而其他线程又因为加锁机制无法获得锁对象,从而无法执行注入资金。

条件对象 Condition 对象在判断资金不足时,调用 await 方法,此时该线程被阻塞,并放弃了拥有的锁,等待另一线程执行注入资金。

 try {

            while (accounts[from] < amount) {
                sufficicentFunts.await(); //条件对象调用 await 方法,该线程被阻塞,并放弃锁
            }

当调用 await 方法后,该线程进入等待集,直到另一个线程调用同一个条件上的 singalAll() 时解除阻塞。

   accounts[from] -= amount;
            accounts[to] += amount;

            sufficicentFunts.signalAll(); //解除其他进程的阻塞

synchronized 关键字

Java 为每个对象都提供了一个内部锁,如果一个对象的方法用 synchronized 关键字声明,那么对象的锁将保护整个方法,即要想调用该方法,线程就必须获得内部的对象锁。即下面两段代码是等价的:

 public void transfer(int from,int to,double amount) throws InterruptedException {

        bankLock.lock(); //加锁

        try {

            while (accounts[from] < amount) {
                sufficicentFunts.await(); //条件对象调用 await 方法,该线程被阻塞,并放弃锁
            }

            accounts[from] -= amount;
            accounts[to] += amount;

            sufficicentFunts.signalAll(); //解除其他进程的阻塞


        }finally {

            bankLock.unlock(); //解锁
        }


    }

等价于:

 public synchronized  void transfer(int from,int to,double amount) throws InterruptedException {

        
            while (accounts[from] < amount) {
               wait();
            }

            accounts[from] -= amount;
            accounts[to] += amount;

         	notifyAll();
    }

需要注意的是,wait() 和 notifyAll() 方法等价于 intrinsicContion.await() 和 intrinsicContion.signalAll() 方法。

综上可以看出 synchronized 关键字将会使代码简洁的多,但是使用synchronized 关键字也存在一些局限:

  1. 不能中断一个试图获得锁的进程
  2. 试图获得锁时不能设置超时
  3. 每个锁仅有单一的条件可能是不够的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值