2.JUC

news2024/11/26 13:35:10
  1. 掌握 Java 并发编程,熟悉线程池并发容器以及锁的机制,如 sychronized、ReentrantLock、AQS等。

在这里插入图片描述

2.0 并发基础

线程等待唤醒机制LockSupport (3种)

方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程(配合synchronized)
方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程(配合ReentrantLock)
方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程。

前两种方式的使用限制条件:

  • 线程需要先获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有的方法都是静态方法,可以让线程再任意位置阻塞,阻塞后也有对应的唤醒方法。归根结底,LockSupport时调用Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程,LockSupport和每个使用它的线程都有一个许可(Peimit)关联,每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark也不会积累凭证

线程通信方式

volatile 、Object类的 wait/notify 方法、Condition类的 await/signal 方法、Semaphore的acquire/release方法、通过Thread类的join()方法

线程通信:用于多个线程之间协作工作,共同完成某个任务。多个线程在并发执行的时候,他们在CPU中是随机切换执行的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务。

线程通信方式

  • **通过 volatile 关键字:**多个线程同时监听一个volatile变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。利用了volatile可见性,即一旦修改变量则立即刷新到共享内存中。
  • **通过Object类的 wait/notify/notifyAll 方法:**当我们使用synchronized同步时就会使用Monitor来实现线程通信,这里的Monitor其实就是锁对象,其利用Object类的wait,notify,notifyAll等方法来实现线程通信。Monitor是Java虚拟机实现锁的一种底层机制,用于控制线程对共享资源的访问。(Object类的wait()方法让线程进入等待状态,notify()唤醒该对象上的随机一个线程,notifyAll()唤醒该对象上的所有线程。这3个方法必须处于synchronized代码块或方法中,否则会抛出IllegalMonitorStateException异常。因为调用这三个方法之前必须拿要到当前锁对象的监视器(Monitor对象),synchronized基于对象头和Monitor对象。)
  • **通过Condition类的 await/signal 方法:**而使用Lock进行同步时就是使用Condition对象来实现线程通信,Condition对象通过Lock的lock.newCondition()方法创建,使用其await,sign或signAll方法实现线程通信。 Condition 是一个与锁 Lock 相关联的条件对象,可以让等待线程在某个条件被满足时被唤醒,从而达到线程协作的目的。
  • 通过Semaphore的acquire/release方法: Semaphore是一个计数信号量,用于控制同时访问某个资源的线程数量。线程可以通过acquire()方法获取许可,release()方法释放许可。
  • **通过Thread类的join()方法:**join() 方法是等待该线程执行完成。A线程调用B线程的join()方法,A线程将被阻塞,直到B线程执行完。

execute和submit有什么区别?

execute和submit都是ExecutorService接口的方法,**用于向线程池提交一个 Runnable 任务,让线程池执行这个任务。**所有线程池都直接或间接实现ExecutorService接口。

execute:参数只能是Runnable,没有返回值
submit:参数可以是Runnable、Callable,返回值是FutureTask

2.1线程池机制(重点!!!)

并发专题汇总:https://blog.csdn.net/qq_59366033/article/details/132391455

线程池面试题

线程池概念:

线程池就是采用池化思想(类似连接池、常量池、对象池等)管理线程的工具,不需要每次接收到一个请求就创建一个线程去处理,用的时候直接从线程池取,用完放回去,这样可以重复使用,大大节省了创建线程的资源开销。

最常用的包括ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool等。ThreadPoolExecutor是最通用的线程池实现。

创建线程池的方法有哪些?

newFixedThreadPool newSingleThreadExecutor newCachedThreadPool newScheduledThreadPool等。

ThreadPoolExecutor的核心参数是什么?工作原理是什么?拒绝策略有哪些?

核心线程数(corePoolSize):池中保持的最小线程数。
最大线程数(maximumPoolSize):池中最大允许的线程数。

线程空闲时间(keepAliveTime):当线程数大于核心线程数时,多余的线程在空闲指定时间后被销毁。

