面试问答总结之并发编程

本文详细阐述了Java并发编程中的多线程概念、核心问题如可见性、乱序性和非原子性,介绍了JMM、volatile关键字、CAS机制以及各种锁的类型和使用,包括乐观锁、悲观锁、ReentrantLock等,并探讨了线程池的原理和实践,以及ThreadLocal的内存泄漏问题及其解决方案。
摘要由CSDN通过智能技术生成

🐒个人主页:信计2102罗铠威

🏅JavaEE系列专栏

📖前言:

本篇博客主要总结面试中对线程知识的考察点

🎀多线程的优点、缺点

提高了程序的响应速度,可以多个线程各自完成自己的工作,以提高硬件设备的利用率。
缺点:可能会出现多个线程资源争夺问题,引发死锁。

🐕并发编程的核心问题 :不可见性、乱序性、非原子性

🪀不可见性

一个线程在自己的工作内存中对共享变量的修改,另外一个线程不能立即看到 。

🪀乱序性

为了优化性能,CPU有时候会改变程序中语句的先后顺序,但基本的程序逻辑不受影响。

🪀非原子性

线程切换带来的非原子性问题,A线程执行时,被切换到B线程执行,之后再A线程执行。 如果解决?加锁,如果只是++的话,JUC并发包下的原子类也可以。

🧸JMM(java内存模型)

java内存模型规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存。线程先将主内存中的共享变量复制到自己的工作内存中去,在自己的工作内存中处理数据,再将处理好的结果写入主内存。

🏅volatile关键字:保证可见性、禁止指令重排序

一旦共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,
1.保证了一个线程修改了此变量,其他线程会立即可见(通过《缓存一致性协议》),保证可见性。
2.禁止指令重排序
3.但是volitile不能保证原子性

🐕CAS机制 (Conpare And Swap 比较并交换)

该算法采用自旋的思想,是一种轻量级锁机制。是(不加锁)乐观锁的实现。

线程先从主内存中获取共享变量复制到工作内存中作为预估值,再处理共享变量,得出结果。此时将线程中的预估值与主内存的值进行比较:
1.如果一样,证明没有其他线程干扰,将结果写入主内存。
2.如果不一样,重新获取预估值,重新进行计算结果,再次进行比较…

CAS缺陷:
由于CAS实现的是乐观锁,他会以自旋的方式不断的进行尝试,而不会像synchronized进行线程阻塞,当大量的线程自旋时,容易把CPU跑满。

原子类的实现是volatile +CAS机制。原子类适合在低并发的情况下使用。

🐕CAS会产生ABA问题

就是一个线程预估值为A,来一个线程将内存值改为B,再有一个线程将内存值又改为A。就不确定内存中的值是否其他线程进行干扰过了。
解决方案:
使用有版本号的原子类AtomicABA 根据版本号来解决问题

🏨java中锁的分类

🪀乐观锁、悲观锁

乐观锁: 它乐观认为对同一个数据的并发操作不会产生线程安全问题,不加锁,它会采用自选的方式,不断的尝试更新数据
悲观锁: 它悲观的认为对同一个数据的并发操作会产生线程安全问题,所以需要加锁进行干预
乐观锁适合“读多写少”的场景,悲观锁适合“读少写多”的场景

🪀可重入锁

就是一个加锁的方法可以调用进入另一个加锁的方法中去,两个方法可以共用同一把锁。可以在一定程度上避免死锁。

🪀读写锁 ReentrantReadwriteLock

可以实现读锁、写锁。读写可以使用一个锁实现。
特点:读读不互斥、读写互斥、写写互斥。
加读锁可以防止写,防止脏读。

🪀分段锁

1.8之后,分段锁是一种思想,减小锁的粒度, 将ConcurrentHashMap表每个区间的第一个节点当做锁对象,可以提高并发效率

🪀自旋锁

其实就是线程不断尝试获取锁的方式,不断的循环请求获取锁,直到抢到锁。会占用CPU资源。
例如:
原子类,需要改变主内存中的共享变量而不断尝试。
synchronized 加锁,其他线程不断获取锁,重复一定次数后会陷入阻塞。

🪀共享锁、独占锁

共享锁:多个线程共享一把锁,读写锁中的读锁,都是读操作时多个线程共享
独占锁:即排他锁,只允许一个线程使用该资源,写锁

🪀公平锁、非公平锁

