多线程与并发-并发编程三大特性(可见性 有序性 原子性)

volatile 可见性

volatile可见性原理- 缓存一致性 -MESI Cache

volatile 关键字,使一个变量在多个线程间可见

* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道

* 使用volatile关键字,会让所有线程都会读到变量的修改值

* 在下面的代码中,running是存在于堆内存的t对象中

* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去

* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行

* 使用volatile,将会强制所有线程都去堆内存中读取running的值

* * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

1.1 现像

public class T01_HelloVolatile {
    private static /*volatile*/ boolean running = true;

    private static void m() {
        System.out.println("m start");
        while (running) {
            //System.out.println("hello");
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) throws IOException {

        new Thread(T01_HelloVolatile::m, "t1").start();

        SleepHelper.sleepSeconds(1);

        running = false;

        System.in.read();
    }
}

打印结果

1.2 现像

/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值
 * 
 * 在下面的代码中,running是存在于堆内存的t对象中
 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
 * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
 * 
 * 使用volatile,将会强制所有线程都去堆内存中读取running的值
 * 
 * 可以阅读这篇文章进行更深入的理解
 * http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
 * 
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;

import java.util.concurrent.TimeUnit;

public class T01_HelloVolatile {
    /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
    void m() {
        System.out.println("m start");
        while(running) {
        }
        System.out.println("m end!");
    }
    
    public static void main(String[] args) {
        T01_HelloVolatile t = new T01_HelloVolatile();
        
        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        t.running = false;
    }
    
}

1.2 分析

正常上面的代码会打印 start 和end

但是实际只打印了start 为什么?

开始的时候主线程和m线程都去缓存里面读取running的copy到各自的线程里面,每次循环都只会和当前线程的缓存里面读取,不会去缓存里面读取

结论 默认一个变量在不同的线程中在一个线程里面修改了在宁一个线程是见不到的

1.3解决办法

volatile 关键字,使一个变量在多个线程间可见

1.4 volatile 可见性详细分析

1.4.1 是否只有volatile 才能保证可见性

System.out.println("hello");

///这里打开,发现能打印m end!了,为什么进去看了synchronized 同步锁,synchronized在一定程度下会保证线程间的可见性 但不可能每个线程里面都加一句println

1.4.2 volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
import com.mashibing.util.SleepHelper;

public class T02_VolatileReference {

    private static class A {
        boolean running = true;

        void m() {
            System.out.println("m start");
            while (running) {
            }
            System.out.println("m end!");
        }
    }
    private volatile static A a = new A();
    public static void main(String[] args) {
        new Thread(a::m, "t1").start();
        SleepHelper.sleepSeconds(1);
        a.running = false;
    }
}
1.4.3 三级缓存

图解

1.4.4 缓存行的基本概念

局部性和空间性读取一块数据的时候,很可能会用到和他相岭的数据,为了提高效率就读取就按块读取

每次读取一整块的数据叫做缓存行 cache line ,在我们内存中的数据都是按照一块一块来读取的,那块的大小到底多大? 64字节

1.4.5 缓存行一致性协议

程序认识

实例1

package com.mashibing.juc.c_001_02_FalseSharing;

import java.util.concurrent.CountDownLatch;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {//这个就相当于缓存行没有在他里面做填充所消耗的时间
        //private long p1, p2, p3, p4, p5, p6, p7;
        public long x = 0L; //8bytes
        //private long p9, p10, p11, p12, p13, p14, p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }

            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }

            latch.countDown();
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}
消耗时间---466

ublic class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
        private long p1, p2, p3, p4, p5, p6, p7;
        public long x = 0L; //8bytes
        private long p9, p10, p11, p12, p13, p14, p15;
    }

消耗时间---437 做了填充时间变快了,T行填满了,在做数据修改的时候分别修改
arr[0] = new T();
arr[1] = new T();不同的行,就不需要消耗通知和同步不同cpu里面缓存行的数据更改

为了保持两边cpu里面缓存行数据的一致性得有一种协议来保持,一边修改了得要通知和同步宁一边也要修改,那么这种协议叫做缓存一致性协议,和volatile没关系

1.4.6 认识Disruptor中缓存行对齐的写法和认识Contended

1.4.6 认识硬件层面的缓存一致性 -MESI Cache -- 硬件层面MESI保证不同cpu之间数据确保缓存行一致性的保证

1.4.6 为什么缓存一行是64字节

1.4.8总结volatile保障线程可见性

• 缓存行

• 缓存一致性协议

volatile 有序性 一定是单线程内的有序性,和锁这些没关系

volatile有序性的原理

volatile屏蔽指令重排序-volatile修饰的任何内存在他前面和后面都加了内存屏障

MESI

cpu 级别是通过缓存一致性协议保持数据一致性的;

2.1现象分析

package com.mashibing.juc.c_001_03_Ordering;

/**
 * 本程序跟可见性无关,曾经有同学用单核也发现了这一点
 */

import java.util.concurrent.CountDownLatch;

