JAVA 多线程

JAVA 多线程,第1张

JAVA 多线程

文章目录
    • 基础概念
      • 进程、线程、协程
      • Ques:
        • 单核cpu设置多线程有意义嘛
          • cpu密集型和 io 密集型
        • 工作线程数(线程池中线程数量)是不是越大越好?设多少合适?
    • 并发编程的三大特性
      • 可见性
          • 三级缓存
          • volatile 保证可见性
      • 有序性
          • 对象创建过程
          • volatile 防止指令重排
      • 原子性
          • 乐观锁(无锁、自旋锁)
            • CAS 的 ABA 问题
            • CAS 比较并交换的过程中如何保障线程安全的呢?

基础概念 进程、线程、协程
首先来讲下这三种的区别。举个例子,我们启动我们的 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溢出的情况(没有初始化已经关联起来,当场拿到值是第一步初始化的默认值)

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这条指令给了个锁,锁定了一个信号啥的。(锁定了一个北桥信号)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5575457.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存