并发面试

CAS:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题

解决方法: 时间戳、+1

 

JAVA内存模型:

缓存一致性问题:

  CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致

处理器优化/指令重排序:

  为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化

并发问题:并发的三个问题:原子性(处理器优化造成),可见性(缓存一致性造成),有序性(指令重排序造成) 。为了解决这些问题,定义了java内存模型,通过限制处理器优化和使用内存屏障解决问题】

 

模型:

栈堆等都是JVM的逻辑概念,在硬件架构中没有这些概念。 右图为硬件架构的概念

 

java内存模型的定义:

  • 所有的变量都存储在主内存(Main Memory)中。
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
  • 不同的线程之间无法直接访问对方本地内存中的变量。

就是说,每个线程有自己的localmemory用来存放需要的共享变量,线程之间不能直接通讯

 

 

如果线程1和线程2都对共享变量进行+1操作,最终的结果会是3而不是2。不会存在缓存一致性问题。

 

 

 

 为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lockunlockreadload,use,assignstorewrite

 

volatile:

  volatile修饰的共享变量,就具有了以下两点特性:1.保证不同线程对该变量的内存可见性    2.禁止指令重排序

内存可见性:

  由于CPU和主存之间的速度差异,JVM定义了一种java内存模型规范,通过限制处理器优化和内存屏障来解决一致性问题。简述一下java内存模型(线程有工作线程,用来存主存中共享变量的值。执行时,先从主存取变量,保存到local,将local的变量副本值传给处理器进行操作,结果保存回local,local传回主存修改值)如果写回主存的速度慢,在此期间有其他线程修改操作,就会造成缓存不一致问题。JMM主要就是围绕着如何在并发过程中如何处理原子性可见性有序性这3个特征来建立的

原子性:

  操作如果执行不可中断,要做一定做完,要么就不会执行。i=3是原子性。  像i++就不是,先读,再加

可见性:

  当一个变量被 volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。通过 synchronizedLock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是 synchronizedLock的开销都更大。

有序性:

  JMM是允许编译器和处理器对指令重排序的,但是规定了 as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变

Volatile如何满足三大特性:

  对一个 volatile域的写, happens-before于后续对这个 volatile域的读,那么当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值

从内存语义上来看

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

Volatile能保证原子性吗?

  不能保证原子性。如果对volatie变量做++操作,是读和写的两步操作,如果读的过程中被阻塞, 其他线程是不可见的。修改才会其他线程可见。、

  可以借助synchronized, Lock以及并发包下的 atomic的原子操作类了,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作

Volatile的底层机制:

  内存屏障

1 . 重排序时不能把后面的指令重排序到内存屏障之前的位置

2 .使得本CPU的Cache写入内存  

3 . 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。

Volatile的使用?

flag状态量标记

单例模式的实现,典型的双重检查锁定(DCL)。这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给 instance加上了 volatile

 

 

 ThreadLocal:
  ThreadLocal通过空间换时间的方案,规避了竞争问题,因为每个线程都有属于自己的变量,变量只对当前线程可见。

 1.线程如何维护自己的变量副本?

    Thread类代码:

   ThreadLocal.ThreadLocalMap  threadLocals  = null;

 

ThreadLocalMap的Entry:

Entry(ThreadLocal<?> k, Object v)

Thread用类似 MapThreadLocal.ThreadLocalMap数据结构来存储以 ThreadLocal类型的变量为 Key的数值,并用 ThreadLocal来存取删,操作 ThreadLocalMap

  • 当我们定义一个 ThreadLocal变量时,其实就是在定义一个 Key
  • 当我们调用 set(v)方法时,就是以当前 ThreadLocal变量为 key,传入参数为 value,向 ThreadLocal.ThreadLocalMap存数据
  • 当我们调用 get()方法时,就是以当前 ThreadLocal变量为 key,从 ThreadLocal.ThreadLocalMap取对应的数据

 

ThreadLocalMap的Hash冲突解决办法

 

采用线性探测的方式,根据 key计算 hash值,如果出现冲突,则向后探测,当到哈希表末尾的时候再从0开始,直到找到一个合适的位置。

 

这种算法也决定了 ThreadLocalMap不适合存储大量数据。

 

ThreadLocalMap的扩容问题

 

ThreadLocalMap初始大小为 16,加载因子为 2/3,当 size大于 threshold时,就会进行扩容。

 

扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的 entry并将其插入到新的hash数组中,在扩容的时候,会把 keynullEntryvalue值设置为 null,以便内存回收,减少内存泄漏问题。

 

线程池:

三种创建方式: 

  extends  Thread  (java没法双重继承,这种方法尽量避免)

  implements Runnable

  implements Callable<>

线程池的两种类型:

  守护线程:

    在start()之前,thread.setDaemon(true)守护线程顾名思义是用来守护的,是给所有得非守护进程提供服务的,所以在 jvm 执行完所有的非守护进程之后, jvm 就会停止,守护线程也不会再运行,最典型的守护线程就是 java 的垃圾回收机制 ( GC)。

  非守护线程:

    java 线程默认设置是非守护线程 thread.setDaemon(false)。当主线程运行完之后,只要主线程里面有非守护线程 jvm 就不会退出,直到所有的非守护线程执行完之后 jvm 才会退出。如果把一个线程设置成守护线程,则 jvm 的退出就不会关心当前线程的执行状态。、

线程池的使用: 

所以使用线程池主要有以下两个好处:  

  1. 减少在创建和销毁线程上所花的时间以及系统资源的开销 。

  2. 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存 。

posted @   NobodyHero  阅读(88)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示