美文网首页
多线程并发之底层原理

多线程并发之底层原理

作者: 蓝色空间号 | 来源:发表于2021-09-02 16:29 被阅读0次

    并发原理、Java 内存模型 (JMM)

    image

    线程共享变量存储在主内存中,每个线程都有一个本地的私有内存,本地内存中存储着该线程以读或写共享变量的副本,本地内存是一个抽象概念,它涵盖了缓存、写缓冲区、cpu寄存器

    线程要读取一个共享变量,会先将其从主内存中读取到本地内存,然后进行运算,最后在将共享变量写回主内存

    并发产生的原因
    原因:

    1.操作的非原子性

    2.多个线程之间的内存不可见性

    解决:
    • volatile:多线程内存可见性,对单个变量的读或写操作是原子性的
    • CAS: 对单个变量的 读-改-写 操作原子性
    • synchronize: 对同步区域的代码具有原子性和可见性

    一般情况下 CAS 都是和 volatile 一起使用的,这样既保证了变量的修改的操作的原子性,又保证了变量的可见性。Java 中 Lock 还有原子类的实现就是基于 CAS 和 volatile

    volatile

    内存语义:

    • 读写具有原子性:对任意单个volatile变量的读或写具有原子性,但是对于 i++ 这样的符合操作是不具有原子性的
    • 禁止指令重排序:利用内存屏障来禁止volatile 前后的指令重新排序
    • 及时刷新内存:把缓冲区的数据刷新到主内存中,并且使其他线程的缓存区的数据无效(这样其他线程在操作变量时会重新从主内存拉取新数据)
    指令重排序

    JVM 指令重排是为了优化执行速度,在单线程下指令重排不会影响到程序的执行结果,因为具有关联关系的指令是不会被重排的,但是在多线程下指令重排就不保证最终结果的正确性了

    例如:当如果线程A指令重排,2 先于 1 执行,然后线程B就会进入if方法,但是此时 a 还未赋值,就会出现 i 的结果为 0;

    class ReorderExample{
        int a = 0;
        boolean flag = false;
        
        //线程A执行该方法
        public void writer(){
            a = 1;  ----------- 1
            flag = true;------- 2
        }
        //线程B执行该方法
        public void reader(){
            if(flag){ ----------3
                int i = a*a; ---4
            }
        }
    }
    
    
    读写原子性和立即刷新内存

    因为读或写的操作具有原子性,所以同一时间只会有一个线程对单个 volatile 进行读或写,并且写完后立刻刷新到主内存,这样的话其他线程无论何时访问主内存获取到的都是最新的值

    CAS(compareAndSwap)

    实现原理:底层指令

    //获取 Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    //该方法为 native 方法,在 Unsafe 类里面
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    

    先读取变量的值判断该变量是否是被其他线程修改过,如果没有修改就更新,并且返回true,如果发现变量被修改了则返回false,开发者可以根据返回结果进行自旋重试。

    此操作具有volatile 读和写的内存语义,即对单个变量读写的原子性,正是因为这样才能正确的读取到内存中的值,从而进行判断内存中的值和当前的预期的值是否一致。

    synchronized

    被称之为重量级锁,1.6 对其进行了优化,引入偏向锁和轻量级锁,使其不是那么重了。

    实现原理:

    synchronized 的锁是存储在对象头中的,

    将锁放入对象头中,每个线程利用CAS争抢锁,在代码执行进入到同步块的时候获取锁,结束时释放锁,如果没有锁竞争的情况下只有一个线程,还是会执行获得锁和释放锁这样的操作。

    java1.6 为了减少获得锁和释放锁带来的性能消耗,在没有锁竞争的时候使用 偏向锁 ,获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁。

    当出现锁竞争时,会升级为 轻量级锁

    如果在轻量级锁竞争时失败了,会升级为 重量级锁

    CAS、volatile、synchronized 的优缺点:

    • volatile 和 CAS 只能实现单个变量的读或写的安全性,但是如果是像更新变量这种操作包含了三步操(读-运算-写),这种情况 volatile 已经无法保证线程安全了。原因是这个操作是非原子操作
    • CAS 存在ABA问题,重试机制会一直消耗cpu资源
    • synchronized 可以对多个步骤实现线程安全操作,性能不如 volatile 和 CAS

    下面演示了,普通变量、volatile变量和Atomic变量的原子性,其中 Atomic 类的底层实现就是利用 CAS+重试

    public class AtomicTest {
    
        /**使用 volatile 修饰不能保证 volatileCount = volatileCount + 1 线程安全;*/
        private volatile static int volatileCount;
        private static int count;
    
        private static AtomicInteger atomicCount = new AtomicInteger();
    
        public static void main(String[] args){
    
            ExecutorService executorService = Executors.newCachedThreadPool();
    
            for(int i=0;i<1000;i++){
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        //非原子性操作
                        count++;
                        //原子性操作
                        atomicCount.getAndIncrement();
                        //非原子性操作
                        volatileCount = volatileCount + 1;
                    }
                });
            }
    
            //等待上面任务执行完成
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            //打印出线程池信息,检查线程是否执行完了
            println(executorService.toString());
            //volatileCount 和 count 的值有可能小于 1000
            println("count= "+count);
            println("atomicCount= "+atomicCount.get());
            println("volatileCount= "+volatileCount);
        }
    }
    
    
    说明 volatile 不能保证复合运算的安全性
    class VolatileFeaturesExample { 
        volatile long vl = 0L; // 使用volatile声明64位的long型变量 public void set(long l) { 
            vl = l; // 单个volatile变量的写 
        }
        public void getAndIncrement () { 
            vl++; //复合(多个)volatile变量的读/写
        }
        public long get() {
            return vl; // 单个volatile变量的读
        } 
    }
    
    class VolatileFeaturesExample { 
        long vl = 0L; // 64位的long型普通变量
        public synchronized void set(long l) {// 对单个的普通变量的写用同一个锁同步 
            vl = l; 
        }
        public void getAndIncrement () {
            // 普通方法调用 
            long temp = get(); //调用已同步的读方法 
            temp += 1L; // 普通写操作 
            set(temp); // 调用已同步的写方法 
        }
        public synchronized long get() { 
            // 对单个的普通变量的读用同一个锁同步 return vl; 
        } 
        
    }
    

    相关文章

      网友评论

          本文标题:多线程并发之底层原理

          本文链接:https://www.haomeiwen.com/subject/urkcwltx.html