线程存活时间单位(TimeUnit unit)

阻塞队列(BlockingQueue):等待执行的任务队列(5个)。

线程工厂(ThreadFactory):用于创建线程,execute()。

拒绝策略(RejectedExecutionHandler):当队列满并且线程数达到最大线程数时,用于处理新任务的策略。

工作原理

如果线程池中的线程数少于核心线程数,创建新线程执行任务。

如果线程池中的线程数达到核心线程数,将任务放入阻塞队列。

如果队列已满,但线程数未达到最大线程数,创建新线程执行任务。

如果队列已满且线程数已达到最大线程数,根据拒绝策略处理任务。

拒绝策略:

AbortPolicy:终止策略,线程池会抛出异常并终止执行此任务

CallerRunsPolciy:把任务交给调用者的(main)线程来执行;

DiscardPolicy:忽略当前最新任务(最新的任务);

DiscardOldestPolicy:忽略任务队列中最早加入的任务,接收新任务(加入任务队列)

如何确定核心线程数?(难)

① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换

② 并发不高、任务执行时间长

IO密集型的任务 --> (CPU核数 * 2 + 1)

计算密集型任务 --> ( CPU核数+1 )

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器

是第二步,至于线程池的设置,设置参考(2)

为什么不建议用Executors创建线程池,而是使用ThreadPoolExecutor?

允许的请求队列的长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。

如何终止线程池

shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。
shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

如何判断线程池中的任务是否执行完成?

1、使用 isTerminated 方法判断
2、使用 getCompletedTaskCount方法判断
3、使用CountDownLatch判断
4、使用 CyclicBarrier判断

2.2 ThreadLocal

谈谈你对ThreadLocal的理解?

ThreadLocal 主要功能有两个:

  • 实现资源对象的线程隔离,避免争用引发的线程安全问题。

  • 实现了线程内的资源共享

底层原理实现?

在ThreadLocal内部维护了一个 ThreadLocalMap类型的成员变量(内部类),用来存储资源对象。

当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为value,放入当前线程的 ThreadLocalMap 集合中

当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值。

当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值。

内存泄露问题知道吗?

首先,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。

其次,是因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用

在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出

ThreadLocal的key是弱引用,这是为什么?

  • 当方法执行完毕后,栈帧销毁,强引用t1也就没有了,但此时线程的ThreadLocalMap里某个entry的Key引用还指向这个对象,若这个Key是强引用,就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收,造成内存泄露
  • 若这个引用时弱引用就大概率会减少内存泄漏的问题(当然,还得考虑key为null这个坑),使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null
ThreadLocal中最后为什么要加remove方法?

由于ThreadLocalMap的value是强引用,所以无法被垃圾回收。

ThreadLocal.remove() 方法可以手动释放 ThreadLocalMap 中的 value,防止内存泄漏。

2.3 Java对象内存布局

对象头+实例数据+对齐填充

