• 并发编程-原子性


    并发编程-原子性

    我们都清楚当多个线程去同时做一件事情的时候,我们需要考虑原子性、可见性、和有序性这几个问题,本章主要说原子性,以下是阐述内容

    1. 原子性:主要用原子性问题进行展开讨论
    2. 同步锁(synchronize):使用同步锁解决问题
    3. MarkWord对象头:锁的状态存在哪里
    4. synchronize的锁升级机制:多个线程抢占资源的时候,锁的底层状态是如何改变的
    5. CAS机制:当无锁化时候(例如自旋锁的时候,cas起到的作用)

    线程的原子性

    【原子性】:指的是一个操作一旦进行就不能被别的操作进行干扰。我们创建两个线程对一个数字进行增加,两个线程都增加数字10000,按照常理来讲,结果应该是20000,实则不然

    public class AtomicDemo {
        private int i =0;
        private void  mock(){
            i++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            AtomicDemo atomicDemo=new AtomicDemo();
            Thread[] thread =new Thread[2];
            for (int i = 0; i < thread.length; i++) {
                thread[i]=new Thread(()->{
                    for (int j = 0; j <10000 ; j++) {
                        atomicDemo.mock();
                    }
                });
                thread[i].start();
            }
            thread[0].join();
            thread[1].join();
            System.out.println(atomicDemo.i);
        }
    }

    运行结果为(证明肯定有一个线程被打扰到,否则结果肯定是20000,这就是一个典型的原子性问题

     分析为什么导致

    以上述demo为例,实际上当线程对 i 进行 ++ 的时候,在底层分三步

    • 加载 内存中 i
    • i++
    • 把++后的数据写入内存

    那我们可以想象一下,当线程a正在对i++

    • ->此时线程cpu切换到线程B
    • ->当线程b把i=0变成i=1
    • ->这个时候cpu又切换到线程a
    • ->那么线程a按照之前执行的位置再次进行相加,但是之前的位置是i=0,所以两个线程都循环了一次结果却只是相加了1
    • 这就是为什么最终结果不等于20000 的原因了

    进行验证:打开terminal 输入 [javap -v AtomicDemo.class] 查看字节指令

     所以很有可能在某个过程中被别的线程打断,从而得到我们预期外的结果

    如何解决这一问题?

    实质上有很多方法解决,我们今天只围绕synchronized进行展开,我们只用给这个方法加上synchronized就可以了。

    synchronize是什么、如何使用?

    synchronize就是一种排他锁,换句话说是一种互斥锁,他可以同一时间只让一个线程对你的逻辑进行访问,那么我我们的逻辑受到怎么样的保护,或者说这个关键字的范围是什么呢?如何使用呢?

    可以修饰:实例方法(对象范围)、代码块(取决于括号中你所放置的内容this/对象.class)、静态方法(对象范围)

    实例方法:

    如果两个线程同时访问不同对象的同一个方法,那么他们不是互斥关系,就是这个关键字不会保护你的逻辑

     

     如果访问的是同一个对象,则会对你的逻辑进行保护,比如你的method1中写了一个逻辑,必须等线程1执行完成线程而才能执行

     代码块:如果你的括号中写的是this那和实例方法的作用域是一样的

     但是如果括号中写的是**.class那作用域就是不管多少线程想执行必须一个一个进行排队

     静态方法(因为静态方法是一个类产生就产生的,所以他的作用也是全局的)

     tips: 细细想来,其实是对象决定synchronized 的范围,我们想想,如果有多个线程抢占同一个资源,那其中一个抢占到怎么通知别的线程这个坑位已经被抢占了呢?是否有一个全局的位置去存储锁的标记呢,那既然对象决定关键字的范围,那么一个资源是否被抢占到,【是否抢占标记】是否会存储在每个对象中呢?对的,现在就来说一下MarkWord对象头

     MarkWord(就是对象在内存中布局的三大部分中的2/1部分,这一部分存储着锁的标记)

    对象在内存中的布局分为:

    • 对象头(Header):这一部分由两部分组成
      • Mark Word:这一部分就是存储锁的标记  
      • class 对象指针(类元信息):对象对应的原数据对象的内存地址
    • 实例数据(Instance Data):这里存储的就是对象的成员变量等
    • 对齐填充(Padding):这一部分属于一个优化部分,由于HotSpot VM的自动内存管理要求对象起始地址必须是8字节的整数倍,所以如果不是8的倍数,那就就会自动填充为8的倍数,否则进行读取就会消耗多余的性能,仅此而已

    tips: 由下图可以看出,当线程对资源进行获取的时候,看一下这个锁的状态,在决定是否进行抢占,这个是否可以抢占在你规定的对象中存储,这也就是为什么线程对两个不同的对象的资源进行抢占时,其资源不受保护的原因了,(因为他们有不同的锁的状态)

     我们来看看是否锁的状态被存储了起来

      增加这个jar,这是用来查询对象的大小和布局的工具,由 openjdk 提供

    <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
    </dependency>

    我们用这张图片作为参考去查询锁的状态

     对对象的布局进行打印

     结果如下:

    tip:我们已经知道了锁的状态被存储起来,那么那些轻量级、重量级、等等,锁的状态都代表了什么呢锁的状态是怎么流转的呢,来让我们继续剖析,以及来模拟状态并且看一下状态markword中的状态是否变了...

     synchronize的锁升级机制

    在jdk1.6之前只有无锁->重量级锁,使用‘synchronize’的流程是,如果一个线程没有抢占到资源,那就直接是一个重量级锁的状态,为了提高性能,才有了后面的锁的状态】

         synchronize锁的类型有这几种:

    • 无锁:
    • 偏向锁:就是当没有线程对资源竞争的时候,
      • 此时线程a得到了资源,
      • 那这个锁就会存储一个线程a的指向地址,
      • 下次线程a再次进入资源,就不需要抢占锁,直接可以进行执行
    • 轻量级锁:避免线程阻塞(因为如何线程阻塞,我们再次对线程进行唤醒,那就增加了性能开销,也就是从用户态到内核态) -》从偏向锁升级而来
      • 线程b也来抢占资源,但是发现锁已经偏向了线程a,那么他就要升级为轻量级锁,轻量级锁,使用自旋锁进行实现
        • 自旋锁:实际上就是用一个for循环不断询问时候别的线程已经执行完成,当前正在抢占线程的线程就可以执行,然而在循环的时候肯定会牵扯到线程问题,那么我们用CAS进行实现
    • 重量级锁:比较消耗性能,为了优化,所以在1.6后引入了偏向锁和轻量级锁
      • 牵扯用户态到内核态的交换(用户态就是在java中的操作,而内核态就牵扯到cpu层的操作,所以更消耗性能),
      • 没有获得锁的线程会阻塞,当这个线程获得锁的时候在进行唤醒  

     我们来验证是否锁的流程是像我们说的一样

    public class LockDemo {
        Object o=new Object();
        public static void main(String[] args) {
            LockDemo lockDemo=new LockDemo();
            System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
            // 加锁后的状态
            System.out.println("加锁之后--------");
            synchronized (lockDemo){
                System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
            }
        }
    }

    我们观察markword中的状态,参考查询锁的状态那张图

     我们发现,按照上面的画的流程来讲,应该首先是无锁,之后是偏向锁啊,为什么这里是自旋锁的?因为偏向锁是默认关闭的,因为在程序启动的时候已经有一些我们不知道的线程在底层运行了,那么下个线程来的话直接就会执行重量级锁了。

    public class HeightLockDemo {
        public static void main(String[] args) {
            HeightLockDemo heightLockDemo = new HeightLockDemo();
            Thread thread = new Thread(() -> {
                synchronized (heightLockDemo) {
                    System.out.println("线程1");
                    System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable());
                }
            });
            thread.start();
            synchronized (heightLockDemo) {
                System.out.println("主线程");
                System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable());
            }
        }
    }

     CAS机制:

    【CAS(Compare And Set)】:其实就是在操作逻辑之前,拿之前的数值,和你的预期 值进行对比,如果相同那就修改你传入的新的数值。实际上有点像数据库的乐观锁

      那在轻量级锁中大概的流程就是这样的:首先不断循环->判断cas,如果cas返回ture则对锁的状态进行修改,当然除了cas的判断外可以进行breadk,肯定还有自旋次数的限制,否则循环就无终止了 

    在java中有使用cas的例子【sun.misc.Unsafe#compareAndSwapObject】

    那么CAS的底层又是如何实现的,因为在底层也是多个线程来抢占这个CAS的,其实CAS的底层也是用锁来实现的,只不过是CPU层面的锁。

  • 相关阅读:
    关于解决Python中requests模块在PyCharm工具中导入问题
    arcgis javascript api 事件的监听及移除
    零基础掌握百度地图兴趣点获取POI爬虫(python语言爬取)(代码篇)
    python 爬取全量百度POI
    Webpack安装使用总结
    开源协议之间的区别
    npm指令后缀
    nrm的作用(及安装)
    Vuejs中关于computed、methods、watch的区别
    VUE参考---watch、computed和methods之间的对比
  • 原文地址:https://www.cnblogs.com/UpGx/p/14803974.html
Copyright © 2020-2023  润新知