JAVA 多线程 总结

基础概念

进程、线程、协程

首先来讲下这三种的区别。举个例子,我们启动我们的 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 (远程) 

并发编程的三大特性

可见性

	  线程将数据从主存加载到本地缓存,以后的操作就读本地缓存的数据。这个时候如果有第二个线程对这个数据进行修改,第一个线程能否看到被修改的值的问题,就是并发编程的可见性问题。
	  针对可见性问题,我们先讲一下三级缓存:
三级缓存

在这里插入图片描述
如图有两颗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:建立关联  引用对象和对象建立关联(位于栈内栈帧中的引用类型指针指向堆)     			     

在这里插入图片描述
在实际运行当中,这些指令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  比较并且交换

在这里插入图片描述

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

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

 aba问题的解决办法也很简单,加个版本号就行。就是你回来一看,这房子版本号99+了已经和你的不一样了,那你就知道有问题了。对比原本数据的时候同时对比版本号
CAS 比较并交换的过程中如何保障线程安全的呢?
大家可以debug下atomic类,举个例子。atomicInteger 

在这里插入图片描述

点开unsafe,可以看到这里有几个cas的方法,但是这个是native c++的本地方法。

在这里插入图片描述

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

悲观锁(sychronized)

补充知识
用户态和内核态
内核态: 执行在内核空间,可以访问所有的指令
用户态: 只能访问用户能访问的指令
jvm对于系统而言也只是用户态程序,然而申请锁资源必须通过kernnel,系统调用。所以jdk早期sychronized叫做重量级锁
对象的内存布局

在这里插入图片描述

可以看出,对象的锁状态被记录在markword当中,具体情况如下图,锁状态主要看后三位橙色的部分

在这里插入图片描述

JOL

使用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());
锁的升级过程

在这里插入图片描述
先讲述一下大致的流程,具体的字段含义我们稍后讲。

1. 创建一个对象,偏向锁未启动时,这将会是一个普通对象
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
此时的锁的状态是无锁状态,markword最后三位为001:

在这里插入图片描述

2.给对象加锁,锁会升级成轻量级锁(无锁、自旋锁)
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

 synchronized (o){
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
 }
此时锁的状态为轻量级锁,markword后两位为00:

在这里插入图片描述

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());

在这里插入图片描述

可以看到这个时候偏向锁已经启动,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());
  }

在这里插入图片描述
可以看出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 会保障可见性,因为在修改数据后会做一个缓存的刷新作用,并且保证了线程顺序化执行 。无法保障有序性
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值