cp到线程 的一个感悟

目录

一:cpu

二:cpu的3级缓存

三:JMM内存模型

四:多线程下的,原子性,可见性,有序性问题

       1: 指令重排发生问题

五:如何保证原子性,有序性,可见性

六:内存屏障

        1:被volatile修饰,加入内存屏障,禁止特定情况下指令重排


介绍:之前时不时就会看看线程 cpu,之类。但是总是没有感觉,你没感觉错是没有感觉看的很死板。 经过很长时间积累,发现他们的有点关联,在此记录分享下。

一般写东西,都说 这个并发问题,并发安全问题。以前迷糊,现在说下下自己的理解(只是自己的理解)。

如果你想把它串下来,主要是cpu.

一:cpu

cpu:是计算机的核心,一般我们都说几核多少内存。 最开始计算机肯定是单核的

  1.  每个cpu的核同一时间只能运行一个线程。为了提高处理速度,就变成多核
  2. 当一个多核cpu运行时,就变成现在的多线程处理。

       强调:多线程就是一个cpu的多个核同时执行  

二:cpu的3级缓存

cpu的3级缓存:cpu运算数据时,先从内存中把数据读取到cpu的处理中心,但是对于cpu来说太消耗性能。所以为了提高性能,在cpu内部配有3级缓存。1级,2级缓存是单核独享,3级缓存是多核共享。先把内存中的数据读到缓存中,cpu运行缓存中的数据(如果仅仅如此我是没有感觉提高性能,无非是,先从内存中读取数据到缓存,cpu运行缓存数据,对吧,接下来继续

  1. cpu操作内存和缓存:   
    1. cpu操作内存时间远远大于操作缓存,假设操作内存用的时间是100,缓存就是1
  2. cpu的两大特性(空间局部性和时间局部性)   
    1. 空间局部性:当cpu读取数据时,会把该数据在内存中连续的数据全部读取到缓存中。比如一个数组int [] a={1,2,3,4,5,6}.如果cpu想读取4这个数据,cpu就会把1,2,3,4,5,6全部读取到缓存中。如果接下来操作5,直接操作缓存。(在此你看,你就会发现把数据从内存读取到缓存中 时间大大提高)
    2.   时间局部性:当cpu读取数据时,把数据先读取到缓存,如果cpu判断接下来的程序(指令)还会用到该数据,那么不会立马清除该数据。(如果清除数据,那么用到时 ,还会从内存读取数据,你看这个也提高了性能(不仅仅如此,它还和指令重排优化有关,接下看))

cpu目前理解的这里接下来是。JMM内存模型

三:JMM内存模型

      JMM:是一个规则,是多线程操作共享变量。围绕着原子性,有序性,可见性(这好理解也不好理解,哈哈

  1.  JMM的工作内存:是每个线程独有的。
  2.   MM的主内存:是所有线程都能读取到的数据

         (不管是主内存(共享内存)还是,工作内存都是定义。只是定义)

解释下:

  1.    原子性:程序一旦操作不可中断。多线程操作一个程序,一个线程在没有结束完其它线程不可操 作。(多线程中,一个线程还没操作完,另一个线程就也开始操作了。会发生问题)
  2.   有序性:写的代码按照顺序执行(但是cpu优化性能,会发生指令重排--多线程可能会出现问题)
  3.   可见性: 多线程之间,操作同一个变量, 当线程一修改变量后,及时通知其他线程此变量已修       改。(多线程中,多个线程同时操作一段代码,一个线程修改变量后,没有通知其它线程变量修改。导致数据最后结果出现问题)

拿图说话:

   举个例子:  int a=1,  int b=a++

   如果一个线程要执行 上段代码,先从主内存把a读取到 该内存的工作中,然后进行操作。  最      后再把工作内存的数据同步到主内存。(在多线程中问题来了)

   多线程问题: 看代码

四:多线程下的,原子性,可见性,有序性问题

第一个例子:多线程可见性问题

         int b = 0;

        for (int i = 0; i < 100; i++) {
            b++;
            System.out.println(b);
        }

     重点: 如果是单线程b是100,如果是多线程不一定是100(为什么呢)

     如果两个线程同时操作,当两个线程拿到的数据假设都是9,但是第一个线程加完之后已经把数据同步到主内存中去 b变成10,但是第二个数据刚拿到9但是主内存已经变成10了。(不知道能感觉到这个点嘛

    违反了可见性:一个线程把变量修改后,没有通知其它变量。

第二个例子:多线程原子性问题

    int j=0;
    @Test
    public void test() {
        //线程2执行的代码
        int i = 0;
        i = 10;

        //线程1执行的代码
        j = i;
    }

两个线程同时操作,当线程1先执行代码,执行到j=i 正常j=10,   但是当i还没有赋值给j的时候,线程2先执行i-=0.   那么线程1给j赋值的值是0

   违反了原子性:程序一旦运行不可中断,(一个线程没操作完,另一个线程也操作代码,导致数据错误。  这个点get到吧)

第三个例子:多线程有序性问题

  public void test() {
        //线程1执行的代码
        int i = 0;   //第一行
        int b=20;   //第二行
        i = 10;   //第三行
    }

  解释:这个我个人感觉和cpu的缓存和时间局部性有关。之前说了 cpu操作内存时间远远大于缓存

            咱们看来:第一步:首先cpu先从内存读取i=0,第二步在从内存读取b=20,第三步再内存i=10,这样操作了3次内存。

           cpu的时间局部性是这样说的,当cpu读取这个变量时,如果这个变量接下来还继续操作,将会在缓存中保留,不删除。

           咱们这个代码。第二行和第三行变换位置执行是不影响最后结果的。所以cpu让它指令重排,代码就变成

 public void test() {
        //线程1执行的代码
        int i = 0;   //第一行
        i = 10;   //第三行
        int b=20;   //第二行
     
    }

           这样在执行:第一步先从内存中读取i=0,发现接下操作还是操作i,那么根据cpu的时间局部性,i=0在缓存中不删除,第二步,在缓存中读取i=0然后处理变成i=10,第三步,cpu从内存读取b=20

        这样程序就还有两次读取内存。

        这就是指令重排。

        但是多线程指令重排会出现问题

       1: 指令重排发生问题

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    Thread t1=new Thread(()->{
        x=b; //指令重排序
        a=1;
    });
    Thread t2=new Thread(()->{
        y=a; //指令重排序
        b=1;
    });

如果同时执行两个线程t1和t2

    如果线程t1先执行结束,那么x=0.   

--------------------------------------------------------------------------------------------------------------------------------

如果线程二先执行并且发生指令重排就变成如下。如果t1线程x=b还没执行,t2线程指令重排,并且执行了b=1.  那么t1线程x值就是1

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    Thread t1=new Thread(()->{
        x=b; //指令重排序
        a=1;
    });
    Thread t2=new Thread(()->{
        b=1;
        y=a; //指令重排序
      
    });

至此:多线程下就可能会发生,原子性,有序性,可见性。

如何保证多线程下不不发生问题呢,来了

五:如何保证原子性,有序性,可见性

volatile: 保证可见性,和有序性

              1:被volatile修饰的变量,会通知其它线程,该变量已修改。(可能会强制线程从内存中读取,并刷新缓存。这个就是常用MESI协议)

                2:被volatile修饰的变量,在特定情况下禁止指令重排(如果做到禁止指令重排呢,这就是内存屏障。)

synchronized:保证原子性

                1:加锁  (再续添加,没写完)

六:内存屏障

        1:被volatile修饰,加入内存屏障,禁止特定情况下指令重排

内存屏障:被volatile,修饰的变量在特定情况下,禁止指令重排(这个想想咋解释哈)

               第一:cpu会判定在那些情况禁止指令重(如何判定呢)

               第二:在代码(指令) 加入内存屏障

 内存屏障提供了四种:LoadLoad(读读屏障),StoreStore(写写屏障),StoreLoad(写读屏障),LoadStore(读写屏障) 

看图:这个是cpu在所有情况,是否要加入内存屏障

class VolatileBarrierExample {
      int a;
      volatile int v1 = 1;
       volatile int v2 = 2;
      void readAndWrite() {
          int i = v1;        // 第一个volatile读
          int j = v2;        // 第二个volatile读
           a = i + j;         // 普通写
           v1 = i + 1;        // 第一个volatile写
           v2 = j * 2;        // 第二个 volatile写
     }
}

指令加入内存屏障后就变成

   解释:

        1:操作一volatile读和操作二volatile读,需要加入LoadLoad(读读屏障)不可以重排

        2:操作一volatile读和普通写,需要加入LoadStore(读写屏障)不可以重排

        3:操作一普通写和volatile写,需要加入  StoreStore(写写屏障)不可以重排

        4:操作一volatile写和volatile写,需要加入  StoreStore(写写屏障)不可以重排


    class VolatileBarrierExample {
      int a;
      volatile int v1 = 1;
       volatile int v2 = 2;
      void readAndWrite() {


                      LoadLoad(读读屏障)

                  int i = v1;        // 第一个volatile读

                     LoadLoad(读读屏障)

                  int j = v2;        // 第二个volatile读

                     LoadStore(读写屏障) 

                 a = i + j;         // 普通写

                        StoreStore(写写屏障)

                  v1 = i + 1;        // 第一个volatile写

                        StoreStore(写写屏障)

                  v2 = j * 2;        // 第二个 volatile写

                      StoreLoad(写读屏障)  //最后一个指令都要加这个屏障
     }
    }

             

这样volatile实现禁止指令重排。

第七 :MESI协议

 这个没有了解很深入,我只说下我的理解

mesi协议是保证 cpu缓存一致性(也解决了多并发的可见性问题)。

他是如何保证缓存一致性呢?

答案就是加锁,在cpu的缓存行上加锁。

举个列子:

             1:如果两个线程同时读取一个变量,第一个线程读取到变量,会存在缓存中(cpu的缓存,存储单元就是缓存行(64个字节)--这里我个人觉得有优化的地方稍后再说),同时给这个缓存行加锁,并通知其它线程去内存中读取。

              2: 当两个线程同时读取到数据,此时缓存行并没有加锁?,这又该处理呢。升级解释下,当一个线程读取变量到缓存行时,此时还没加锁,把该数据处理完后,会把最终数据传送给3级缓存并在这里留一个标注。这个标注会告诉其他线程,这个变量是不是被加锁了。当其它线程把最终值复制给3级缓存时,判断这个缓存行是否被加锁。 如果加锁将移除自己的工作空间的变量,重新去主内存中取数据。(理解的大概这意思、每个缓存行都有4种状态。这个不解释。没深究。是以上说法是让自己更容易了解的流程)

              3:这里有个问题,上面说了一个缓存行64字节,如果读取的变量超过一个缓存行。比如80个字节。按照之前解释,就可能需要在两个缓存行加锁。两个缓存行加锁不能保证原子性。 当出现这种情况,不会在缓存行加锁,是直接在消息总线加锁。(这里就有优化)

此解释是为了自己更好理解: 当以前没出现缓存时,怎么解决一致性。  是在消息总线加锁,就是当一个线程从内存读取到数据时,会在消息总线加锁,告诉其它线程不能读取。这样一来很消耗性能。

之前说过,cpu从内存读取数据消耗远远大于从缓存读取数据,为了优化出现3级缓存,以及 MESI协议。在缓存行加锁,提高性能。

根据MESI协议,当变量大于一个缓存行时,会在多个缓存行存入,为了保证缓存一致性,加锁升级,直接在消息总线加锁。 这样性能会损失

优化:在编写程序时,尽量让数据小于64字节,尽可能小。

第八:synchronized

      解释下字节了解的发展史,更容易理解

      jdk1.6之前,synchronized只有一个重量级锁。它是赖操作系统的MutexLock(互斥锁)来实现的。所以每次操作,需要系统从用户空间切换到内核空间。很消耗性能(后来一个大佬研究出另外一个锁ReentrantLock(一个并发框架AQS),性能很高)

   后来synchronized升级,就有了偏向锁,轻量级锁和重量级锁。

  1. 偏向锁: 偏向锁是单例的,就是这段代码只被一个线程访问时,加入偏向锁。
  2. 轻量级锁:被多个线程访问,但是其它线程等待时间很短(其它线程会在锁外自旋,等待一会重新尝试获取)。如果其它线程等待时间很长,会升级重量级锁
  3. 重量级锁:需要让系统从用户态切换到内核态

synchronized三种加锁方式

  1. 在方法上。
  2. 在静态方法上
  3. 在代码块上
public class SynchronizedTest implements Runnable {
    static int i = 0;

    /**
     * synchronized 方式一 :在方法上加锁  ,  锁加载当前this对象, 那个对象 new SynchronizedTest(),  锁加载那个对象上
     */
        public synchronized void increase() {
                System.out.println("测试:" + Thread.currentThread().getName() + ",i=" + i++);
            }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
           increase();   //方式一

        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest sy=new SynchronizedTest();
        Thread t1= new Thread(sy);
        Thread t2= new Thread(sy);
        t1.setName("线程1");
        t1.start();
        t2.setName("线程2");
        t2.start();
        System.out.println(i);
    }
}
public class SynchronizedTest2 implements Runnable {
    static int i = 0;
    /**
     * synchronized 方式二 :在静态方法上加锁。  锁加载当前SynchronizedTest 对象上面
     */
    public  static synchronized void increase() {
        System.out.println("测试:" + Thread.currentThread().getName() + ",i=" + i++);
    }


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest2 sy=new SynchronizedTest2();
        Thread t1= new Thread(sy);
        Thread t2= new Thread(sy);
        t1.setName("线程1");
        t1.start();
       // t1.join();
        t2.setName("线程2");
        t2.start();
       // t2.join();
        System.out.println(i);
    }
}
public class SynchronizedTest3 implements Runnable {
    static int i = 0;
    static SynchronizedTest instance=new SynchronizedTest();
    /**
     * 修饰实例方法
     */
    public    void increase() {
        System.out.println("测试:" + Thread.currentThread().getName() + ",i=" + i++);
    }


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            /**
             * synchronized 方式三 :在方法内部的同步块上加锁  。作用在 当前方法上快
             */
            synchronized(instance){
                increase();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest3 sy=new SynchronizedTest3();
        Thread t1= new Thread(sy);
        Thread t2= new Thread(sy);
        t1.setName("线程1");
        t1.start();
       // t1.join();
        t2.setName("线程2");
        t2.start();
       // t2.join();
        System.out.println(i);
    }
}

 第 九:ReentrantLock

 

 第 十:JVM逃逸分析

什么是逃逸分析呢,我个人理解,方法体内创建的对象,只被其内部所引用,不被其它方法引用。 当这个方法执行结束,垃圾回收器,会对其进行回收。 如果该方法内部的对象被其他方法引用,垃圾回收器是不回收这个方法。这被称为逃逸

 开启逃逸分析可以对程序进行优化:

   1: 锁消除:

                线程同步锁是消耗性能的,当编译器确定当前对象只对当前线程起作用,那么就会移除该对象的同步锁。

                  列:Stringbuffer和Vector都是被Synchronized修饰的线程安全的,但是大部分在当前当前线程中使用,那么编译器就会移除这些锁 操作

锁消除的 JVM 参数如下:

  • 开启锁消除:-XX:+EliminateLocks
  • 关闭锁消除:-XX:-EliminateLocks

锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。

列代码:Stringbuffer是线程安全   append被加锁,Stringbuffer作用于在局部变量,没用被引用,发生逃逸。那么编辑器,会把锁消除。

   public void test3(){
        StringBuffer sb= new StringBuffer();
        sb.append("df");
    }
    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

2:标量替换:

先明白什么是标量和聚合量,聚合量就是对象,标量就是对象里面的基础类型。

聚合量可以分解成标量,将一个对象的成员变量分解成分散的变量成为标量替换(什么叫做分散的变量,就是他可以在栈和寄存器创建)

列入:如果一个对象没有发生逃逸,这个对象不会被创建,会在线程栈或者寄存器中创建使用的标量,节省内存空间,提升程序性能

标量替换的 JVM 参数如下:

  • 开启标量替换:-XX:+EliminateAllocations
  • 关闭标量替换:-XX:-EliminateAllocations
  • 显示标量替换详情:-XX:+PrintEliminateAllocations

标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。

3:锁的粗化:

简单理解把锁合并,编写并发一般情况会把锁加在代码块中,锁的范围也只是这个方法,如果在一个方法中锁多次代码块,并且这些代码块没有相互影响共享数据,那么会进行锁的粗化,把相互不影响的代码块用一个锁进行加锁。 代码如下


    public int a = 1;
    public int b = 1;

    public void test1() {

        synchronized (T) {
            for (int i = 0; i < 100; i++) {
                a++;
            }
        }
        synchronized (T) {
            for (int j = 0; j < 100; j++) {
                b++;
            }
        }
    }

---发生锁的粗化


    public void test2() {

        synchronized (T) {
            for (int i = 0; i < 100; i++) {
                a++;
            }

            for (int j = 0; j < 100; j++) {
                b++;
            }
        }

    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值