每日一技|活锁,也许你需要了解一下

楼下小黑哥 小黑十一点半

前两天看极客时间 Java 并发课程的时候,刷到一个概念:活锁。死锁,倒是不陌生,活锁却是第一次听到。

在介绍活锁之前,我们先来复习一下死锁。下面的例子模拟一个转账业务,多线程环境,为了账户金额安全,对账户进行了加锁。

 1public class Account { 2    public Account(int balance, String card) { 3        this.balance = balance; 4        this.card = card; 5    } 6    private int balance; 7    private String card; 8    public void addMoney(int amount) { 9        balance += amount;10    }11      // 省略 get set 方法12}13public class AccountDeadLock {14    public static void transfer(Account from, Account to, int amount) throws InterruptedException {15        // 模拟正常的前置业务16        TimeUnit.SECONDS.sleep(1);17        synchronized (from) {18            System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());19            synchronized (to) {20                System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());21                // 转出账号扣钱22                from.addMoney(-amount);23                // 转入账号加钱24                to.addMoney(amount);25            }26        }27        System.out.println("transfer success");28    }2930    public static void main(String[] args) {31        Account from = new Account(100, "6000001");32        Account to = new Account(100, "6000002");3334        ExecutorService threadPool = Executors.newFixedThreadPool(2);3536        // 线程 137        threadPool.execute(() -> {38            try {39                transfer(from, to, 50);40            } catch (InterruptedException e) {41                e.printStackTrace();42            }43        });4445        // 线程 246        threadPool.execute(() -> {47            try {48                transfer(to, from, 30);49            } catch (InterruptedException e) {50                e.printStackTrace();51            }52        });535455    }56}

上述例子中,当两个线程进入转账方法,线程 1 获取账户 6000001 这把锁,线程 2 锁住了账户 6000002 锁。

接着当线程 1 想去获取 6000002 的锁时,由于这把锁已经被线程 2 持有,线程 1 将会陷入阻塞,线程状态转为 BLOCKED。同理,线程 2 也是同样状态。

1pool-1-thread-1 lock from account 60000012pool-1-thread-2 lock from account 6000002

通过日志,可以看到两个线程开始转账方法之后,就陷入等待。

synchronized获取不到锁就会阻塞,进行等待。既然这样,我们可以使用 ReentrantLock#tryLock(long timeout, TimeUnit unit)进行改造。tryLock若能获取锁,将会返回 true,若不能获取锁将会进行等待,直到满足下列条件:

  • 超时时间内获取到了锁,返回 true
  • 超时时间内未获取到锁,返回 false
  • 中断,抛出异常
    改造后代码如下:
 1public class Account { 2    public Account(int balance, String card) { 3        this.balance = balance; 4        this.card = card; 5    } 6    private int balance; 7    private String card; 8    public void addMoney(int amount) { 9        balance += amount;10    }11      // 省略 get set 方法12}13public class AccountLiveLock {1415    public static void transfer(Account from, Account to, int amount) throws InterruptedException {16        // 模拟正常的前置业务17        TimeUnit.SECONDS.sleep(1);18        // 保证转账一定成功19        while (true) {20            if (from.lock.tryLock(1, TimeUnit.SECONDS)) {21                try {22                    System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());23                    if (to.lock.tryLock(1, TimeUnit.SECONDS)) {24                        try {25                            System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());26                            // 转出账号扣钱27                            from.addMoney(-amount);28                            // 转入账号加钱29                            to.addMoney(amount);30                            break;31                        } finally {32                            to.lock.unlock();33                        }3435                    }36                } finally {37                    from.lock.unlock();38                }39            }40        }41        System.out.println("transfer success");4243    }4445    public static void main(String[] args) {46        Account from = new Account(100, "A");47        Account to = new Account(100, "B");4849        ExecutorService threadPool = Executors.newFixedThreadPool(2);5051        // 线程 152        threadPool.execute(() -> {53            try {54                transfer(from, to, 50);55            } catch (InterruptedException e) {56                e.printStackTrace();57            }58        });5960        // 线程 261        threadPool.execute(() -> {62            try {63                transfer(to, from, 30);64            } catch (InterruptedException e) {65                e.printStackTrace();66            }67        });68    }69}

上面代码使用了 while(true),获取锁失败,不断重试,直到成功。运行这个方法,运气好点,一把就能成功,运气不好,就会如下:

1pool-1-thread-1 lock from account 60000012pool-1-thread-2 lock from account 60000023pool-1-thread-2 lock from account 60000024pool-1-thread-1 lock from account 60000015pool-1-thread-1 lock from account 60000016pool-1-thread-2 lock from account 6000002

transfer 方法一直在运行,但是最终却得不到成功结果,这就是个活锁的例子。

死锁将会造成线程阻塞,程序看起来就像陷入假死一样。就像路上碰到人,你盯着我,我盯着你,互相等待对方让道,最后谁也过不去。


你愁啥?瞅你咋啦?
而活锁不一样,线程不断重复同样的操作,但也却执行不成功。还拿上面举例,这次你往左一步,他往右边一步,巧了,又碰上。然后不断循环,最后还是谁也过不去。

图片来源:知乎
分析死锁这个例子,两个线程获取的锁的顺序不一致,最后导致互相需要对方手中的锁。如果两个线程加锁顺序一致,所需条件就会一样,势必就不会产生死锁了。

我们以卡号大小为顺序,每次都给卡号比较大的账户先加锁,这样就可以解决死锁问题,代码修改如下:

 1// 其他代码不变     2public static void transfer(Account from, Account to, int amount) throws InterruptedException { 3        // 模拟正常的前置业务 4        TimeUnit.SECONDS.sleep(1); 5        Account maxAccount=from; 6        Account minAccount=to; 7        if(Long.parseLong(from.getCard())<Long.parseLong(to.getCard())){ 8            maxAccount=to; 9            minAccount=from;10        }1112        synchronized (maxAccount) {13            System.out.println(Thread.currentThread().getName() + " lock  account " + maxAccount.getCard());14            synchronized (minAccount) {15                System.out.println(Thread.currentThread().getName() + " lock  account " + minAccount.getCard());16                // 转出账号扣钱17                from.addMoney(-amount);18                // 转入账号加钱19                to.addMoney(amount);20            }21        }22        System.out.println("transfer success");23    }

对于活锁的例子,存在两个问题:

一是锁的锁超时时间都一样,导致两个线程几乎同时释放锁,重试时又同时上锁,然后陷入死循环。解决这个问题,我们可以使超时时间不一样,引入一定的随机性。

二是这里使用 while(true),实际开发中万万不能这么玩。这种情况我们需要设置最大的重试次数。

画外音:如果重试这么多次,一直不成功,但是业务却想成功。现在不成功,不要傻着一直试,先放下,记录下来,待会再重试补偿呗~

活锁的代码可以改成如下:

 1        public static final int MAX_TIME = 5; 2    public static void transfer(Account from, Account to, int amount) throws InterruptedException { 3        // 模拟正常的前置业务 4        TimeUnit.SECONDS.sleep(1); 5        // 保证转账一定成功 6        Random random = new Random(); 7        int retryTimes = 0; 8        boolean flag=false; 9        while (retryTimes++ < MAX_TIME) {10            // 等待时间随机11            if (from.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {12                try {13                    System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());14                    if (to.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {15                        try {16                            System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());17                            // 转出账号扣钱18                            from.addMoney(-amount);19                            // 转入账号加钱20                            to.addMoney(amount);21                            flag=true;22                            break;23                        } finally {24                            to.lock.unlock();25                        }2627                    }28                } finally {29                    from.lock.unlock();30                }31            }32        }33        if(flag){34            System.out.println("transfer success"); 35        }else {36            System.out.println("transfer failed");37        }38    }

总结

死锁是日常开发中比较容易碰到的情况,我们需要小心,注意加锁的顺序。活锁,碰到情况可能不常见,本质上我们只需要注意设置最大的重试次数,就不会永远陷入一直重试中。

参考链接

http://c.biancheng.net/view/4786.html

https://www.javazhiyin.com/43117.html

©著作权归作者所有:来自51CTO博客作者mb5ff59251db416的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. Node.js多线程完全指南[每日前端夜话0x43]
  2. 理解Redis单线程运行模式
  3. 代码详解Python多线程、多进程、协程
  4. 简单的php多线程解决方法
  5. PHP 多进程和多线程的优缺点
  6. PHP使用swoole实现多线程爬虫
  7. Java并发编程:线程封闭和ThreadLocal详解
  8. 100道Java并发和多线程基础面试题大集合(含解答),这波面试稳了~
  9. 使用 Thread Pool 不当引发的死锁

随机推荐

  1. Android利用调试器调试程序
  2. LinearLayout 和 RelativeLayout的属性对
  3. 老罗Android开发视频教程 (android解析xml
  4. android adb shell 命令大全
  5. Android VectorDrawable与SVG
  6. Android中margin和padding的区别
  7. Android学习路线图
  8. android 各种小项目
  9. Android流式布局FlowLayout
  10. Android: Android图形基础