公平锁:是指按照请求锁的顺序来分配锁,具有稳定获取锁的机会
非公平锁:不按照先来后到,谁抢到锁就是谁的。
对于synchronized是一种非公平锁。
而ReentrantLock默认是非公平锁,但是底层可以通过AQS机制来转化为公平锁

🪀针对synchronized锁的状态(底层monitorenter加锁、moniterexit释放锁)

锁的状态是根据对象头中的对象监视器来记录的,对象头Mark Word中记录对象的一些相关信息:hash值、分代年龄、锁的状态、偏向锁的线程id
无锁: 没有任何线程使用锁对象
偏向锁: 当前只有一个线程访问,那么锁对象会在对象头Mark Word中记录该线程id,下次自动获取锁,降低获取锁的难度。
轻量级锁: 当锁状态是偏向锁的时候,又有一个线程访问,此时锁的状态会升级成轻量级锁,其他线程会通过自旋的方式尝试获取锁,不会阻塞线程,提高性能。
重量级锁: 当锁的状态为轻量级锁时,线程自旋获取锁的次数达到一定次数时,锁的状态会升级成重量级锁。 会让自旋次数多的线程进入阻塞状态,以降低CPU的消耗
在这里插入图片描述

在这里插入图片描述

🪀ReentrantLock实现

在这里插入图片描述
。(ReentrantLock 中的内部抽象类sync继承了抽象同步队列AQS,里面定义了lock() 、unlock()方法)
()

🏅AQS (AbstractQueuedSynchronizer,抽象同步队列 在JUC并发包下)

是一个底层同步的实现者,有很多的同步类都继承了它。(ReentrantLock 中的内部类sync继承了它)
AbstractQueuedSynchronizer中有一个volatile修饰的int 类型的变量记录锁的状态(线程是否访问)
AQS里面还有一个内部类Node (双向链表,里面存放线程)
AQS里面获取锁状态方法getState()、修改锁状态方法通过CAS机制进行状态的更新

🪂集合可以引申过来:ConcurrentHashMap

聊到hashMap k-v存储
双列集合,键不能重复,值可以重复。它只适合单线程使用,在多线程情况下会报“并发修改异常”。他的键是由一个哈希表+链表的数据结构实现的。哈希表的初始大小为
16 ,通过对象哈希值%(哈希表长度-1)或者(哈希表长度-1)& 对象哈希值 确定对象在哈希表中的存储位置。如果出现哈希冲突了,通过拉链法解决,形成链表。
当哈希表存储超过0.75时,会进行2倍扩容。当每个节点上链表长度阈值超过8并且哈希表长度>=64,链表将转化为红黑树。当链表长度阈值为6时,红黑树会再次退化成链表。

hashTable它是线程安全的,因为它在每个方法上都加了synchronized锁,哪怕是在读方法上同样也上锁。虽然很安全,但是每个方法只允许一个线程进入,并发访问效率就比较低,适合并发量低的情况下。

在JUC并发包下还有一个ConcurrentHashMap的类。它是线程安全的。它采用了 CAS机制 +synchronized来保证线程安全。它是给节点加锁,降低了锁的粒度。
🏅put()时,先用key计算节点在哈希表中的位置,
它会来判断当前位置上有没有节点(null),如果没有,就使用CAS机制尝试放入。
如果有,就使用第一个节点作为锁对象,用synchronized加锁。这样就可以降低锁的粒度,可以同时有多个线程同时进入put()方法中,提高了并发的效率。但是如果线程在同一个位置操作,那必须还得一个一个来。
concurrentHashMap与hashTable都不允许存储null键,null值。
代码不允许,在源码中if(key==null ||value==null ) 抛出一个空指针异常

为什么不允许存储键为null或值为null?
为了消除歧义,因为如果值为null,不知道是原本Map中没有找到,还是本身就是null.。键同样也是这个道理,会不清楚传进去的键是因为其他原因报错导致的,还是本身就准备传进去一个为null的键。

🪂集合可以引申过来:CopyOnWriteArrayList

单列集合List
ArrayList 底层是数组实现的 ,可以存储重复元素,它是有序的(按添加顺序) , 可以实现动态扩容, 默认大小为10 , 可以进行1.5倍扩容,它是线程不安全的。
Vector 他是线程安全的 ,与ArrayList类似,默认大小为10 ,可以进行2倍扩容,它给每个方法上都加了synchronized锁。虽然它是安全的,但是锁是添加到方法上的,并发访问效率很低,适合并发量不大的情况。

