3

JAVA 多线程 总结

 1 year ago
source link: https://blog.51cto.com/u_15352400/6865863
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

JAVA 多线程 总结

精选 原创

我本半山人 2023-07-27 10:03:21 博主文章分类:Java ©著作权

文章标签 插入图片 数据 缓存 文章分类 Java 后端开发 阅读数182

Table of Contents

进程、线程、协程

首先来讲下这三种的区别。举个例子,我们启动我们的 xx.exe ,首先是会在内存当中开辟一块空间给这个程序加载到内存当中,要启动它的话,我们的系统要找到这个程序内部的主线程进行运行。
定义:
进程是操作系统进行资源分配的基本单位
线程是调度执行的基本单位,多个线程共享一个进程的资源
协程/纤程 是绿色线程,即用户管理的而不是操作系统管理的线程

Ques:

单核cpu设置多线程有意义嘛

	有意义,线程有些操作(等数据,调io啥的)不消耗cpu
cpu密集型和 io 密集型
	cpu密集型程序指的是对cpu利用率高(大量时间用于cpu计算)
	io密集型(cpu利用率低(大量时间用于io调度))

工作线程数(线程池中线程数量)是不是越大越好?设多少合适?

   不是 ,具体线程数一般要通过模拟实际情况进行压测
   公式: N(threads) = N(cpu) *U(cpu)*(1+W/C)
               N(cpu) 处理器的核数
               U(cpu) 期望的CPU利用率
               W/C  等待时间与计算时间的比率
   W/C咋确定? 
			   Profiler(性能分析工具) 测算
			   java JProfiler
		       Arthas (远程) 

并发编程的三大特性

	  线程将数据从主存加载到本地缓存,以后的操作就读本地缓存的数据。这个时候如果有第二个线程对这个数据进行修改,第一个线程能否看到被修改的值的问题,就是并发编程的可见性问题。
	  针对可见性问题,我们先讲一下三级缓存:
JAVA 多线程 总结_缓存
如图有两颗cpu,每颗cpu有两个核,
多个cpu共享内存,每个cpu独享L3缓存,
每个核独享L1,L2缓存,cpu内核共享L3缓存。
每个线程读数据的时候,会先从内存中奖数据加载到L3缓存中->L2->L1,读的时候则是从L1->l2->L3到内存
补充: 空间局部性原理:当我们用到某个值的时候,我们很快会用到相邻的值;
      时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
      因此读数据的时候会将身边的值一并读到缓存当中。一般是一个缓存行大小。一个缓存行64字节。所以操作数据的时候,为了防止数据不一致,出现了缓存一致性原理。
      (java8当中有@Contended注解(后续版本取消了),可以了解一下,就是将数据前后填满,保证只会读到这个数据,浪费空间换时间)
注意:缓存一致性协议≠MESI,MESI只是微软的缓存一致性协议,比较有名而已。感兴趣的可以去了解了解
volatile 保证可见性
 1.volatile修饰的内存,对于它的每次修改,都可见--->进行修改也同步修改主存中数据,同时通知其他用到的线程重新load数据
 2.volatile修饰引用类型 (对另外的线程还是不可见)-->一般不会出现volatile修饰引用类型        
并发编程有序性问题:为了提高执行效率,Cpu可能会乱序执行
乱序执行的条件:as-if-serial ->不影响单线程的最终一致性
对象创建过程
 解释几条重要的指令  0::new->申请一块内存,设置默认值(半初始化状态)
 			      3:特殊调用,这边是调用初始化方法
 			      4:建立关联  引用对象和对象建立关联(位于栈内栈帧中的引用类型指针指向堆)     			     
JAVA 多线程 总结_缓存_02
在实际运行当中,这些指令3、4的顺序可能会重排序–>可能会出现this溢出的情况(没有初始化已经关联起来,当场拿到值是第一步初始化的默认值)
this 溢出是指对象还没完成实例化就已经返回引用。
happens-before原则 JVM规定了8种情况不允许重排(这方面内容此处跳过,感兴趣的可以去百度搜索看看)
volatile 防止指令重排
volatile实现细节 jvm层面
           在指令间加内存屏障,屏障两边的指令不给重排
           JVM层级   LOADLOAD   读读屏障  上面读和下面读不允许换序   其他不限制
                    STORESTORE 写写屏障   
                    LOADSTORE   读写屏障   
                    STORELOAD   写读屏障
原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。 
这边先介绍几个基础概念
1.rare condition 竞争条件--->多个线程访问共享数据产生竞争
2.unconsistency  数据不一致
3.上锁的本质:将并发编程序列化,将并发操作变成顺序化操作
            将锁内部的东西当一个原子执行
4.monitor  管程 (锁)
5.critical section 临界区 锁住的大括号内部的
    如果临界区执行时间长,语句多,叫做锁锁的粒度比较粗,泛指,锁的粒度比较细

所谓上锁,就是保障临界区操作的原子性(atomicity)

乐观锁(无锁、自旋锁)

CAS操作:compare and swap/set/exchange  比较并且交换
JAVA 多线程 总结_缓存_03
 就是一个线程对数据操作时,操作过后将我这原来的值和内存当中的值进行比较,如果相同,则将新的值更新到内存当中
 (细心的哥们可能发现了,这边的操作仍然可能有坑在读和更新中间,这个时候要是有人抢先一步更新了,是否还会有多线程问题呢?这个问题如何解决的,我们等下再讲)