public class T01_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        for (long i = 0; i < Long.MAX_VALUE; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(2);

            Thread one = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    x = b;

                    latch.countDown();
                }

            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;

                    latch.countDown();
                }
            });
            one.start();
            other.start();
            latch.await();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            }
        }
    }

}

2.2 运行结果

2.3 分析

不管下面这两句话所在的线程1和线程2的执行顺序怎么打乱都不会出现 x ==0 y ==0 ,只有在当这两线程里面的语句内的执行顺序发生了(语句1 先执行 x = b;再执行 a = 1; 语句2 先执行 y = a;再执行 b = 1; )变化才会出现x ==0 y ==0;


public void run() {
                    a = 1;
                    x = b;

                    latch.countDown();
                }

2 
public void run() {
                    b = 1;
                    y = a;

                    latch.countDown();
                }

2.4 那么上面的情况为什么会出现线程内部的执行顺序会发生变化不是顺序执行的?

结论 两个语句之间有一定的概率会交换顺序执行

原因就是就是每一条语句到底层都是一条汇编指令

为什么会出现乱序?cpu存在乱序执行的情况

简单说就是为了提高执行效率

cpu 执行效率或者寄存器效率要比内存100倍还多

我的第一条指令去读取数据返回的过程中,我完全都可以先执行第二条指令,第二条指令寄存器执行速度非常快,微观的角度讲我的第二条指令就跑到第一条指令前面先执行完,底层的角度讲,cpu为了提高效率的进行优化机制,所以就有了乱序的存在,但并不是随随变变的都会乱序,

什么情况下会出现乱序?当前后两条语句没有依赖关系就有可能发生乱序,

但不影响单线层的最终一致性

2.5 什么是单线程的乱序执行和as-if serial ?

as – if serial?看上去像是序列化执行

只要不影响单线程的最终一致性 这两句话和指令是可以换顺序的,这就是乱序存在的原则

2.6 实例演示

下边的小程序存在的问题

//两个问题 1 线程之间的可见性 ready = true;设为true后,程序可能并不会马上停止,MESI 主动性, Thread.yield();同步刷新你可能也能立即看到他的输出

//严谨点volatile boolean

//问题2 有序性问题 乱序问题number = 42; ready = true;可能会发生乱序,number输出就是0

package com.mashibing.juc.c_001_03_Ordering;

public class T02_NoVisibility {
    private static volatile boolean ready = false;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws Exception {
        Thread t = new ReaderThread();
        t.start();
        number = 42;
        ready = true;
        t.join();
        //两个问题 1 线程之间的可见性 ready = true;设为true后,程序可能并不会马上停止,MESI 主动性,  Thread.yield();同步刷新你可能也能立即看到他的输出
        //严谨点volatile boolean
        //问题2 有序性问题 乱序问题number = 42; ready = true;可能会发生乱序,number输出就是0
    }
}

2.6 对象的创建过程

三步 new 成员变量的默认值 m = 0;这个时候是半初始化状态

invokespecial 变量的值变为8

astore t和m建立关联

2.7 问题解析程序

System.out.println(this.num) //num这里输出的值可能是中间状态的值0

package com.mashibing.juc.c_001_03_Ordering;

public class T03_ThisEscape {

    private int num = 8;


    public T03_ThisEscape() {
        new Thread(() -> 
                System.out.println(num) //num这里输出的值可能是中间状态的值0
        ).start();
    }

    public static void main(String[] args) throws Exception {
        new T03_ThisEscape();
        System.in.read();
    }
}

所以注意不要在构造方法里面new线程并启动

package com.mashibing.juc.c_001_03_Ordering;

public class T03_ThisEscape {

    private int num = 8;

    Thread t ;
    public T03_ThisEscape() {
        t = new Thread(() ->
                System.out.println(this.num) //num这里输出的值可能是中间状态的值0
        );
    }

    public  void  start(){
        t.start();
    }
    public static void main(String[] args) throws Exception {
        new T03_ThisEscape();
        System.in.read();
    }
}

2.8 8连问 Object o = new Object ()连问

  1. 请解释一下对象的创建过程?(半初始化)

new 申请内存空间- 成员变量的默认值 m = 0;这个时候是半初始化状态

invokespecial 调用他的构造方法 变量的值变为8

astore t和m建立关联 将值赋给分配的内存

  1. 加问DCL和volatile问题?(指令重排)

volatile 保证线程间的可见性和禁止指令重排

volatile屏蔽指令重排序-volatile修饰的任何内存在他前面和后面都加了内存屏障

DCL 编程技巧 double check lock 单列的双重校验-提高效率

