关于并发编程:并发编程CAS与Volatile

39次阅读

共计 3388 个字符,预计需要花费 9 分钟才能阅读完成。

第一、并发编程三大个性
原子性: 指一系列操作是不可分割的,一旦执行则整个过程将会一次性全副执行实现,不会停留在中间状态。(有点相似于事务的概念)。
举例:例如 A 向 B 汇款 1000 元,那么就须要有两个操作,一个是 A 账户减 1000 元,另一个是 B 账户减少 1000 元,如果这个过程中任何一个操作呈现故障,都是不合乎规矩的也是不能保障汇款人和收款人的财产平安。换句话说,如果想要保障每次转账都不会造成单方任何一方的财产损失,咱们必须要保障操作的原子性。要么都做,要么都不做。

可见性: 多个线程拜访同一共享数据的时候,如果某一个线程批改了此共享数据,那么其余线程可能立刻看到此数据的扭转。即批改可见。

有序性: 代码执行时的程序与语句程序统一。也就是说执行前不重排。指令重排序不会影响单个线程的执行,然而会影响到线程并发执行的正确性

要想并发程序正确地执行,必须要保障原子性、可见性以及有序性。只有有一个没有被保障,就有可能会导致程序运行不正确。
第二、CAS
定义一个账户接口:Account .java

import java.util.ArrayList;
import java.util.List;
/**
 * 定义一个账户接口
 * @author shixiangcheng
 * 2019-12-17
 */
public interface Account {
    // 获取余额
    Integer getBalance();
    // 取款
    void withdraw(Integer amount);
    // 办法内启动 1000 个线程,每个线程做 -10 元的操作。若初始余额为 10000,那么正确后果该当为 0
    static void demo(Account account) {List<Thread> ts=new ArrayList<Thread> ();
        long start=System.currentTimeMillis();
        for(int i=0;i<1000;i++) {ts.add(new Thread(()->{account.withdraw(10);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {t.join();
            } catch (InterruptedException e) {e.printStackTrace();
            }
        });
        long end=System.currentTimeMillis();
        System.out.println(account.getBalance()+"cost:"+(end-start)+"ms");
    }
}

2. 接口实现类 AccountImpl.java

import java.util.concurrent.atomic.AtomicInteger;
/**
 * 接口实现类
 * @author shixiangcheng
 * 2019-12-17
 */
public class AccountImpl implements Account {
    private AtomicInteger balance;// 账户余额
    public AccountImpl(int balance) {// 通过构造方法给一个默认的余额
        this.balance = new AtomicInteger(balance);
    }
    @Override
    public Integer getBalance() {return balance.get();
    }
    @Override
    public void withdraw(Integer amount) {
        // 一直尝试,直到胜利为止
        while(true) {int prev=balance.get();
            int next=prev-amount;
            /** 比拟替换: 在 set 前先比拟 prev 和以后值?
             * 不统一,next 作废,cas 返回 false 标识失败
             * 统一, 以 next 设置为新值, 返回 true 标识胜利
             */
            if(balance.compareAndSet(prev, next)) {break;}
        }
    }
}

测试类 Test.java

/**
 * 测试类
 * @author shixiangcheng
 * 2019-12-17
 */
public class Test {public static void main(String [] args) {Account.demo(new AccountImpl(10000));
    }
}

测试后果
0 cost: 131ms
总结:compareAndSet 的简称就是 CAS,它必须是原子操作。CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都可能保障比拟 - 替换的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行结束,再开启总线,这个过程中不会被线程的调度机制所打断,保障了多个线程对内存操作的准确性,是原子的。

第三、Volatile

/**
 * 测试可见性
 * @author shixiangcheng
 * 2019-12-17
 */
public class TestVolatile{
    static boolean run=true;
    static long i=0;
    public static void main(String [] args) throws InterruptedException {System.out.println("A");
        Thread t=new Thread(()-> {System.out.println("1");
            while(run) {i++;}
            System.out.println("2");
        });
        t.start();
        Thread.sleep(1000);
        run=false;
        System.out.println("B");
    }
}

浏览代码揣测,程序执行后果输入:A 1 B 2,然而理论执行后执行后果:

从执行后果看,右上角还有一个红色的标识,标识代码没有执行完结。t 线程没有完结,而是始终在 while 中循环,因为其并没有感知到 run 的值曾经被批改了。也就是主线程对共享变量的批改,对其它线程不可见。这将会造成线程不平安。
java 线程内存模型如下:

每个线程有独立的工作内存 (寄存器和高速缓存合称工作内存),为进步执行效率,线程会将数据从主存复制一份到工作内存,线程操作的是本人的工作内存,而不会间接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅扭转了本人的工作内存的变量的正本,那么对于其余线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是应用旧的值的话,同样的也能够列为不可见。对于 jvm 来说,主内存是所有线程共享的 java 堆,而工作内存中的共享变量的正本是从主内存拷贝过来的,是线程公有的局部变量,位于 java 栈中。
解决方案:
static volatile boolean run=true;
在 JVM 底层 volatile 是采纳“内存屏障”来实现的。察看退出 volatile 关键字和没有退出 volatile 关键字时所生成的汇编代码发现,退出 volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),
** 内存屏障会提供 3 个性能:
1. 它确保指令重排序时不会把其前面的指令排到内存屏障之前的地位,也不会把后面的指令排到内存屏障的前面;即在执行到内存屏障这句指令时,在它后面的操作曾经全副实现;
2. 它会强制将对缓存的批改操作立刻写入主存;

  1. 如果是写操作,它会导致其余 CPU 中对应的缓存行有效。
    第四、CAS 的特点 **
    联合 CAS 和 volatile 能够实现无锁并发,实用于竞争不强烈,多核 CPU 的场景下。
    CAS 是基于乐观锁的思维。
    synchronized 是基于乐观锁的思维。
    CAS 体现的是无锁并发,无阻塞并发。
    因为没有 synchronized,所以线程不会陷入阻塞,这是效率晋升的因素之一。但如果竞争强烈,能够想到重试必然频繁产生,反而效率会受到影响。
    第五、为什么无锁效率高
    无锁状况下,即便重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有取得锁的时候,产生上下文切换,进入阻塞。但无锁状况下,因为线程要放弃运行,须要额定 CPU 反对,如果没有,线程高速运行也就无从谈起,尽管不会进入阻塞,但因为没有分到工夫片,依然会进入可运行状态,还是会导致上下文切换。

留神:应用 CAS 时,线程数不能够超过 CPU 的外围数。

正文完
 0