侧边栏壁纸
博主头像
慢行的骑兵博主等级

贪多嚼不烂,欲速则不达

  • 累计撰写 29 篇文章
  • 累计创建 27 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

慢行的骑兵
2021-10-14 / 0 评论 / 0 点赞 / 321 阅读 / 2,055 字
  • 从一个Java代码案例开始分析,线程安全问题造成的原因,从而引出synchronized、CAS原理等知识,并对其进行总结。
  • 想要学好并发,必须要把Jvm学好。而Jvm的学习(学过一点),是需要投入大量的时间。目前,站在android开发的角度对并发的某些点进行学习;

一.案例分析

  • Java下的线程安全分析,代码示例如下

01.代码示例

  • 以上的代码如何执行呢?是不是很有规律的,一个+然后再一个-,答案不是。
  • 对counter增加volatile修饰,会实现很有规律的,一个+然后再一个-吗,答案不是。
    • 原因:volatile处理的是一次性的原子操作(简单理解,就是一次完成的操作);
    • 而以上代码存在的问题是:线程上下文切换带来的指令执行问题(暂时不太好理解这句话,不急,很快就知道);
  • 我们需要查看汇编指令

02.字节码指令

  • count++的字节码指令一共有4个:13-16行
  • 我们按照代码来分析一下字节码指令

03.案例分析

  • 开始是线程2先执行,在线程2执行前3个字节码指令的时候,资源不够,会将时间片的使用权交给线程1

04.案例分析

  • 线程1执行完之后,主内存的是counter是-1,接着,又切换回线程2继续执行,那么,主内存中的counter就是1了。
  • 总结一下
    • ++操作,不是单纯的原子操作,里面从指令层面来说,它是4个指令完成一次加完之后的赋值(提数据,再计算,再赋值);
    • 该案例引出的问题是:线程的上下文切换导致的安全性问题;
    • 关于该问题会引出两个概念,临界区和竞态条件;

二.临界区和竞态条件

1.概念

  • 临界区:一个程序运行多个线程本身没有问题,出现问题最大的地方在于多个线程访问共享资源。多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码为临界区;
  • 竞态条件:多个线程在临界区内执行,由于代码执行序列不同而导致结果无法预测,称之为竞态条件(指多个代码行程的一组原子);

2.规避手段

  • 为了避免临界区的竞态条件发生,JAVA提供多种手段进行规避
    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

三.synchronized

1.对象锁

  • 采用互斥方式让同一时刻最多只有一个线程持有对象锁,其它线程在获取这个对象锁会被阻塞,不用担心线程上下文切换;

  • Java默认对象提供锁:Jvm底层实现过程中,对于锁的判断依据全部扔到了对象上面进行判定;

  • synchronized的等价方案(存在弊端,容易产生内存泄漏)

05.案例分析

  • 推荐:单独给锁对象(更好可控)或者使用byte数组(byte[] b = new byte[0],更节省空间);
    • 引出问题
      • 为什么要用一个对象加锁?
      • 锁到底加的什么内容?

2.Jvm普通对象头32位Mark word

  • 对象头中实际采用的是64位数据作为承载,如果使用的是数组,多出一个32位的长度

  • mark word:32位的数据,没有固定的数据格式(根据状态对于里面的数据进行变更);

06.JVM普通对象头32位Mark word解析

3.Mark中的数据对于并发的支持

  • 线程分布的不同锁分类:
    • 偏向锁: 只针对于一个线程,单个线程体系下加锁,本质就只有一把,直接应用markword解决识别问题;
    • 轻量级锁:只针对于两个线程,利用栈区结构来存储线程ID不同;
    • 重量级锁:两个以上线程,利用一个全新的结构来存储不同的ID;

4.Monitor

  • 先从案例分析重量级锁(最初的实现方案是Monitor)
public static void main(String[] args) throws InterruptedException {
    byte[] b = new byte[0];

    Thread t1 = new Thread(()->{
        for (int i = 0; i < 10000; i++)
        {
            synchronized (b){
                counter++;
            }
        }
    });

    Thread t2 = new Thread(()->{
        for (int i = 0; i < 10000; i++) {
            synchronized (obj){
                counter--;
            }
        }
    });

    t1.start();
    t2.start();

    t1.join();
    t2.join();
    System.out.println(counter);
}
  • 当碰到synchronized的时候,会对对象头部的markWord进行修改,此时,重量级锁的标志位为01,向Jvm申请一把锁(可以理解成创建一个Monitor-实现具体锁的业务),然后把Monitor对象的地址塞入mark头;
  • Monitor的实现,见下图

07.synchronize与Monitor

  • 当Owner中的内容释放掉之后,会从entryList(等待容器)中提取线程,至于哪个线程去Owner容器是采取竞争的方案(通过算法);

  • 基于公不公平的实现原理是基于上图的位置实现;

  • 没有所谓的公平锁存在;

  • notify实际上就是,把在waitSet的线程移到了entryList中;

  • 如果是单线程运行的情况下,需不需要加锁?

我们现在是纤细monitor占用空间大,而且在某些场景下没有比较
场景:
    1.单个线程在运行---》仅仅只要进行标记一下--->直接标记在我们markword上面
    2.两个线程在运行---》我的需求,要不就是1用,要不就是2用,我也不要monitor,但是需要有数据的记录
      	这个时候我们要想个地方来存两个线程的数据,所以我找了个地方,就是栈帧里面,好处是什么?
    3.多个线程在运行(需要monitor的支持)
  • 轻量级锁

08.轻量级锁内存应用过程

  • 锁膨胀:就是在线程慢慢开辟的过程中,它的处理方案的变更;

09.锁膨胀

  • Monitor对象过程注意事项
    • 执行同步代码块内容,然后唤醒entryList中其他线程时,此处采取竞争策略,先到不一定先得,所以synchronize锁是非公平;
    • 非公平锁: 在锁可用的时候,一个新到来的线程要占有锁,可以不需要排队,直接获得;
    • 公平锁: 在锁可用的时候,一个新到来的线程要占有锁,需要排队,等待执行;
  • 有没有比synchronized速度更快的方案?
    • 利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronize阻塞算法,J.U.C在性能上有了很大的提升;

四.CAS

  • 一套同步主内存跟工作内存数据的方案

  • 实际上是在逻辑层面运用算法和判定去规定数据异常的方案;

  • CAS机制当中使用了3个基本操作数:

    • 内存地址V
    • 旧的预期值A
    • 要修改的新值B
  • 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

  • 总结,通过以上,我们认识JAVA锁(了解底层实现),JAVA锁不是处理互斥的唯一方案,以及CAS理论的应用也可以解决互斥;

0

评论区