多并发笔记

多并发笔记,第1张

理解记忆:

1、首先从计算机原理回忆,一颗单核CUP 一般包含AUL(Arithmetic Logic Unit运算器:用于计算机运行过程中的算数运算、逻辑运算等),寄存器(存储二进制代码,AUL从寄存器中读取值进行运算),PC(程序计数器,记录下一条要执行的指令)等等

2、AUL从寄存器中读取数据的速度是远超过从内存中读取的(小于1ns 和 约 80ns ,大约差了100倍),为了进一步提升CPU 的效率,避免因从内存中读取造成的时间浪费,在寄存器和内存之间增加了cache缓存;目前一般用的都是三级缓存,其中L1和L2 缓存在CPU核的内部,L3在CPU的内部,供多核CPU的不同核之间共享数据;

3、缓存是一级一级取出的,假如我去遍历一个数组arr[];从L1缓存中取arr[0]的时候没有,逐层去找,一直在内存中才找到,那继续取arr[1]在L1缓存中扔没有,还是要像刚刚一样取一次,就会浪费大量的时间,解决办法就是,每次都会读取一定长度的数据,称作缓存行(64位 64*8bit),这样就会大大提高读取效率。(MySQL中的分页查询也是类似的)

4、在多核工作的时候,如果CPU核1 和 核 2 同时修改某个缓存行的数据,就会引发多线程问题;

MESI (Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议, x86处理器所使用的缓存一致性协议就是基于MESI协议的。

MESI协议对内存数据访问的控制类似读写锁,它使得针对同一地址的读内存 *** 作是并发的,而针对同一地址的写内存 *** 作是独占的,即针对同一内存地址进行的写 *** 作在任意一个时刻只能够由一个处理器执行。 在MESI协议中, 一个处理器往内存中写数据时必须持有该数据的所有权。

更多MESI 可以参考:深入学习缓存一致性问题和缓存一致性协议MESI(一) - 掘金

5、多线程并发的目的是保证安全性的前提下尽可能的提升效率,刚刚说64位的缓存行是高速缓存读取的基本单位,如果两个CPU核同时需要修改一个缓存行中的数据,根据MESI协议,缓存行的目的状态会在多核之间会不断的标记,其实是会降低效率的;有没有好的处理办法呢?

disruptor 中的 RingBuffer 给了一个比较好的思路,即:在 缓存的前后各增加7个Long 类型的无意义数据进行占位,空间换时间,保证一个缓存行不会被多个核同时进行修改。这样就不会频繁的更换缓存行的标记,造成时间上的浪费了;

(因为图片上传问题,仅用文字描述自己的理解,详细的笔记可以参考以下链接:

提升--04---并发编程之---有序性---volatile两大作用_高高for 循环的博客-CSDN博客)

6、指令重排序造成的问题:

如图,前三种是未发生指令重排序的时候,多并发下 xy 最后可能的值,如果发生了指令重排序,那么就会出现第四种情况。

7、指令重拍造成的this溢出问题:

首先了解一下Java中new一个对象分为以下几步,汇编码如下:

new的时候开辟一块内存空间,私有变量m赋值为默认值为0;invokespecial 的时候调用初始化方法,将m赋值为8;astore_1 将t 和 m=8 关联起来;

下图就是一个this 溢出问题的代码,在构造器中new了一个线程,打印this.num,如果在创建对象的时候,invokespecial 指令和 astore_1 指令发生了重排序,那么打印出来的值就不是num=8,而是num=0; 

 8、DCL (Double check lock )双重检查锁 需不需要volatile 关键字呢?

答案是需要的,首先要理解synchronized关键字可以保证原子性和可见性,但是没有办法保证顺序性,因此有可能发生指令重排序;如上说的new 对象过程中发生了指令重排序,如果一个线程在synchronized 锁里面执行到一半时,失去了时间片,刚好此时 astore_1 执行完,对象指向了还还未初始化完毕的对象(已经不为null了,但是还没有完全完成初始化),这个时候其他线程拿到时间片,外层判空的时候发现不为null,就会直接返回 线程1初始化一半的对象;

那么既然已经说了synchronized关键是保证原子性的,那么原子里面的代码执行一半也能被其他线程使用吗?这里就要明确一个问题,上锁代码和不上锁代码能不能同时执行?答案是可以的!因此,DCL是需要加volatile关键字的!(volatile关键字 两种作用:保证可见性 和 禁止指令重排序)

更详细的图解可以参考:对DCL(双重检查锁 Double Check Lock)及Volatile原理的理解_少年啊!的博客-CSDN博客

DCL  的双重检查代码: 

    public static T01 getInstance(){
        //业务代码
        //.......

        if(INSTANCE==null){
            synchronized (T01.class){
                if(INSTANCE==null){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE=new T01();
                }
            }
        }
        return INSTANCE;
    }

9、volatile 是怎么保证指令执行的顺序呢?

首先jvm级别,识别到volatile关键词,会执行jvm内存屏障,包括 loadload 屏障、storestore 屏障、loadstore屏障、storeload 屏障(其中load是读,store是写),

jvm识别到volatile关键词后,:

a) 会在写之前加 storestore,写之后加storeload,保证在写之前完成其他的写,在自己写完之后才能继续其他的读

