并发编程-原子性

并发编程-原子性

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

  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层面的锁。

posted @ 2021-05-24 13:34  UpGx  阅读(331)  评论(0编辑  收藏  举报