Java多线程——同步(一)

        可能我们在开发项目进行过程中,通常会冒出这样的困惑:应该选择效率,还是选择质量?会不会有偷懒的思维,觉得把一些摸不清头绪,不知道怎么写的代码片段去掉,可以节省许多时间,更早的完成项目计划,其实以前我也是这么想的,但最近我开始意识到,这个问题的纠结之处不在于选择困难,而在于问题本身是个伪命题。
        什么是“质量”呢?对于我们程序员来说可能是测试通过率,变量命名,代码格式化,组件化,查找bug,程序测试等。也有可能是程序的可扩展性,服务延时,产品功能的完整程度。前一种围绕代码的问题可以看成“代码质量”问题,第二种可以看成“执行质量”。
从“代码质量”来看,走捷径的偷懒思维,其实是种十分短视的做法。含糊绕过某个问题,你可能一时觉得省事不少,但到头来,往往发现因此搅乱了系统而要花费更多的时间来一行行的检查代码,找出bug,甚至重新调整整理逻辑框架。所以牺牲代码质量换取速度通常是得不偿失的。
        相反地,高质量的代码其实是可以帮助你节省时间的。统一代码规范和变量命名,不仅可以帮到别的程序员,还可以帮到以后的你,更好地理解你现在写下的代码,经过严密思考而设计出的轻量级代码架构,则可以让你在迭代产品的时候获得更高的效率,更清晰地了解该从何入手,而不是到数据库里找需要替代的地方,而高测试通过率还可以给你充足的自信去调整代码,减少bug数量,减少QA时间。

       我们现阶段还是注重好代码的质量,执行质量就不多说啦,当然这些都是题外话,想和各位分享!接下来还是进入正题。


什么是竞争条件


       在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,将会发生什么?可能就会产生错误的对象。这样的情况通常称为竞争条件。

       我们来模拟一个有若干账户的银行,随机在这些账户之间转账,每一个账户一个线程。每一次交易,会从线程所服务的账户中随机转移一定的金额到另一个账户中。

       这是Bank类中转账的方法:

    public void transfer(int from, int to, double amount) {
        if (accounts[from] < amount) {
            return;
        }
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf("转账金额: %10.2f 转出账户:  %d 转入账户: %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf("  最后的金额: %10.2f%n", getTotalBalance());
    }

      这是转账线程类中run方法(随机选择一个目标账户和一个随机账户调用转账方法,然后睡眠):

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

      当它运行时,我们不清楚某一时间某一个账户有多少钱,但是,所有账户的总金额应该是保持不变的。

      下面是例子的完整代码:

      Bank类:

/**
 * @author XzZhao
 */
public class Bank {

    private final double[] accounts;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
    }

    public void transfer(int from, int to, double amount) {
        if (accounts[from] < amount) {
            return;
        }
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf("转账金额: %10.2f 转出账户:  %d 转入账户: %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf("  最后的金额: %10.2f%n", getTotalBalance());
    }

    public double getTotalBalance() {
        double sum = 0;
        for (double a : accounts) {
            sum += a;
        }
        return sum;
    }

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

}

     TransferRunnable类:

/**
 * @author XzZhao
 */
public class TransferRunnable implements Runnable {

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

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

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

/**
 * @author XzZhao
 */
public class UnsynchBankTest {
    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);
        for (int i = 0; i < NACCOUNTS; i++) {
            TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
            Thread t = new Thread(r);
            t.start();
        }
    }
}
程序出错:总金额发生变化


锁对象


有两种机制防止代码块受并发访问的干扰。一个是ReentrantLock类,还有一个是synchronized关键字。

用ReentrantLock保护代码块的结构:

        Lock myLock = new ReentrantLock();
        myLock.lock();
        try {
            // work code
        } finally {
            myLock.unlock();
        }

        这一结构确保任何时刻只有一个线程进入。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。

        使用一个锁来保护Bank类的transfer方法:

/**
 * @author XzZhao
 */
public class Bank {

    ...
    private final Lock     bankLock = new ReentrantLock();

    public void transfer(int from, int to, double amount) {
        if (accounts[from] < amount) {
            return;
        }
        bankLock.lock();
        try {
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf("转账金额: %10.2f 转出账户:  %d 转入账户: %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf("  最后的金额: %10.2f%n", getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }
    ...
}

        再次运行程序后,发现不会出现错误,线程之间不会互相影响了。

        锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。所以,被一个锁保护的代码可以调用另一个使用相同的锁的方法。


条件对象


       当线程进入区域,却发现在某一条件满足之后才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

       我们来修改一下例子,我们使用条件对象来避免选择没有足够资金的账户作为转出账户。

      Bank类:

/**
 * @author XzZhao
 */
public class Bank {

    private final double[]  accounts;
    private final Lock      bankLock;
    private final Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; 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 转出账户:  %d 转入账户: %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf("  最后的金额: %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;
    }

}
 TransferRunnable类:

/**
 * @author XzZhao
 */
public class TransferRunnable implements Runnable {

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

    public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
        super();
        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) {
            Thread.currentThread().interrupt();
        }
    }
}


     

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值