b) 会在读之后加上loadload 和 loadstore ,保证在自己读完之后其他的才能读,自己读完之后,其他的才能写
 

10、启动线程的三种方式


package JUC;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 继承 Thread 类 ,重写run 方法,调用 start() 方法开启一个新的线程;
 * ****注意,是要调用start方法开启新的线程,如果调用run方法,只是普通的方法调用****
 */
public class TestThread {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 第一种方法
        MyThread1 myThread1 = new MyThread1();
        myThread1.start();

        // 第二种方法
        MyThread2 myThread2 = new MyThread2();
        Thread thread = new Thread(myThread2);
        thread.start();

        // 第三种方法
        MyThread3 myThread3 = new MyThread3<>();
        FutureTask futureTask = new FutureTask<>(myThread3);
        Thread thread1 = new Thread(futureTask);
        thread1.start();
        String s = (String) futureTask.get();
        System.out.println(s);
    }
}

/**
 * Thread 类本质上是实现了 Runnable 接口的一个实例, start()方法是一个 native 方法
 */
class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread1 is running!");
    }
}

/**
 * 实现一个Runnable 接口的时候,需要按照以下步骤开启一个新的线程
 * 1、先实例化一个实现runnable接口的MyThread2实例,然后用Thread创建一个实例,然后调用start方法;
 */
class MyThread2 implements Runnable {

    @Override
    public void run() {
        System.out.println("MyThread2 is running !");
    }
}

/**
 * 实现Callable接口,可以有返回值,返回值用泛型表示
 * 实现Callable接口开启线程方式如下:
 * MyThread3 myThread3 = new MyThread3<>();
 * FutureTask futureTask = new FutureTask<>(myThread3);
 * Thread thread1 = new Thread(futureTask);
 * thread1.start();
 * 

* 使用futureTask实例的get方法可以得到线程的返回值; * * @param */ class MyThread3 implements Callable { @Override public String call() throws Exception { String string = (String) "MyThread3 is running!"; return string; } }

11、线程执行时间短(加锁的代码)、线程数目少的时候,适合自旋锁,相反的,执行时间长,线程数目多,适合用系统锁(重量级锁)。

12、synchronized : 锁的是对象(不能用string常量),而不是代码; 如果没有写对象,默认是 this (XX.class);锁定方法和非锁定方法时可以同时执行的。

为什么不能用String常量?有可能你和别人写的代码锁定的是同一个对象。

用对象做锁的时候,如果对象变成了另外一个对象,则加锁就会出问题,因此,一般定义锁对象的时候,需要加上 final 关键字  : final Object o = new Object();

13

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/729200.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存