对象头:(在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节)

  • 对象标记(Mark Word)

    • 默认存储对象的HashCode分代年龄锁标志等信息。

    • 这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。

    • 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。

  • 类元信息(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象哪个类的实例

实例数据:存放类的属性(Field)数据信息,包括父类的属性信息。

对齐填充:(保证8个字节的倍数)

  • 虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐。

对象头的MarkWord对象标记

在这里插入图片描述

2.4 并发容器(重点!!!)

Java中的并发容器主要包括以下几类:

  1. Map 类型

    • ConcurrentHashMap:线程安全的HashMap实现,通过锁分段技术和CAS算法保证线程安全和运行效率。在JDK 1.7中采用分段锁来减少锁的竞争,而在JDK 1.8中则直接使用Node数组+链表/红黑树的数据结构来实现,并发控制使用synchronized和CAS操作。

    • ConcurrentSkipListMap:基于跳表的并发Map实现,使用跳表的数据结构进行快速查找。

      跳表原理:链表加多级索引的结构 https://blog.csdn.net/2401_83977530/article/details/137216610

  2. List 类型

    • CopyOnWriteArrayList:线程安全的List实现,适用于读多写少的场景。当新增和删除元素时,会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。
  3. Queue 类型

    • ConcurrentLinkedQueue:基于链表实现的并发队列,使用乐观锁(CAS)保证线程安全。
    • ConcurrentLinkedDeque:基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出(FIFO)外,也可以先进后出(FILO)。
  4. Set 类型

    • CopyOnWriteArraySet:基于CopyOnWriteArrayList实现的并发Set,每次add都要遍历整个集合才能知道是否存在,不存在时需要插入(加锁)。

这些并发容器通过不同的机制保证线程安全,如锁分段技术、CAS算法等,以提高并发访问的性能和效率。

2.5 锁机制(重点!!!)

Java中的锁:

对锁的态度:悲观锁 乐观锁 (ReentrantLock是悲观锁:https://blog.csdn.net/m0_38045306/article/details/119812773)

对数据的操作方式:ReadWriteLock / 共享锁 互斥锁

可重入锁:synchronized ReentrantLock

可重试/自旋锁:SpinLock (CAS + 循环) 常见的如ReentrantLock (sync在jdk1.6后才支持自旋!)

是否公平:公平锁 不公平锁

锁的状态:无锁 , 偏向锁 , 轻量级锁 , 重量级锁

AQS

分段锁

死锁

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象。

发生死锁的必要条件:

  1. 互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
  2. 占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
  3. 非抢占:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
  4. 循环等待:有一组等待进程 {P0, P1,..., Pn}P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,……,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。

排查死锁方法

  • 纯命令
    • jps -l
    • jstack 进程编号
  • 图形化
    • jconsole

预防死锁的方法:

  1. 按顺序获取锁:确保所有线程获取锁的顺序是一致的,这样可以避免循环等待的情况发生。
  2. 使用超时等待:在获取锁的时候使用超时等待机制,如果超过一定时间还未获取到锁,可以放弃或重新尝试。
  3. 避免嵌套锁:尽量避免在持有一个锁的情况下去获取另一个锁,这可能导致死锁。
  4. 使用锁的粒度尽量小:锁的粒度越小,发生死锁的可能性越小。
  5. 使用工具检测死锁:可以使用一些工具来检测和排查死锁,如上述的 jstackjconsole 工具。

注意:ReentrantLock是悲观锁,底层是基于AQS,虽然用到了CAS乐观锁,但这里的CAS只是为了判断线程是否获取到了锁,保证获取锁和释放锁操作的原子性,而不是ReentrantLock用的CAS判断共享值有没有被改变。。

CAS(Compare-And-Swap)在 ReentrantLock 中的作用主要是确保锁状态的原子性更新,以保证线程安全。即保证获取锁的时候锁状态更新的线程安全问题,防止并发时多个线程都获取锁成功释放锁也需要提高CAS保证释放操作的原子性,防止出现死锁的情况。

2.6 synchronized和Lock的区别 ?

接口和关键字、死锁问题、让等待锁的线程响应中断、得知是否成功获取锁、性能对比、分布式锁

  • 接口和关键字。Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  • 死锁问题。synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • 让等待锁的线程响应中断。Lock可以可以通过lockInterruptibly()获取锁的方法让等待锁的线程响应中断。而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 得知是否成功获取锁。通过Lock可以通过tryLock()知道有没有成功获取锁,而synchronized却无法办到。
  • 性能对比。两者性能差不多。JDK6之前synchronized没有锁升级,线程竞争非常激烈时Lock的性能要远远优于synchronized;而JDK6引入锁升级后,线程竞争激烈时候两者性能也相差无几。
  • **分布式锁:**SETNX、Redisson。Redisson基于Redis协议,可以实现可重入锁、公平锁、读写锁、信号量、闭锁(计数器),支持看门狗自动续期。

2.7 synchronized 和 ReentrantLock 的区别?

1.实现方式:synchronized是Java语言内置的关键字,基于JVM实现,锁的获取和释放是交给jvm自动处理的;ReentrantLock是基于Java的Lock接口实现,需要手动编写代码,并使用lock()方法获取锁,使用完毕后需要显式调用unlock()方法释放锁。。

2.是否是自旋锁:都是可重入锁,但是synchronized不是自旋锁,获取锁失败就会阻塞等待,等待过程不能被中断;ReentrantLock是自旋锁,利用了CAS+循环,获取锁失败就会不断的重试获取锁。

3.锁的公平性:synchronized是非公平锁;ReentrantLock既可以是公平锁也可以是非公平锁,默认情况下是非公平锁。

4.条件变量支持。synchronized没有直接对应的条件变量功能;ReentrantLock可以通过Condition对象实现线程间的等待和唤醒操作。

5.性能表现:在低并发情况下,synchronized的性能通常优于ReentrantLock;在高并发情况下,ReentrantLock的性能可能更佳,因为它提供了更多的控制选项和更灵活的锁策略。

2.8 synchronized底层原理

介绍、作用于三个位置、对象头、Monitor对象、锁升级

1.synchronized锁介绍:

synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。synchronized锁基于对象头、CAS、Monitor对象,在1.6之后引入轻量级锁、偏向锁等优化。

2.作用于三个位置:

1.作用在静态方法上,则锁是当前类的Class对象。

2.作用在普通方法上,则锁是当前的实例(this)。

3.作用在代码块上,则需要在关键字后面的小括号里,显式指定锁对象,例如this、Xxx.class。

3.对象头存储锁信息

synchronized的底层是采用对象头的Mark Word来存储锁信息的。Hotspot 虚拟机中每个对象都有一个对象头(Object Header),包含Mark Word(标记字段) 和 Class Pointer(类型指针)。

  • Mark Word(标记字段):存储哈希码、GC分代年龄、锁信息、GC标记(标志位,标记可达对象或垃圾对象)等。锁信息包括:
    • 锁标志位:64位的二进制数,通过末尾能判断锁状态。01未锁定、01可偏向、00轻量级锁、10重量级锁、11垃圾回收标记
    • 偏向锁线程ID、时间戳等
    • 轻量级锁的指针:指向锁记录的指针
    • 重量级锁的指针:指向Monitor锁的指针
  • **类型指针:**指向它的类元数据的指针,用于找到对象所在的类。
    不考虑共享资源是类变量等特殊情况的话,有共享资源的多个线程通常都属于同一个对象。

4.Monitor对象:

在这里插入图片描述

2.9 synchronized锁升级:

JDK6之前synchronized只有无锁和重量级锁两个状态,JDK6引入偏向锁、轻量级锁两个状态,锁可以根据竞争程度从无锁状态慢慢升级到重量级锁。当竞争小的时候,只需以较小的代价加锁,直到竞争加剧,才使用重量级锁,从而减小了加锁带来的开销。

  • **锁升级顺序:**无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
  • **无锁:**没有线程访问同步代码块时。没有对资源进行锁定,所有的线程都能访问并不断修改同一个资源,但同时只有一个线程能修改成功,失败线程会不断重试。
  • **偏向锁:**当有一个线程访问同步代码块时升级为偏向锁。一段同步代码块一直被一个线程所访问,那么该线程id会CAS写入对象头,下次再访问同步代码块时对象头检查到该线程id,就不用加锁解锁了,降低获取锁的代价。
  • **轻量级锁(自旋锁):**有锁竞争时升级为轻量级锁。其他线程会通过自旋的形式尝试通过CAS将对象头中Mark Word替换为指向线程栈帧里锁记录的指针,从而获得锁;同时线程锁记录里存放Mark Word信息。竞争的线程不会阻塞但会一直自旋,消耗CPU性能,但速度快。
  • **重量级锁:**锁膨胀(自旋失败10次)时升级为重量级锁。Mark Word中存储的是指向Monitor锁的指针,对象Mark Word信息也会保存在Monitor锁里,当一个线程获取了Monitor锁后,竞争线程会被阻塞,不再自旋,不消耗CPU,速度慢。

2.10 ReentrantLock的底层原理?

ReentrantLock是一个可重入锁:调用 lock 方法获取了锁之后,再次调用lock,是不会再阻塞,内部直接增加重入次数 就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。

ReentrantLock是属于juc包下的类,属于api层面的锁,跟synchronized一样,都是悲观锁。通过lock()用来获取锁,unlock()释放锁。

它的底层实现原理主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似,底层直接AQS

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。

ReentrantLock 底层使用了CAS,为什么它不是乐观锁而是悲观锁?

2.11 AQS 和 CAS(重点)

AQS

概念、原理、同步状态、应用场景、同步队列、基于模板方法

1.AQS(AbstractQueuedSynchronizer抽象队列同步器):

AQS是一个抽象类,在JUC.locks包下,它通过内部的状态变量和同步队列来实现线程的同步(允许多个线程协作共享访问共享资源)。

2.state变量和等待队列:

state变量:在AQS中,volatile修饰的int类型的state变量表示锁的状态,通过CAS原子操作这个状态变量来保证线程安全。初始是0,代表没拿到锁,1代表拿到锁。
同步队列:在AQS中,FIFO(先进先出)队列用来管理等待锁的线程,队列每个节点记录等待锁的线程的地址、状态、等待锁的条件。先进先出确保同步器的公平性(公平锁的实现),也就是先等待的线程先获得锁。非公平锁实现不采用队列先进先出的特点,而是所有节点CAS来获取锁。队列底层是双向链表

3.实现线程同步的原理:

线程通过CAS原子性修改state变量,修改成功则获得锁,失败则插入队尾等待。

4.应用场景:

很多锁和同步器都是基于AQS实现的。ReentrantLock,ThreadPoolExecutor,CountDownLatch等都是基于AQS实现。

  • ReentrantLock:可重入锁,同一个线程在持有锁的情况下,可以重复地获取该锁,无需等待,只需记录重入次数。能防止死锁,因为不用线程自己等待自己释放锁。是Lock接口的实现类。可以通过构造参数true或false指定公平锁或非公平锁,可以通过newCondition()方法创建多个Condition对象分组唤醒等待线程。
    • 公平锁:按加锁顺序获取锁。线程竞争锁时判断AQS队列里有没有等待线程,有就加入队尾。
    • 非公平锁(默认):可能某个线程会不断获取锁,牺牲公平的情况下提高了效率。不管AQS队列里有没有等待线程,都会先尝试获取锁;如果抢占不到,再加入队尾。如果线程刚好在上个线程释放时拿到锁,就不用像公平锁那样还要阻塞等待、放队尾、唤醒,这些操作涉及到对内核的切换,对性能有影响。
    • Condition对象:用于线程间通信,通过await()和signal(),signalAll()让线程等待或唤醒。通常用lock锁创建Condition对象,即lock.newCondition();
  • CountDownLatch:计数器,它允许一个或多个线程等待其他线程完成操作后再执行。countDown()方法让计数器减一,await()方法阻塞当前线程直到计数器减为0。
  • ThreadPoolExecutor

5.基于模板方法:

AQS是基于模板方法模式进行设计的。锁的实现类需要继承AQS并重写抽象的行为方法,方法包括:独占式获取同步状态、独占式释放同步状态、共享式获取同步状态、共享式释放同步状态。

https://blog.itpub.net/70024924/viewspace-3008324/

CAS

1.CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。它包含三个操作数—内存位置预期原值更新值

执行CAS操作的时候,将内存位置的值与预期原值进行比较:

  • 如果相匹配,那么处理器会自动将该位置更新为新值
  • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

2.应用场景原子类、AQS、并发容器、synchronized的偏向锁和轻量级锁

原子类各原子操作的方法,AQS修改state变量时是CAS原子性修改,ConcurrentHashmap初始化数组和头结点、扩容、插入链表头结点时是CAS操作。

synchronized偏向锁和轻量级锁

偏向锁:当有一个线程访问同步代码块时升级为偏向锁。一段同步代码块一直被一个线程所访问,那么该线程id会CAS写入对象头,下次再访问同步代码块时对象头检查到该线程id,就不用加锁解锁了,降低获取锁的代价。
轻量级锁(自旋锁):有锁竞争时升级为轻量级锁。其他线程会通过自旋的形式尝试通过CAS将对象头中Mark Word替换为指向线程栈帧里锁记录的指针,从而获得锁;同时线程锁记录里存放Mark Word信息。竞争的线程不会阻塞但会一直自旋,消耗CPU性能,但速度快。

在操作共享变量的时候使用的自旋锁,效率上更高一些

CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的。

ABA问题

ABA问题是在使用CAS(Compare and Swap)操作时可能遇到的一种典型问题。它指的是一个共享变量的值在操作期间从A变为B,然后再从B变回A,而CAS操作可能会错误地认为没有其他线程修改过这个值。这会导致CAS操作的误判,可能会引发潜在的问题。

例如,假设有两个线程T1和T2,它们对一个共享变量V执行CAS操作,初始值为A。线程T1首先将V的值从A改变为B,然后再将其从B改回A,而线程T2在此期间可能执行CAS操作,由于期望值是A,CAS操作将成功,尽管T1在期间对V进行了多次改变。

解决方法:解决ABA问题的方法是使用版本号或标记。这样,在CAS操作中,不仅需要比较共享变量的值,还需要比较版本号或标记。只有在值和版本号都匹配时,CAS操作才会成功。

在Java中,可以使用AtomicStampedReference类来解决ABA问题。它允许您在CAS操作中包含一个标记,以便跟踪共享变量的版本。通过比较值和标记,可以避免ABA问题。

2.12 JMM 和 volatile

  • 你知道什么是Java内存模型JMM吗?
  • JMM没有那些特征或者它的三大特征是什么?
  • happens-before先行并发原则你有了解过吗?
  • 请谈谈你对 volatile 的理解?
  • volatile凭什么可以保证可见性和有序性?
  • 内存屏障Memory Barrier

你知道什么是Java内存模型JMM吗?

JMM(Java内存模型Java Memory Model)本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范。

我们定义的所有共享变量都储存在物理主内存中。

每个线程都有自己独立的工作内存(主内存中该变量的一份拷贝)。

线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存在读写(不能越级)。

不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能互相访问)。

JMM有那些特征或者它的三大特征是什么?

原子性:指一个操作是不可被打断的,即多线程环境下,操作不能被其他线程干扰。

可见性:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。

有序性:为了提升性能,编译器和处理器对指令序列进行重新排序,单线程环境里确实能够保证程序最终执行结果和代码顺序执行的结果一致多线程环境中线程交替执行,由于编译器优化重排的存在,可能出现乱序现象,两个线程使用的变量能否保证一致性是无法确定的,结果无法预测

happens-before先行并发原则你有了解过吗?

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 如果两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

请谈谈你对 volatile 的理解?

volatile 是一个关键字,可以修饰类的成员变量类的静态成员变量,主要有两个功能

第一:保证变量的可见性,某个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

第二: 禁止指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

volatile凭什么可以保证可见性和有序性?

通过内存屏障

有序性保证

  • 在每一个volatile写操作前面插入一个StoreStore屏障—>StoreStore屏障可以保证在volatile写之前,其前面所有的普通写操作都已经刷新到主内存中。
  • 在每一个volatile写操作后面插入一个StoreLoad屏障—>StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序
  • 在每一个volatile读操作后面插入一个LoadLoad屏障—>LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序
  • 在每一个volatile读操作后面插入一个LoadStore屏障—>LoadTore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序

可见性保证

  • 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)