JUC并发包下,CopyOnWriiteArrayList认为读取方法也加锁会造成浪费,因为读是不改变数据的。
它的读操作是不加锁的,并且为了尽可能的提高读的效率。对于改变数据的操作(插入 更新 )会先把原本数据复制到本地副本数组中进行修改,不影响原本数组,所以在写的过程中,也可以读数据。最后将修改好的本地副本数组直接替换为原本数组即可。(写的过程是加锁的,在方法里面使用了ReentrantLock锁,进行数组复制操作…)
它适合读多写少的场景。

CopyOnWriteArraySet底层基于CopyOnWriiteArrayList实现,线程安全的,不能存储重复的数据。

🦓辅助类CountDownLatch

它允许一个线程等待其他线程执行完成后再执行,底层是通过AQS来实现的。刚创建一个CountDownLatch对象时,指定一个初始化state表示需要等 待线程的数量,每当一个线程执行完成后 ,AQS的内部·state数量就减1。

🎀线程池

池:一个容器,将实现创建好的对象放到容器里面,待到使用的时候,不需要在去重新创建对象了,直接从池子里面拿,节省了创建对象的时间。用完不销毁,还放入池子中。

🐕为什么要使用线程池?

以前创建线程的时候,是直接创建线程,使用完后再销毁线程。如果线程比较多(测试5000个数据库连接对象),那么频繁的创建、销毁线程非常占时间,而使用线程池,可以省去频繁创建对象带来的时间开销,直接使用池子里面的,用完不销毁,在放入池子中,速度非常快。

🐕你通常是怎么创建线程池的?为啥不使用其他的类Executors?

通常使用TreadPoolExecutor类创建线程池的,为啥使用这个是因为我参考了《《阿里巴巴 java 开发规范》中推荐使用TreadPoolExecutor来创建线程,而不使用Executors来创建线程。因为TreadPoolExecutor类的构造方法中有七个参数配置,可以准确的配置对象的数量、最大等待的数量、拒绝策略…

🏨TreadPoolExecutor构造方法的七个参数

1.corePoolSize 核心池子中对象的数量
2. maximumPoolSize线程池最大线程对象数量
3. keepAliveTime非核心池对象多长空闲时间后销毁
4. unit:存活时间的单位
5. workQueue:阻塞队列,用于存放等待的线程
6. treadFactory线程工厂,主要用来创建线程
7. handler拒绝策略,表示拒绝处理任务时的策略
在这里插入图片描述

🪂线程池拒绝策略handler

  1. AbortPolicy 抛出异常
  2. CallerRunsPolicy 只要线程池未关闭,如果该任务被拒绝了,则由提交该任务的线程来执行此任务(例如main线程)
  3. DiscardOleddestPolicy 丢弃队列中等待时间最长的任务
  4. DiscardPolicy 直接丢弃任务,不予理会

🪂execute()与submit()的区别

execute( “实现Runnable接口任务” )返回值 void 适合不需要关注返回值的场景,
submit( “实现Callable接口任务” ) 返回值 Future 适合需要关注返回值的场景

🪂关闭线程池shutdown、shutdownNow

shutdown() 等待所有的任务执行完成,关闭线程池,在此期间不接受新的任务。
shutdownNow() 紧急关闭线程池,立即终止正在执行的线程,返回没有执行完任务的列表

🎀ThreadLocal 本地线程变量

是用来给每一个线程提供一个线程副本变量,使得每个线程中的变量是相互隔离的。
在ThreadLocal 底层源码中,它的内部维护了一个ThreadLocalMap内部类,如果为线程创建副本变量,就会判断该线程是否存在ThreadLocalMap
每个线程都有一个ThreadLocalMap属性,以ThreadLocal对象作为键,副本变量作为值。如果该线程已经有了ThreadLocalMap,那么就直接在Map中添加这个 ThreadLocal键 ,副本变量值 即可

🐕什么原因造成ThreadLocal内存泄漏问题?如何解决?

当本地变量不再使用时,由于ThreadLocalMap中这个vaule值还与外界保持着引用关系(强引用),这样一来,垃圾回收器就无法回收这个ThreadLocalMap对象了。
解决办法:
用完后就删除threadLocal.remove();
下次垃圾回收时,就可以回收ThreadLocalMap了。

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

信计2102罗铠威

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值