Android多线程编程 - 线程同步-锁

前言

在多线程应用中,两个或者两个以上的线程需要共享对一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。竞争条件最容易理解的例子如下:比如火车站售卖火车票,火车票是一定的,但卖火车的窗口到处都有,每个窗口就相当于一个线程。这么多的线程共用所有的火车票资源,如果不使用同步是无法保证原子性的。在一个时间点上,两个线程同时使用火车票资源,那么其取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦。解决方法如下:当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个要用这个资源的线程。这样就不会出现上述情况了。

1. 重入锁

重入锁ReentrantLock是Java SE5.0引入的,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。用ReentrantLock保护代码块的结构如下所示:

    ReentrantLock mLock = new ReentrantLock();
    int i = 1;
    public void changeNumber(){
        mLock.lock();
        try {
            i++;
        }finally {
            mLock.unlock();
        }
    }

这一结构确保任何时刻只有一个线程进入临界区,临界区就是在同一时刻只能有一个任务访问的代码区。一旦一个线程封锁了锁对象,其它任何线程都无法进入Lock语句。把解锁的操作放在finally中是十分必要。如果在临界区发生了异常,锁是必须要释放的。否则其它线程将会永远被阻塞。进入临界区,却发现在某一个条件满足后,它才能执行。这时可以是使用一个条件对象来管理那些已经获得了一把锁,但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面的例子来说明为何需要条件对象。假设一个场景需要使用支付宝转账。我们首先写了支付宝的类,它的构造方法需要传入支付宝账户的数量和每个账户的账户金额。

    public class Alipay {
        private double[] accounts;
        private ReentrantLock alipayLock;
        public Alipay(int n, double money){
            accounts = new double[n];
            alipayLock = new ReentrantLock();
            for (int i = 0; i < accounts.length; i++){
                accounts[i] = money;
            }
        }
    }

接下来我们要转账,写一个 转账的方法,from是转账方,to是接受方,amount是转账金额,如下所示:

    public void transfer(int form, int to, int amount){
        alipayLock.lock();
        try {
            while (accounts[form] < amount){
                //wait
            }
        }finally {
            alipayLock.unlock();
        }
    }

结果我们发现转账方的余额不足;如果有其它线程给这个转账方再转足够的钱,就可以转账成功了。但是这个线程已经获取了锁,它具有排它性,别的线程无法获取锁来进行存款操作。这就是我们需要引入条件对象的原因。一个锁对象拥有多个相关的条件对象,可以用newCondition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁。整理以上代码,并加入条件对象,如下:

    public class Alipay {
        private double[] accounts;
        private ReentrantLock alipayLock;
        private Condition condition;
        public Alipay(int n, double money){
            accounts = new double[n];
            alipayLock = new ReentrantLock();
            //得到条件对象
            condition = alipayLock.newCondition();
            for (int i = 0; i < accounts.length; i++){
                accounts[i] = money;
            }
        }

        public void transfer(int form, int to, int amount) throws InterruptedException{
            alipayLock.lock();
            try {
                while (accounts[form] < amount){
                    //阻塞当前线程,并放弃锁
                    condition.await();
                }
            }finally {
                alipayLock.unlock();
            }
        }
    }

一旦一个线程调用await方法,它就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的signalAll方法时为止。当另一个线程转账给我们此前的转账方时,只要调用signalAll方法,就会重新激活因为这一条件而等待的所有线程。代码如下所示:

    public void transfer(int form, int to, int amount) throws InterruptedException{
        alipayLock.lock();
        try {
            while (accounts[form] < amount){
                //阻塞当前线程,并放弃锁
                condition.await();
            }
            accounts[form] = accounts[form] - amount;
            accounts[to] = accounts[to] + amount;
            condition.signalAll();
        }finally {
            alipayLock.unlock();
        }
    }

当调用signalAll方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞争实现对对象的访问。还有一个方法是signal,它则是随机解除某个线程的阻塞。如果该线程仍然不能运行,则再次被阻塞。如果没有其它线程再次调用signal,那么系统就死锁了。

2. 同步方法

Lock接口和Condition接口为程序设计人员提供了高度的锁定控制,然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从Java1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。代码如下:

    public synchronized void method(){
        
    }

等价于:

    ReentrantLock mLock = new ReentrantLock();
    public void method(){
        mLock.lock();
        try {
            ...
        }finally {
            mLock.unlock();
        }
    }

对于前面支付宝转账的例子,我们可以将Alipay类的transfer方法声明为synchronized,而不是使用一个显示的锁。内部对象只有一个相关条件,wait方法将一个线程添加到等待集中,notifyAll或者notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.awaita(),notifyAll等价于condition.signalAll()。所以,前面例子中的transefer方法也可以这样写:

    public synchronized void transfer(int form, int to, int amount) throws InterruptedException {
        while (accounts[form] < amount) {
            //阻塞当前线程,并放弃锁
            wait();
        }
        accounts[form] = accounts[form] - amount;
        accounts[to] = accounts[to] + amount;
        notifyAll();
    }

3. 同步代码块

  • 每一个Java对象都有一把锁,线程可以调用同步方法来获得锁。还有另一种机制可以获得锁,那就是使用同步代码块,如下所示:
    synchronized(obj){
        
    }

其获得了obj的锁,obj指的是一个对象。下面来看Alipay类,我们用同步代码块进行改写。

    public class Alipay {
        private double[] accounts;
        private Object lock = new Object();
        
        public Alipay(int n, double money){
            accounts = new double[n];
            for (int i = 0; i < accounts.length; i++) {
                accounts[i] = money;
            }
        }
        public void transfer(int from, int to, int amount){
            synchronized (lock){
                accounts[from] = accounts[from] - amount;
                accounts[to] = accounts[to] + amount;
            }
        }
    }

在这里创建了一个名为lock的Object类,为的是使用Object类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用。一般实现同步最好用java.util.concurre包下的类,比如阻塞队列。如果同步方法适合你的程序,那么请尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率。如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值