//3. 获取单例对象的静态方法
    public static  Singleton_04 getInstance(){

        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null){ Double check Lock
            synchronized (Singleton_04.class){
                //第二次判断,抢到锁之后再次进行判断,判断是否为null
                if(instance == null){

                    instance = new Singleton_04();
                    /**
                     * 上面的创建对象的代码,在JVM中被分为三步:
                     *      1.分配内存空间
                     *      2.初始化对象
                     *      3.将instance指向分配好的内存空间
                     */

                }
            }
        }
        return instance;
    }
  1. 对象在内存中的存贮布局?(对象与数组的存储不同)

  1. 对象头具体包括什么?

  1. (包括

  1. markword

  1. class pointer

  1. synchronized 锁信息以及hashCode()是Identity hashCode

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>${version}</version>
</dependency>
//用jol来查看java的内存对象布局

public static void main(String[] args) {
        Object O = new Object();
        String s = ClassLayout.parseInstance(O).toPrintable();
        System.out.println(s);
    }
//打印结果

  1. 对象怎么定位?(直间 间接)

定位就是怎么通过T找到new出来的对象

  1. 对象怎么分配?(栈上-线程本地-Eden-old)

  1. Object o = new Object()(在内存中占用多少内存)

详细回答 看你有没有压缩 class pointer oops 内存是不是32g以下或者以上然后告诉你占用多少内存

20 个字节

  1. 新问题:为什么hotspot 不实用c++对象来代表java对象?

  1. 新问题:Class对象是在堆还是在方法区?

方法区

2.9 内存屏障

3. 0 volatile在hotspot中的实现

volatile jvm 前后加了四个内存屏障

hotspot通过lock实现

三 原子性

即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行

现象:

正常应该是1000000,为什么会出现下面的情况653444,因为n++;操作是先从缓存里面读取数据到寄存器进行++操作,操作完写回缓存,但是这个操作在第一个线程

还没完成的时候其他无数线程又去从内存读取数据进行++,导致最后协会缓存的数据不一致

//原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行

public class T00_00_IPlusPlus {
    private static long n = 0L;

    public static void main(String[] args) throws Exception {

        //Lock lock = new ReentrantLock();

        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
//                    synchronized (T00_00_IPlusPlus.class) {
                        //lock.lock();
                        n++;
                        //lock.unlock();
//                    }
                }
                latch.countDown();
            });
        }
        for (Thread t : threads) {
            t.start();
        }

        latch.await();

        System.out.println(n);

    }
}

结果:

当判断不了一条语句是不是原子性,给他上锁就行

要保证最终的结果100000是正确的必须满足三条

volatile 保证可见性和有序性

加锁synchronized 保证原子性

3.1 上锁的本质

上锁的本质是把并发编程序列化

把原来所谓的并发操作给他序例化,当然效率降低了

3.2 上锁synchronized ///synchronized保证了多线程的序列话和可见性

 synchronized (T00_00_IPlusPlus.class) {//synchronized保证了多线程的序列话和可见性
                        //lock.lock();
                        n++;
                        //lock.unlock();
                    }

3.2 **一些基本概念**

race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争

数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果

如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好),

monitor (管程) ---> 锁

critical section -> 临界区

如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细

3.3 具体: 如何保障操作的原子性(Atomicity)

1. 悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)

2. 乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作

CAS = Compare And Set/Swap/Exchange

3.4 CAS概念 = Compare And Set/Swap/Exchange

ABA问题 假如从缓存种读取的n是0,到寄存器进行修改为1后,放回缓存的时候查看缓存的值依然还是0这就是ABA问题 ---总结其他线程修改数次之后最后的直还是和原来的值相同

3.5 解决ABA问题

在乎最后缓存里面的值,比如引用,虽然最后回到缓存的时候引用没有改变,但是可能引用里面的值已经做了修改,为了解决这问题 添加verison

3.5 让CAS产生作用

CBA compare and swap /compare and exchange 其实内部是让读取完值进行修改后将值设置回缓存内部是if(new ==old)是的话设置进去,但是再if判断成立的时候刚要修改原始值的时候正好被别的线程修改为别值,这个时候设置就有问题,解决这问题必须要保证CAS过程是原子性的

必须让CAS是原子性的

3.6 悲观锁的效率不一定比乐观锁的效率低

悲观锁的实现逻辑:有一个队列存放等待的线程(线程的状态的wating sleep paking),队列里面的线程是不消耗cpu的,在队列里面等待的线程等待上一个锁的释放,等待操作系统的调度

乐观锁的实现逻辑: 乐观锁是等待的线程在那里一直转圈,一直在询问上一个锁释放释放,一直在那里原地转圈,一直转圈在不段的消耗cpu资源

3.7悲观锁和乐观锁的使用场景

不同的场景:

临界区执行时间比较长 , 等的人很多 -> 重量级

时间短,等的人少 -> 自旋锁

实战中就用synchronized 他做了优化,内部既有自旋锁 也有乐观锁 偏向锁,做了自适应升级性能非常不错了

3.9 synchronized如何保障可见性

synchronized最底层有一条lock语句,他在unlock解锁的时候,他会把内存的状态和缓存做一个刷新同步,下一条语句才可进行,lock前后有内存屏障的作用,前后的语句都不能越过他,他会做内存同步的,所以他能保障可见性;但是他不能保证有序性;

4.0 synchronized和volatile

synchronized 即保证可见性也保证了原子性,但不能保证有序性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值