搞透并发编程---原子性

原子性

原子性指一个操作是不可中断的,一个操作一旦开始就不会被其他线程影响;

public class SynchronizedTest {
    private volatile static int a=0;
    private static Object lockObj=new Object();
    private static CountDownLatch countDownLatch=new CountDownLatch(100);

    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<100;i++){
            new Thread(()->{
                for (int j=0;j<10000;j++){
//                    synchronized (lockObj){
//                        a++;
//                    }
                    a++;
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.err.println(a);
    }
}

上述代码执行对变量a的累加操作,我们的期望值是1000000 但是由于a++ 不具备原子性,导致结果并不是我们预想中的, 所以需要添加synchronized同步块;

synchronized的使用方法

1.在静态方法上使用,锁定的是Class对象;
1.1 这里注意了,一个类中不要有多个 public synchronized static 的方法,很容易出现并发问题;这里t1 执行不完,t2 是不会执行的;
试想一下如果存在多个这种方法能够随意调用,可能就会出现频繁的线程阻塞,唤醒;

    public synchronized static void t1(){

        System.err.println("t1 running...");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void t2(){
        System.err.println("t2 running...");
    }

    public static void main(String[] args){
        new Thread(()->{
            t1();
        }).start();

        new Thread(()->{
            t2();
        }).start();
    }

2.在实例方法上使用,锁定的是实例对象;
在方法上添加synchronized 在字节码层面会在方法的描述中添加 synchronized 描述;
在这里插入图片描述

3.在同步块中使用,锁定的是实际传入的对象;
在同步代码块中使用 在字节码层面会在字节码中添加 monitorenter 和 monitorexit;
在这里插入图片描述
当方法调用时,遇到synchronized标记 或者 monitorenter 和 monitorexit 时,就会尝试去获取锁定对象的monitor;获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通 过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切 换,对性能有较大影响。

什么是monitor? 可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象 是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把 看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的 是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于 HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
在这里插入图片描述
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当 前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁); 同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式 获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。 那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局

在这里插入图片描述
1.无锁状态: 25bit 存储对象的hashcode,4bit存储分代年龄,1bit存储是否为偏向锁,固定2bit为锁标记位;
2.偏向锁状态: 23bit 存储偏向的线程id,2bit 存储epoch, 4bit存储分代年龄,
3.轻量级锁状态: 30bit 存储获取锁的线程栈帧中Lock Record的指针,
4.重量级锁状态:30bit 存储monitor的指针(依赖操作系统的Metux互斥)

可以通过以下工具包,查看对象头的变化;

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

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
//        Thread.sleep(5000);
        SynchronizedTest lockObj=new SynchronizedTest();
        System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
//        synchronized (lockObj){
//
//        }
    }
}

查看输出的对象信息时需要注意大小端问题:操作系统大小端问题
在这里插入图片描述
Windows系统时小端存储,按照小端存储我们整理一下数据格式;
mark word 占 8byte 64bit;
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
类型指针占4byte 32 bit;
11111000 00000000 11000001 00000101
由于要满足8字节对齐,所以填充了4byte的空白
通过观察mark word可以看到此时是无锁的状态;

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        SynchronizedTest lockObj=new SynchronizedTest();
        synchronized (lockObj){
            System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
        }
    }
}

在这里插入图片描述
添加了一个synchronized 同步块之后,升级成偏向锁了; 这里为什么Sleep5秒? 是因为jvm启动的时候也会执行带有锁的操作,避免偏向锁的升级过程所以延迟启动了;

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        SynchronizedTest lockObj=new SynchronizedTest();
        Thread t1 = new Thread(() -> {
            synchronized (lockObj) {
                System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lockObj) {
                System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
    }
}

模拟竞争不激烈的情况下的锁升级;
在这里插入图片描述
t1线程执行时,锁状态为偏向锁;

在这里插入图片描述
t2线程执行时,锁状态为轻量级锁; 30 bit用来存储获取锁的线程栈帧中Lock Record的指针; Lock Record存储了升级轻量级锁时Mark Word的副本;

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest lockObj=new SynchronizedTest();
        Thread t1 = new Thread(() -> {
            synchronized (lockObj) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lockObj) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println(ClassLayout.parseInstance(lockObj).toPrintable());
            }
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
此时的锁状态为重量级锁,后面的线程就要阻塞了; 这也是synchronized 性能低的原因; 线程的阻塞和唤醒需要由用户态切换为内核态才能操作比较耗时;

Synchronized的优化

在jdk1.6以前,synchronized的性能比较低是因为直接添加重量级锁,导致其他线程只能阻塞; synchronized 作为java的亲儿子 怎么能被李二狗的AQS比下去呢? 所以在1.6以后做了大量的锁优化,前面提到的偏向锁,轻量级锁,重量级锁的升级过程是一部分优化; 在轻量级升级重量级的过程中,还加入了自适应自旋来尽量避免升级为重量级锁;

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的 是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。 默认开启偏向锁 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时 Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情 况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线 程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时 进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以 节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的 数据支持。 锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析 :-XX:+DoEscapeAnalysis 开启逃逸分析 -XX:+EliminateLocks 表示开启锁消除。

逃逸分析

使用逃逸分析,编译器可以对代码做如下优化:
一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存, 而是存储在CPU寄存器中。

线程

进程与线程的区别?
进程是操作系统进行资源分配的最小单位;
线程是操作系统进行cpu调度的最小单位,一个进程中可能包含多个线程;

线程的状态

1.新建
2.运行  -->启动start()方法后;
3.等待 --> 调用了wait(),LockSupport.park(),方法后进入等待状态, 等待状态的线程不会被CPU分配时间片,需要等待其他线程唤醒;
4.阻塞--> 当发生资源竞争时才会发生阻塞,阻塞的线程会等待获得锁;
5.结束

wait 和 sleep 的区别时: wait会释放锁资源; sleep不会释放锁资源;

附上锁升级的大致流程图

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值