多线程下对共享资源的保护
有一个Account接口,一个抽象方法withdraw(int money) 取款方法,一个抽象方法getBalence()获取余额。现在需要保证对共享资源balance的线程安全性。
public interface Account{
void withdraw(Integer money);
Integer getBalance();
}
如果不使用锁会出现线程安全问题
class UnsafeAccount implements Account{
private Integer balance;
void withdraw(Integer money){
balance -= money;
}
}
书写测试方法
/**
* 多线程对共享资源访问的测试类
* @param accountMethod account的多种实现
*/
public static void demo(Account accountMethod){
Account account = accountMethod;
ArrayList<Thread> accountList= new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
accountList.add(new Thread(()->{
account.withDraw(10);
System.out.println("取了10元余额为" + account.getBalance() + ",还剩"+ countDownLatch.getCount() + "次");
countDownLatch.countDown();
}));
}
long nacos = System.nanoTime();
for (int i = 0; i < 1000; i++) {
accountList.get(i).start();
}
countDownLatch.await();
long nacos1 = System.nanoTime();
System.out.println("余额 = " + account.getBalance());
System.out.println("消耗时间:" + (nacos1 - nacos)/1000_000 + "ms");
}
- 由于
balance -= money
这个指令不具有原子性,所以会出现线程安全问题。比如A线程读取到变量balance
,此时B线程也读取到balance
并且已经完成了balance-money
操作,此时A线程再对变量balance
进行运算时就会导致balance
本来应该减了两次,实际上只减了一次。这是不满足原子性的体现。 - 再比如线程A对
balance
变量进行修改后,balance
还没写进主存中。此时B线程从主存中读取到了还没修改完成的balance
变量进行操作也会导致数据不一致的结果。这是不满足可见性的体现。 - 由于本例子中只有一行操作,所以有序性暂时可以保证。
加锁保护共享变量
- 我们使用sychronized实现,只需要在
withdraw
方法加一个sychronized
就可以了 - 为了保证读操作的安全性,需要在
getBalance()
方法也加上sychronized
来保证读取的时候不会有其他线程对balance
进行修改,防止读到了脏数据。
无锁保护共享变量
主要使用到CAS方式来实现。
class SafeAccount implements Account{
private AtomicInteger balance;
public SafeAccount(int balance){
this.balance = new AtomicInteger(balance);
}
@Override
public void withDraw(Integer amount) {
while (true){
//获取余额最新值
int prev = balance.get();
balance.getAndAdd(5)
int next = prev - amount;
/**
* 比较prev,是否等于balance的值
* 如果相等,修改balance的值为next
* 如果不相等,进行重试
*/
if(balance.compareAndSet(prev,next)){
break;
}
}
}
public static void demo() throws InterruptedException {
Account.demo(new SafeAccount(10000));
}
@Override
public Integer getBalance() {
return balance.get();
}
}
CAS和volatile可以实现无锁并发,适用于线程数少、多个cpu的场景。CAS的方法相比传统的加锁方法,虽然是用到了自旋,但是效率比加锁是要高的,因为线程是一直处于活跃状态。而加锁后线程会进入到Block状态,获取到锁后才处于就绪状态来抢夺cpu时间片,这种线程状态的切换开销是比较大的。
- CAS是基于乐观锁的思想,sychronized是居于悲观锁的思想
- CAS体现的是无锁并发、无阻塞并发。如果竞争激烈,重试的情况会频繁发生,反而效率会受到影响。