Java微服务架构师—并发编程基础(下)(1)

  • 集合遍历问题(list)

  • 共享成员变量

线程安全的集合

  • Collections.synchronized***

编码规约

  • 考虑代码健壮性

  • 常见场景敏感性

常用处理方法

  • ThreadLocal

  • 线程局部变量,是一种多线程间并发访问变量的解决方案。与synchronized等加锁方式不同,ThreadLocal完全不提供锁,而是以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全。

  • 从性能上说,ThreadLocal不具绝对优势,在并发不是很高的时候,加锁的性能会更好,但作为与锁完全无关的线程安全解决方案在高并发量或者锁竞争激烈的场景,使用ThreadLocal可以在一定程度上减少锁竞争问题。

  • Volatile

  • 概念定义

  • volatile关键字主要作用是使变量的更改在多个线程间即时可见。

  • 作用

  • 在多个线程间可进行变量的变更,使得线程间进行数据的共享可见。

  • 阻止指令重排序,happens-before语义

  • 内存模型JMM

  • 一个线程可以执行的操作有使用(use)、赋值(assign)、装载(load)、存储(store)、锁定()(lock)、解锁(unlock)

  • 而主内存可以执行的操作有读(read)、写(write)、锁定(lock)、解锁(unlock),每个操作都是原子性的操作

  • volatile的作用就是强制线程到主内存(共享内存)里去读取变量,而不是线程工作内存里去读取数据,从而实现了多线程间的变量的可见,也就是满足了线程安全的可见性

  • 解决的主要问题是一个线程对共享变量的写入何时对另一个线程可见

  • 所有的变量都存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程使用到的变量是主内存中的拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量也不例外)

  • 内存屏障

  • 指令重排序

  • 程序指令的执行顺序可能与代码书写顺序不一致,这个过程就是指令重排序。

  • 意义

  • JVM能根据处理器的特性,充分利用多级缓存、多核心等进行适当的指令重排序,是程序在保证业务正确运行的同时,充分利用CPU的执行特点,最大限度发挥机器性能

  • 有些场景不合适指令重排序(使用volatile解决)

  • HappensBefore (JMM内存机制)

  • 代码中一个操作执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,使用这个语义来阐述操作之间的内存可见性。

  • 相应规则

  • 一个线程的每一个操作,happens-before于该线程中后续任意操作(写操作happens-before于其后的读操作)

  • 对一个锁的解锁,happens-before与随后的对这个锁的加锁

  • 对一个volatile域的写,happens-before于任意后续对这个volatile域的读

  • 传递性规则:A h-b B 且 B h-b C 则 A h-b C

  • 应用案例

  • 停止线程

  • Atomic

  • 概念定义

  • 封装了一系列的基础类型和对象操作,主要目的是为了实现原子性操作(多线程操作的线程安全操作)

  • 保证一个JVM中是原子性、(同时不能为复合操作,否则中间的操作读取复合操作部分可能不对 虽然最终结果是对的 可以通过加锁解决

  • 主要核心类

  • AtomicInteger

  • AtomicLong

  • AtomicBoolean

  • AtomicIntegerArray

  • AtomicLongArray

  • AtomicReference

同步类容器(串行)


同步锁

  • Vector

  • HashTable

底层机制

  • 都是由JDK的Collections.synchronized***等工厂方法去创建实现的

  • 使用synchronized关键字对每个公用的方法都进行同步

  • 使用Object mutex对象锁使得每次只能有一个线程访问容器的状态

问题

  • 还可能出现(并发修改异常)

  • 并发处理性能差

  • 并发场景下,严重降低吞吐量

  • 锁竞争

并发类容器(异步)jdk1.5开始


ConcurrentHashMap

  • 基本使用

  • size()

  • 消除伪共享(缓存行)

  • 分之思想

  • volatile long value

  • 基本原理

  • 内部结构使用段(Segment)来表示不同的部分,每一段其实就是一个小的HashTable升级版,它们有自己的锁,降低了锁的粒度

  • 只要多个修改操作发生在不同的段上,就可以并发进行,默认16段,所以最高支持16个线程的并发修改操作

  • 减小锁的粒度达到降低锁竞争的方式,并且代码中大多共享变量采用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。

CopyOnWrite

  • 基本使用

  • jdk中常用的

  • CopyOnWriteArrayList

  • ConyOnWriteArraySet

  • 最佳使用场景

  • 读多写少,元素不能太多

  • 基本原理

  • 往一个容器添加元素时,不直接往当前的容器添加,而是将当前容器进行Copy,复制出一个新容器,然后新容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

  • 采用读写分离的思想,读和写不同的容器,实现对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前这个容器不会添加任何的元素。

ConcurrentSkipListMap

  • 支持并发排序

队列


(顶层接口Queue)

  • 并发队列(ConcurrentLinkedQueue)

  • 适应高并发场景的队列,通过无锁的方式,实现高并发状态下的高性能,通常性能好于BlockingQueue,是一个基于链表的无界线程安全队列

  • 该队列遵循先进先出原则,头是最先加入,尾是最后加入,该队列不允许null元素。

  • 基本使用

  • add()和offer()都是添加元素方法,在这里这两个方法没有任何区别

  • poll()和peek()都是取头元素节点,区别在于前者是会删除元素,后者不会

  • 阻塞队列(BlockingQueue)

  • 基本使用

  • offer(anObject) 将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳则返回true,否则返回false (不会阻塞线程)

  • offer(E e, long timeout ,TimeUint uint ) 设置等待时间,如果指定时间内,还不能往队列加入则返回失败。

  • put() 把anObject加入BlockingQueue里,如果BlockingQueue没有空间,调用此方法的线程会被阻塞直到队列里有空间为止。

  • poll(long timeout,TimeUint uint) 从队列里取出一个队首元素,如果在指定时间内,队列一旦有元素可取,则立即返回队列中的元素,否则直到时间超时还没有元素可取,返回失败。

  • take()取出BlockiQueue里排在队首的元素,若队列为空,阻塞进入等待状态直到队列有新的元素被加入。

  • drainTo() 一次性从队列获取所有可用的元素(还可以指定获取的个数),通过该方法,可以提高获取元素的效率,不需要多次分批加锁或释放锁。

  • 常用实现

  • 基于数组的阻塞队列实现ArrayBlockingQueue

  • 内部维护一个定长数组,缓存队列中的数据元素,其内部没有实现读写分离,也就意味着生产和消费不能完全并行。长度需要定义,可以指定先进先出或者先进后出,由于是定长所以也叫有界队列。

  • 子主题 2

  • LinkedBlockingDeque

  • PriorityBlockingQueue

  • 基于优先级的阻塞队列,出队列是依据优先级

  • 优先级的判断是通过构造函数传入的Compator对象来决定,也就是说传入的队列的元素必须实现Comparable接口,在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,也一个无界队列。

  • 元素不能为基本类型

  • SynchronousQueue

  • 一种没有缓冲的阻塞队列

  • 生产者生产的数据直接会被消费者获取并消费

  • DelayQueue

  • 带有延迟时间的队列

  • 其中的元素只有指定延迟时间到了,才能够从队列中获取到该元素,DelayQueue的元素必须实现Delayed接口,该队列是一个无界队列,应用场景:对缓存超时的数据进行移除,任务超时处理,空闲链接超时关闭等。

  • 阻塞队列手写模拟

  • 拥有固定长度的装载元素的容器

  • 计数器统计容器的容量大小

  • 当队列里面没有元素的时候需要执行线程等待

  • 当队列元素已满的时候执行的线程也需要等待

  • 合理的线程间通信


常见锁

  • 同步锁

  • Lock

  • LockSupport

常见实现

  • 对象锁

  • 类锁

JUC


Unsafe

  • 概念定义

  • 由于Java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作

  • 主要提供的功能

  • 内存操作

  • allocateMemory

  • 分配内存

  • reallocateMemory

  • 扩充内存

  • freeMemory

  • 释放内存

  • 字段的定位与修改

  • 可以定位对象某字段的内存位置也可以修改对象的字段值,即使它是私有的

  • 挂起与恢复

  • 将一个线程进行挂起是通过park方法实现的,调用park后,线程将一直阻塞直到超时或中断等条件出现

  • unpark可以终止一个挂起的线程,使其恢复正常。

  • 整个并发框架中对线程的挂起操作被封装在LockSupport类中,这个类中有各种版本的pack方法,但最终都调用了Unsafe.park()方法

  • CAS操作(乐观锁)

  • Compare And Swap 简单来说就是比较并交换。

  • CAS包含三个操作数,内存位置(V)、预期原值(A) 和 新值(B)

  • 如果内存位置的值与预期原值匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何处理,无论哪种情况,它都会在CAS指令之前返回该位置的值。

  • CAS有效的说明:当某个位置v应该包含值A,如果包含该值,则将B放到这个位置,否则,不用更改该位置,值需告诉这个位置现在的值即可

  • Java并发包(java.util.concurrent)中大量使用CAS操作,涉及到并发的方法都调用了sun.misc.Unsafe类方法进行CAS操作,在Unsafe中是通过compareAndSwapXXX方法实现的

AQS

  • AQS核心

  • 概念定义

  • AbstractQueuedSynchronizer 抽象队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现底层依赖于它,比如:ReetrantLock/Semaphore/CountDownLatch

  • 内部设计

  • 它内部维护一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会放入此队列),这里volatile是核心关键字,具体volatile的语义,state的访问方式有三种

  • getState()

  • setState()

  • compareAndSetState()

  • AQS定义两者资源共享方式

  • 独占式Exclusive

  • 只有一个线程能执行(如:ReetrantLoack)

  • 共享式Share

  • 多个线程可同时执行,(如:Semaphore、CountDownLatch)不同的自定义同步器争用共享资源的方式也不同

  • CLH队列(FIFO)锁)

  • head、tail

  • 核心方法

  • 自定义同步器在实现时只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队),AQS已经在底层实现好了。自定义同步器实现主要实现如下几种方法

  • isHeldExclusively()

  • 该线程是否正在独占资源,只有用到Condition才需要实现它

  • tryAcquire()

  • 独占方式,尝试获取资源,成功则返回tru,失败则返回false

  • tryRelease()

  • 独占方式,成功则返回true,失败则返回false

  • tryAcquireShared(int)

  • 共享方式,尝试获取资源,负数表示失败,0表示成功,但没有剩余可以资源;正数表示成功,且有剩余资源

  • tryRelaeseShared(int)

  • 共享方式,尝试获取资源,成功则返回true,失败则返回false

  • 锁方式

  • 公平锁

  • 队列排队,先进先服务

  • 非公平锁

  • ReetrantLock重入锁

  • state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占该锁并将state+1,此后其他线程在tryAcquire()时就会失败,直到A线程unlock()到state=0(释放锁)为止,其他线程才能有机会获取该锁,当然释放锁之前,A线程自己是可以重新获取此锁的(state会累加)这就是可重入的概念,但要注意,获取多少次就要释放多少次,这样才能保证state能回到零态的

  • 重入锁,在需要进行同步的代码部分加上锁定,不要忘记最后一定要释放锁定,不然会造成锁无法释放,其他线程进不了的问题

  • 重载的有参构造可以选择公平锁和非公平锁实现方法

  • 应用场景

  • 重入锁需求

  • 阻塞唤醒多个线程

  • Condition

  • 可配合ReetrantLock实现阻塞唤醒线程通信

  • 在使用Lock,可以使用一个等待、通知的类,它就是Condition,这个类一定是针对具体某一把锁的,也就是在只有锁的基础上才会产生Condition

  • 我们可以通过一个Lockd对象产生多个Condition进行多线程间的交互,可以使得部分需要唤醒的线程唤醒,其他线程则继续等待通知

  • await、signal signalall

  • ReadWriteLock读写锁

  • ReetrantReadWriteLock

  • 其核心是实现读写分离的锁,在高并发访问下,读多写少的情况下,性能高于重入锁

  • 分成两把锁,读锁和写锁,在读锁下,多线程可以并发访问,但在写锁的时候只能一个一个顺序访问

  • 口诀:读读共享,写写互斥,读写互斥

  • lockSupport(线程锁)

  • LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于‘许可(permit)’ 作为关联,permit相当于一个信号量。默认为0,线程之间不再需要一个Object获取其他变量存储状态,不需要关心对方状态

  • 对象Object锁缺点:先阻塞再唤醒 配合synchronized一起使用

  • LockSupport静态方法 park和unpark

  • 没有顺序限制

  • 针对具体某个线程

  • 不需要在同步代码块里,所以线程间也不需要维护一个共享的同步对象,实现线程间的解耦

  • unpark函数可以先于park调用,所以无需担心线程的执行先后顺序

  • 底层调用Unsafe类的方法

  • 减少锁竞争

  • 避免死锁

  • 减少锁的持有时间

  • 减少锁的粒度

  • 锁的功能分离

  • 尽量使用无锁的操作,如原子操作(Atomic系列类),volatile关键字,CAS

  • AQS源码分析

  • acquire(int)

  • 此方法是独占模式下线程获取共享资源的顶层入口,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,整个过程忽略中断的影响,这也正是lock()的语义,当然不仅仅只限制于lock(),获取资源后,线程就可以去执行其临界代码了

  • AQS核心acquire() ,tryAcquire()尝试获取资源,如果成功则直接返回,addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;acquireQueue()使线程在等待队列中获取资源,一直获取到资源后才返回,如果整个等待过程被中断过,则返回tru,否则返回false。 如果线程在等待过程中被中断过,它是不响应的,只是获取资源后才再进行自我中断selfInterrupt将中断补上

  • UML类绘制

  • CountDownLatch

  • 任务分成n个子线程去执行,state也初始化n(注意n要与线程数一致)。这n个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1,等到所以子线程都执行完后(state=0),会unpark()调用线程,然后主调用线程就会从await()函数返回继续后续操作