内存屏障(重点)

是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也称为内存栅栏或栅栏指令。

作用:

  • 阻止屏障两边的指令重排序
  • 写操作时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
  • 读操作时加入屏障,线程私有工作内存的数据失效,重新回到主物理内存中获取最新值

总结:

  • volatile写之前的操作,都禁止重排序到volatile之后
  • volatile读之后的操作,都禁止重排序到volatile之前
  • volatile写之后volatile读,禁止重排序

2.13 线程使用场景问题

如果控制某一个方法允许并发访问线程的数量?

在jdk中提供了一个Semaphore类(信号量),它提供了两个方法:

  • semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。

  • 第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1。

如何保证Java程序在多线程的情况下执行安全呢?

1.使用同步机制:synchronized ReentrantLock

2.原子类:AtomicInteger等。

3.使用并发容器:List Set Queue Map 相关并发容器

4.ThreadLocal线程隔离实现同一个线程的不同组件之间的数据共享。

  • 原子类、synchronized、LOCK,可以解决原子性问题

  • synchronized、volatile、LOCK,可以解决可见性问题

  • Happens-Before 规则可以解决有序性问题

你在项目中哪里用了多线程?

参考场景一:

es数据批量导入

在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以当时我就想到可以使用线程池的方式导入,利用CountDownLatch+Future来控制,就能大大提升导入的时间。

