什么是活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

百度定义:活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

造成活锁的原因

当一系列封锁不能按照其先后顺序执行时,就可能导致一些事务无限期等待某个封锁,从而导致活锁。

活锁的解决

每个线程休眠随机数,错开拿锁的时间。

活锁重现

还拿我们死锁中转账的业务,也可以使用显示锁来解决

/**
 * 活锁
 * 转账业务
 */
public class TransferMoneyDeadlock {


    public static void transfer(Account from, Account to, int amount) {
        //自旋 一直尝试到转账成功
        while (true) {
            //先锁住转账的账户
            if (from.tryLock()) {
                System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁成功");
                //休眠增加死锁产生的概率
                sleep(100);
                try {
                    System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁成功");
                    if (from.balance < amount) {
                        System.out.println("余额不足");
                        //退出
                        return;
                    } else {
                        if (to.tryLock()) {
                            //休眠增加死锁产生的概率
                            sleep(100);
                            try {
                                from.debit(amount);
                                to.credit(amount);
                                System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱成功");
                                //转账成功退出自旋
                                return;
                            } finally {
                                System.out.println("线程【" + Thread.currentThread().getId() + "】释放TO锁【" + to.name + "】成功");
                                to.unLock();
                            }
                        }
                    }
                } finally {
                    System.out.println("线程【" + Thread.currentThread().getId() + "】释放FROM锁【" + from.name + "】锁成功");
                    from.unLock();
                }
            }
            //休眠随机数字,避开同时同时拿锁释放锁
            //   sleep(new Random().nextInt(10));
        }


    }

    private static class Account {
        //显示锁
        private Lock lock = new ReentrantLock();

        String name;
        int balance;

        public Account(String name, int balance) {
            this.name = name;
            this.balance = balance;
        }

        void debit(int amount) {
            this.balance = balance - amount;
        }

        void credit(int amount) {
            this.balance = balance + amount;
        }

        //尝试获取锁
        boolean tryLock() {
            return lock.tryLock();
        }

        //尝试释放锁
        void unLock() {
            lock.unlock();
        }
    }


    /**
     * 休眠
     *
     * @param time
     */
    private static void sleep(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //创建账户A
        Account A = new Account("A", 100);
        //创建账户B
        Account B = new Account("B", 200);
        //A -> B 的转账
        executorService.execute(() -> transfer(A, B, 5));
        //B -> A 的转账
        executorService.execute(() -> transfer(B, A, 10));
        executorService.shutdown();
    }
}

输出

线程【13】获取【A】账户锁成功
线程【13】释放FROM锁【B】锁成功
线程【13】获取【B】账户锁成功
线程【12】获取【B】账户锁成功
线程【12】释放FROM锁【A】锁成功
线程【12】获取【A】账户锁成功
线程【13】获取【A】账户锁成功
线程【13】释放FROM锁【B】锁成功
....

现在同时出现两笔交易,账户A转账户B,账户B转账户 A。那么,线程一会占有账户A,线程二则会占有账户B。

结果,线程一拿不到账号B,转账没法执行,线程一结束;线程二也拿不到账号A,转账也没法执行,线程二结束。

到这里,第一轮循环结束,两笔转账都没有完成,新一轮循环开启。但在第二轮循环中,上面的过程又再次重复。如此不断地循环下去,两笔转账却一直没有完成。

我们发现 转账没有成功一直在尝试拿锁释放锁,没有做具体的事情,但是也没有阻塞,这就是活锁

避开活锁很简单休眠一个随机数字,把这行代码解开即可

//休眠随机数字,避开同时同时拿锁释放锁
 sleep(new Random().nextInt(10));
复制线程【12】获取【A】账户锁成功
线程【13】获取【B】账户锁成功
线程【13】获取【A】账户锁成功
线程【13】释放FROM锁【B】锁成功
线程【12】获取【B】账户锁成功
线程【12】从【A】账户转账到【B】账户【5】元钱成功
线程【12】释放TO锁【B】成功
线程【12】释放FROM锁【A】锁成功
线程【13】获取【B】账户锁成功
线程【13】获取【A】账户锁成功
线程【13】从【B】账户转账到【A】账户【10】元钱成功
线程【13】释放TO锁【A】成功
线程【13】释放FROM锁【B】锁成功

虽然还有部分尝试拿锁,因为我们休眠了100ms ,但是我们的代码是成功的。

总结

在开发中应该想办法避免死锁,可以尝试使用显示锁,但是显示锁要小心活锁的产生,一直在尝试拿锁释放锁,不做任何事情。