最后,附一张自己面试前准备的脑图:

image

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

image

  • Spring全家桶(实战系列)

image.png

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

image

很多人感叹“学习无用”,实际上之所以产生无用论,是因为自己想要的与自己所学的匹配不上,这也就意味着自己学得远远不够。无论是学习还是工作,都应该有主动性,所以如果拥有大厂梦,那么就要自己努力去实现它。

最后祝愿各位身体健康,顺利拿到心仪的offer!

线程去执行,state也初始化n(注意n要与线程数一致)。这n个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1,等到所以子线程都执行完后(state=0),会unpark()调用线程,然后主调用线程就会从await()函数返回继续后续操作

最后,附一张自己面试前准备的脑图:

[外链图片转存中…(img-EQui48OU-1714370950075)]

面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典

  • Java核心知识整理

[外链图片转存中…(img-XqvqQx4w-1714370950076)]

  • Spring全家桶(实战系列)

[外链图片转存中…(img-WCeLD2XX-1714370950076)]

Step3:刷题

既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。

以下是我私藏的面试题库:

[外链图片转存中…(img-zLOGqWTh-1714370950076)]

很多人感叹“学习无用”,实际上之所以产生无用论,是因为自己想要的与自己所学的匹配不上,这也就意味着自己学得远远不够。无论是学习还是工作,都应该有主动性,所以如果拥有大厂梦,那么就要自己努力去实现它。

最后祝愿各位身体健康,顺利拿到心仪的offer!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值