参考场景二:

单个业务涉及多个微服务的调用:

在我做那个xx电商网站的时候,里面有一个数据汇总的功能,在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2190234.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构 ——— 单链表oj题:链表分割(带哨兵位单向不循环链表实现)

目录 题目要求 手搓简易单链表 代码实现 题目要求 现有一链表的头指针 ListNode* head ,给一定值 x ,编写一段代码将所有小于 x 的节点排在其余节点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头节点 举例说明&a…

【包教包会】2D图片实现3D透视效果(支持3.x、支持原生、可合批)

将去年写的SpriteFlipper从2.x升级到3.x。 如果需要2.x版本或需要了解算法思路,请移步:https://blog.csdn.net/weixin_42714632/article/details/136745051 优化功能:可同时绕X轴和Y轴旋转,两者效果会叠加。 完美适配Web、原生…

typescript使用webpack打包编译问题

解决方案:在webpack.config.js中的mdule.exports中设置mode。 再次运行npm run start即可。

Python基本库的使用--urllib

开篇 本篇文章旨在总结urlib基本库的一些常用用法。 相关方法 urlopen 用户打开和读取URL。 import urllib.requestresponse urllib.request.urlopen(https://www.python.org) print(response.read().decode(utf-8))带data参数 import urllib.parse import urllib.requestda…

【计算复杂性理论】P可归约(归约,P-reducible)与P、NP、NP-Hard、NP-Complete问题

1 问题背景 如果想要了解P问题、NP问题、NP-Hard问题、NP-Complete问题之间的关系,那就需要从了解NP-complete问题和归约概念开始。上一篇文章中,我们介绍了计算复杂性理论的奠基之作《The Complexity of Theorem-Proving Procedures》,在这篇…

初识算法 · 滑动窗口(1)

目录 前言: 长度最小的子数组 题目解析 算法原理 算法编写 无重复长度的最小字符串 题目解析 算法原理 算法编写 前言: 本文开始,介绍的是滑动窗口算法类型的题目,滑动窗口本质上其实也是双指针,但是呢&#…

初识数据结构--时间复杂度 和 空间复杂度

数据结构前言 数据结构 数据结构是计算机存储、组织数据的方式(指不仅能存储数据,还能够管理数据-->增删改)。指相互之间存在一种或多种特定关系的数据元素的集合。没有单一的数据结构对所有用途都有用,所以我们要学习各种的数据结构,比…

[Python] 编程入门:理解变量类型

文章目录 [toc] 整数常见操作 浮点数字符串字符串中混用引号问题字符串长度计算字符串拼接 布尔类型动态类型特性类型转换结语 收录专栏:[Python] 在编程中,变量是用于存储数据的容器,而不同的变量类型则用来存储不同种类的数据。Python 与 C…

Springboot项目整合RabbitMQ+Redis实现可靠的阿里云短信异步收发功能(手把手实操详细教程)

文章目录 1、项目介绍1.1、项目描述1.2、项目结构 2、创建项目(idea)2.1、依赖引入2.2、 配置文件2.3、 数据库表2.4、 实体类2.5、 配置类2.6、 验证码服务类2.7、 短信发送服务类2.8、 消费者类2.9、发送服务类2.10、定时任务类2.11、启动类2.12、测试控制器 3、效果测试4、总…

计算机科学英语词汇汇总(上)(Computer Science English Complete Vocabulary)

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…

对不经常变动的数据集合添加Redis缓存

目录 前言 什么是缓存 如何使用缓存 添加商户缓存 缓存模型和思路 实现代码 问题分析 解决方案 实现商铺和缓存与数据库双写一致 实现代码 前言 什么是缓存 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码 缓存…

【框架篇】过滤器和拦截器的区别以及使用场景

在项目开发中,常常会同时配置拦截器(Interceptor)和过滤器(Filter),以下就是它们两个主要的区别: 过滤器(Filter) 配置和实现 Filter的实现还是很简单的,可…

提升快递管理效率的必备技能:教你批量查询与导出物流信息

在当今快节奏的商业环境中,快递与物流行业的效率直接关系到企业的运营成本和客户满意度。随着订单量的不断增加,如何高效地管理和追踪大量的物流信息成为了企业面临的一大挑战。批量查询与导出物流信息作为一种高效的数据处理手段,正逐渐成为…

微信小程序-npm支持-如何使用npm包

文章目录 1、在内建终端中打开2、npm init -y3、Vant Weapp4、通过 npm 安装5、构建 npm 1、在内建终端中打开 Windows PowerShell 版权所有 (C) Microsoft Corporation。保留所有权利。尝试新的跨平台 PowerShell https://aka.ms/pscore6PS C:\Users\dgq\WeChatProjects\minip…

重学SpringBoot3-集成Redis(三)

更多SpringBoot3内容请关注我的专栏:《SpringBoot3》 期待您的点赞👍收藏⭐评论✍ 重学SpringBoot3-集成Redis(三) 1. 引入 Redis 依赖2. 配置 RedisCacheManager 及自定义过期策略2.1 示例代码:自定义过期策略 3. 配置…

大学生就业招聘系统:Spring Boot技术解析

3系统分析 3.1可行性分析 通过对本大学生就业招聘系统实行的目的初步调查和分析,提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本大学生就业招聘系统采用JAVA作为开发语言,S…

【现代控制理论】第2-5章课后题刷题笔记

文章目录 第二章:线性控制系统的状态空间描述第三章:控制系统状态空间描述的特性3.1 计算状态转移矩阵(矩阵指数函数)3.2 计算系统的时间响应(状态方程的解)3.3 判断系统的能控性和能观性,以及能…

【笔记】I/O总结王道强化视频笔记

文章目录 从中断控制器的角度来理解整个中断处理的过程复习 处理器的中断处理机制**中断驱动I/O方式** printf——从系统调用到I/O控制方式的具体实现1轮询方式下输出一个字符串(程序查询)中断驱动方式下输出一个字符串中断服务程序中断服务程序与设备驱动程序之间的关系 DMA方…

【测试】接口测试与接口自动化

壹、接口测试基础 一、接口测试概念 I、基础概念 是测试系统组件间接口的一种测试。 主要用于检测外部系统与系统间、内部子系统间的交互点;测试重点检查数据的交换、传递和控制管理过程,以及系统间的相互逻辑依赖关系。 内部接口调用相当于函数调用&am…

C语言基础(9)之指针(1)

目录 1. 指针的概念 1.1 内存 1.2 指针是什么 1.3 指针变量的大小 2. 指针类型 2.1 指针类型的意义 2.2 指针类型意义的疑问 3. 野指针 3.1 野指针的概念 3.2 野指针的成因 3.3 如何规避野指针 4. 指针运算 4.1 指针 或 - 整数 4.2 指针 - 指针 4.3 指…