03-多线程----无锁解决并发的方案
1、问题
多线程模拟银行取钱操作,代码如下:
public interface Demo4 {
//获取当前的余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
//操作某一个账户
static void demo(Demo4 account) {
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>(1000);
//创建1000个线程,每个线程取款10
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.withdraw(10);
});
ts.add(thread);
}
//启动线程
ts.forEach(Thread::start);
//等待线程全部执行完毕
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance() + " cost : " + (end - start) / 1000_00 + "ms");
}
}
新建一个实现类,无锁实现类:
public class Demo4Impl1 implements Demo4 {
private Integer balance;
public Demo4Impl1(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
this.balance -= amount;
}
public static void main(String[] args) {
//存在线程安全问题
Demo4.demo(new Demo4Impl1(10000));
}
}
打印结果肯定是不正确的
7740 cost : 1715ms
解决方案一:
增加synchronized关键,加锁解决。
public class Demo4Impl2 implements Demo4 {
private Integer balance;
public Demo4Impl2(Integer balance) {
this.balance = balance;
}
@Override
public synchronized Integer getBalance() {
return this.balance;
}
@Override
public synchronized void withdraw(Integer amount) {
this.balance -= amount;
}
public static void main(String[] args) {
//加锁解决
Demo4.demo(new Demo4Impl2(10000));
}
}
解决方案二:
使用Java ->JUC内置的数据类型,AtomicInteger类型
public class Demo4Impl3 implements Demo4 {
//原子int类型
private AtomicInteger balance;
public Demo4Impl3(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return this.balance.get();
}
@Override
public void withdraw(Integer amount) {
// while (true){
// int prev = balance.get();
// int next = prev - amount;
// if(balance.compareAndSet(prev,next)){
// break;
// }
// }
//简化写法
balance.addAndGet(-1 * amount);
}
public static void main(String[] args) {
//无锁解决
Demo4.demo(new Demo4Impl3(10000));
}
}
0 cost : 2683ms
2、CAS和volatile
AtomicInteger内部没有锁是怎么实现线程安全的?核心代码分析
@Override
public void withdraw(Integer amount) {
while (true){
//当前值
int prev = balance.get();
//下次需要更新的值
int next = prev - amount;
//cas操作
//赋值前先拿balance和当前值prev进行比较
//如果两个值相同,才会更新next
//如果不相等则会进行下一次的 while循环
if(balance.compareAndSet(prev,next)){
break;
}
}
}
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交 换】的原子性。
但是AtomicInteger内维护的value值必须是volatile修饰的变量,看源码可以看到,构造方法赋值给value
//保证多线程访问下,数据的可见性
rivate volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
再次强调:
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原 子性)
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
3、无锁效率是否更高?
- 无锁下,即使重试失败线程仍然在进行while循环,没有停止。synchronized会让线程在没有获得到锁的时候,发生上下文切换。进入阻塞状态。重新回复线程的运行状态耗时较长。
- 无锁状态,想要保证线程的告诉运转,需要cpu的支持,如果cpu核数较少,也是会造成无法获得执行机会,仍然会导致上下文切换的发生。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- cas是乐观锁的思想,其他线程修改后继续重试
- synchronized是悲观锁思想,只有拿到锁才会进行操作,提防其他线程修改变量
cas是体现无锁并发,无阻塞并发。因为没有使用synchronized,线程不会阻塞,这是效率提升的因素之一。
但是如果线程之间竞争激烈,会造成重试几率增大频繁发生,反而影响性能。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
评论已关闭