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

前两天看极客时间 Java 并发课程的时候,刷到一个概念:活锁。死锁,倒是不陌生,活锁却是第一次听到。
在介绍活锁之前,我们先来复习一下死锁,下面的例子模拟一个转账业务,多线程环境,为了账户金额安全,对账户进行了加锁。
public class Account {
public Account(int balance, String card) {
this.balance = balance;
this.card = card;
}
private int balance;
private String card;
public void addMoney(int amount) {
balance += amount;
}
// 省略 get set 方法
}
public class AccountDeadLock {
public static void transfer(Account from, Account to, int amount) throws InterruptedException {
// 模拟正常的前置业务
TimeUnit.SECONDS.sleep(1);
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
// 转出账号扣钱
from.addMoney(-amount);
// 转入账号加钱
to.addMoney(amount);
}
}
System.out.println(“transfer success”);
}

public static void main(String[] args) {
    Account from = new Account(100, "6000001");
    Account to = new Account(100, "6000002");

    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 线程 1
    threadPool.execute(() -> {
        try {
            transfer(from, to, 50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    // 线程 2
    threadPool.execute(() -> {
        try {
            transfer(to, from, 30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });


}

}
复制代码上述例子中,当两个线程进入转账方法,线程 1 获取账户 6000001 这把锁,线程 2 锁住了账户 6000002 锁。
接着当线程 1 想去获取 6000002 的锁时,由于这把锁已经被线程 2 持有,线程 1 将会陷入阻塞,线程状态转为 BLOCKED。同理,线程 2 也是同样状态。
pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002
复制代码通过日志,可以看到两个线程开始转账方法之后,就陷入等待。
synchronized 获取不到锁就会阻塞,进行等待。既然这样,我们可以使用 ReentrantLock#tryLock(long timeout, TimeUnit unit) 进行改造。tryLock 若能获取锁,将会返回 true,若不能获取锁将会进行等待,直到满足下列条件:

超时时间内获取到了锁,返回 true
超时时间内未获取到锁,返回 false
中断,抛出异常

改造后代码如下:
public class Account {
public Account(int balance, String card) {
this.balance = balance;
this.card = card;
}
private int balance;
private String card;
public void addMoney(int amount) {
balance += amount;
}
// 省略 get set 方法
}
public class AccountLiveLock {

public static void transfer(Account from, Account to, int amount) throws InterruptedException {
    // 模拟正常的前置业务
    TimeUnit.SECONDS.sleep(1);
    // 保证转账一定成功
    while (true) {
        if (from.lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
                if (to.lock.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
                        // 转出账号扣钱
                        from.addMoney(-amount);
                        // 转入账号加钱
                        to.addMoney(amount);
                        break;
                    } finally {
                        to.lock.unlock();
                    }

                }
            } finally {
                from.lock.unlock();
            }
        }
    }
    System.out.println("transfer success");

}

public static void main(String[] args) {
    Account from = new Account(100, "A");
    Account to = new Account(100, "B");

    ExecutorService threadPool = Executors.newFixedThreadPool(2);

    // 线程 1
    threadPool.execute(() -> {
        try {
            transfer(from, to, 50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    // 线程 2
    threadPool.execute(() -> {
        try {
            transfer(to, from, 30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

}
复制代码上面代码使用了 while(true),获取锁失败,不断重试,直到成功。运行这个方法,运气好点,一把就能成功,运气不好,就会如下:
pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002
pool-1-thread-2 lock from account 6000002
pool-1-thread-1 lock from account 6000001
pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002
复制代码transfer 方法一直在运行,但是最终却得不到成功结果,这就是个活锁的例子。
死锁将会造成线程阻塞,程序看起来就像陷入假死一样。就像路上碰到人,你盯着我,我盯着你,互相等待对方让道,最后谁也过不去。

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

分析死锁这个例子,两个线程获取的锁的顺序不一致,最后导致互相需要对方手中的锁。如果两个线程加锁顺序一致,所需条件就会一样,势必就不会产生死锁了。
我们以卡号大小为顺序,每次都给卡号比较大的账户先加锁,这样就可以解决死锁问题,代码修改如下:
// 其他代码不变
public static void transfer(Account from, Account to, int amount) throws InterruptedException {
// 模拟正常的前置业务
TimeUnit.SECONDS.sleep(1);
Account maxAccount=from;
Account minAccount=to;
if(Long.parseLong(from.getCard())<Long.parseLong(to.getCard())){
maxAccount=to;
minAccount=from;
}

    synchronized (maxAccount) {
        System.out.println(Thread.currentThread().getName() + " lock  account " + maxAccount.getCard());
        synchronized (minAccount) {
            System.out.println(Thread.currentThread().getName() + " lock  account " + minAccount.getCard());
            // 转出账号扣钱
            from.addMoney(-amount);
            // 转入账号加钱
            to.addMoney(amount);
        }
    }
    System.out.println("transfer success");
}

复制代码对于活锁的例子,存在两个问题:
一是锁的锁超时时间都一样,导致两个线程几乎同时释放锁,重试时又同时上锁,然后陷入死循环。解决这个问题,我们可以使超时时间不一样,引入一定的随机性。
二是这里使用 while(true),实际开发中万万不能这么玩。这种情况我们需要设置最大的重试次数。

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

活锁的代码可以改成如下:
public static final int MAX_TIME = 5;
public static void transfer(Account from, Account to, int amount) throws InterruptedException {
// 模拟正常的前置业务
TimeUnit.SECONDS.sleep(1);
// 保证转账一定成功
Random random = new Random();
int retryTimes = 0;
boolean flag=false;
while (retryTimes++ < MAX_TIME) {
// 等待时间随机
if (from.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
if (to.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
// 转出账号扣钱
from.addMoney(-amount);
// 转入账号加钱
to.addMoney(amount);
flag=true;
break;
} finally {
to.lock.unlock();
}

                }
            } finally {
                from.lock.unlock();
            }
        }
    }
    if(flag){
        System.out.println("transfer success"); 
    }else {
        System.out.println("transfer failed");
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值