多线程并发之底层原理

并发原理、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; 
    } 
    
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容