CAS 的 ABA 问题

讲个通俗的例子,你出差了,然后临走前看了下家里的样子,出差过程中,你的某个亲戚把房子卖了,中间转了99手,最后你亲戚心虚把房子又买回来了,你回来之后,一对比,家还是那个家,但是总感觉有些地方不对了,这就是cas的aba问题

 aba问题的解决办法也很简单,加个版本号就行。就是你回来一看,这房子版本号99+了已经和你的不一样了,那你就知道有问题了。对比原本数据的时候同时对比版本号
CAS 比较并交换的过程中如何保障线程安全的呢?
大家可以debug下atomic类,举个例子。atomicInteger 
JAVA 多线程 总结_缓存_04
点开unsafe,可以看到这里有几个cas的方法,但是这个是native c++的本地方法。

JAVA 多线程 总结_数据_05

这边直接给大家说结论,感兴趣的可以去整Hotspot源码debug一下
这里最后是到一个   Atomic::cmpxchg 方法中,当中会有if_mp的判断 ,mp是multi processor(多处理器)的意思。
在汇编语言中会执行  lock cmpxchg指令。cmpxchg(不是原子的)   所以lock这条指令给了个锁,锁定了一个信号啥的。(锁定了一个北桥信号)

悲观锁(sychronized)

用户态和内核态
内核态: 执行在内核空间,可以访问所有的指令
用户态: 只能访问用户能访问的指令
jvm对于系统而言也只是用户态程序,然而申请锁资源必须通过kernnel,系统调用。所以jdk早期sychronized叫做重量级锁
对象的内存布局
JAVA 多线程 总结_数据_06
可以看出,对象的锁状态被记录在markword当中,具体情况如下图,锁状态主要看后三位橙色的部分
JAVA 多线程 总结_插入图片_07

使用JOL可以看到一个对象的内存布局,具体maven依赖如下

	   <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.8</version>
       </dependency>
		Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

锁的升级过程

JAVA 多线程 总结_数据_08
先讲述一下大致的流程,具体的字段含义我们稍后讲。
1. 创建一个对象,偏向锁未启动时,这将会是一个普通对象
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
此时的锁的状态是无锁状态,markword最后三位为001:
JAVA 多线程 总结_数据_09
2.给对象加锁,锁会升级成轻量级锁(无锁、自旋锁)
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

 synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
 }
此时锁的状态为轻量级锁,markword后两位为00:
JAVA 多线程 总结_数据_10
3.竞争加剧:有线程超过10次自旋,-XX:PreBlockSpin(1.6之前 可以通过这个指令控制自旋次数),或者自旋线程数超过cpu核数的一半则升级为重量级锁。
  1.6之后加入Adapative Self Spinning(自适应自旋)
7.偏向锁已启动,则new出来的对象有匿名偏向锁

Jvm通过 -XX:BiasedLockingStarupDelay (偏向锁默认启动延迟 4s)设置偏向锁启动时间,默认是4s后启动因为jvm启动过程中会有很多线程竞争,所以默认情况下不会打开偏向锁

Thread.sleep(5000);
Object t = new Object();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
JAVA 多线程 总结_数据_11
可以看到这个时候偏向锁已经启动,markword最后三位是101
8.sychronized 给匿名偏向锁加锁后就是偏向锁。
  Thread.sleep(5000);
  Object t = new Object();
  System.out.println(ClassLayout.parseInstance(t).toPrintable());
  synchronized (t){
       System.out.println(ClassLayout.parseInstance(t).toPrintable());
  }
JAVA 多线程 总结_插入图片_12
可以看出markword后三位是101 偏向锁
5.轻度竞争:只要有一个线程来争抢这个锁,就会从偏向锁升级成轻量级锁
6.重度竞争:竞争加剧:有线程超过10次自旋,-XX:PreBlockSpin(1.6之前 可以通过这个指令控制自旋次数),或者自旋线程数超过cpu核数的一半则升级为重量级锁。
4.普通对象升级成为偏向锁

以class为单位,为每个class维护一个偏向锁撤销计数器 LR 。每次该class的对象发生偏向撤销操作时,增加一个LR(LR有个指针指着displacedMarkword),当这个值达到重偏向阈值时(默认20),jvm就会感觉这个class偏向锁有问题,因此会进行批量重偏向。如果达到40就会出现批量重撤销。感兴趣的可以去了解一下epoch批量重偏向和批量重撤销

偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗 。 轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。 “偏向”的意思是, 偏向锁假定将来只有第一个申请锁的线程会使用锁 (不会有任何线程再来申请锁)
偏向锁是否一定比自旋锁效率高 ?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销的过程,这时候直接关闭偏向锁,直接使用自旋锁

乐观锁和悲观锁谁效率更高?

  等待线程多,临界区执行长,建议悲观
  等待线程少,临界执行短,建议乐观 
  悲观锁线程等待是在队列当中等待操作系统调度,不消耗cpu资源。乐观锁自选比较更消耗cpu资源
  实战--->建议直接 sychronized (现在优化的很好了)  
  sychronized 会保障可见性,因为在修改数据后会做一个缓存的刷新作用,并且保证了线程顺序化执行 。无法保障有序性
  • 收藏
  • 评论
  • 分享
  • 举报

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK