JAVA并发

在这里插入图片描述

并发

多线程

上下文切换

在一个CPU 上,多个线程共享 CPU 时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括程序计数器、寄存器、栈指针等,以便下次继续执行该线程时能够恢复到正确的执行状态。同时,需要将切换到的线程的状态信息恢复,以便于该线程能够正确运行。

如何减少上下文切换?

  • 减少线程数:通过合理的线程池管理,避免过多线程导致的上下文切换。
  • 使用无锁并发编程:避免线程因等待锁而阻塞
  • 使用CAS算法:避免线程阻塞和唤醒
  • 使用协程:协程是用户态线程,切换不需要操作系统参与,避免了操作系统级别的上下文切换。
  • 合理使用锁:缩小同步块或同步方法的范围,减少线程等待时间

线程状态

如何创建线程

  • 继承Thread类
  • 实现Runnable接口
  • 通过Callable和FutureTask创建线程
  • 线程池

Java内存模型 (JMM)

控制多线程环境下线程如何访问共享内存,确保线程间的可见性、原子性和有序性。通过主内存和工作内存的交互机制,来保证多线程程序的正确性。

as-if-serial 原则

单线程,编译器和处理器按这个原则,就可以认为是按顺序执行的

happens-before原则

多线程

  • 如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作是可见的,即第一个操作在内存中的改变对第二个操作是可见的。
  • 所有的同步操作(如synchronized、volatile变量的读写等)都遵循happens-before原则

异常

  • try-catch中只能捕获本线程的异常,不能捕获子线程的异常
  • 每个线程是独立的,并发执行,一个线程出现异常,JVM进程不会退出

如何捕获?

  • 使用Callable和Future:通过实现Callable接口并在call方法中抛出异常,可以使用Future.get()方法来获取子线程的执行结果和异常。
  • 异常处理器接口:可以定义一个异常处理器接口,并在子线程中调用该接口的方法来处理异常。
  • CompletableFuture:CompletableFuture.supplyAsync() 来执行任务,并使用handle()方法捕获异常。

并发安全

概念:

并发环境中,多个线程同时访问共享变量,得到正确的结果。

  • 并发:一个CPU,为多个用户进程分配时间片,时间片轮转。并行:多个CPU同时进行
  • 共享变量:保存在堆、方法区
    • 成员变量:类内,方法外
      • 类变量(静态变量):方法区
      • 实例变量:堆
    • 局部变量:类内,方法内。栈
  • 正确:原子性 有序性 可见性

实现线程安全的方案

  • 单线程 redis
  • 互斥锁 Synchronized、ReentrantLock等
  • 读写分离 COW
  • 原子操作 一般依赖CAS
  • 不可变模式 string
  • 数据不共享 ThreadLocal

线程同步的方式

  • Synchronized 方法、代码块,同一时间只有一个线程执行。
  • ReentrantLock 更灵活
  • Semaphore 多个线程同时访问资源,限制线程个数
  • CountDownLatch 计数器,允许一个或多个线程等待其他线程完成操作后再继续执行。
  • CyclicBarrier 同步屏障,允许多个线程相互等待,直到到达某个公共屏障点后再一起继续执行。

Synchronized

原理

  • 底层:监视锁Monitor
    • 每个对象都有自己的Monitor
    • Monitor 结构:
      • _header:对象头(存储 Mark Word,用于锁状态)。
      • _count:锁计数器(记录重入次数)。
      • _owner:当前持有锁的线程。
      • _WaitSet:等待锁的线程集合(调用 wait() 的线程)。
      • _EntryList:竞争锁的线程集合(未获取锁的线程)。

同步方法

  • 同步方法的常量池中会有一个ACC_SYNCHRONIZED 标志,当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED
    • 如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
    • 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块

  • 使用 monitorenter和monitorexit两个指令实现。可以把执行monitorenter 指令理解为加锁,执行monitorexit理解为释放锁。
    • 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。 当计数器为0的时候,锁将被释放,其他线程便可以获得锁。

锁升级

  • 无锁:对象未被锁定。
  • 偏向锁:
    • 触发条件:首次进入sychronized块的线程
    • 锁对象会记录偏向线程 ID,后续该线程访问时直接获取锁。
  • 轻量级锁:
    • 触发条件:另一个线程尝试获取已被偏向的锁
    • 通过 CAS 操作竞争锁,失败则升级为重量级锁。
  • 重量级锁:
    • Monitor结构
    • 线程想获取锁,需要先进入等待队列

锁优化

  • 锁升级
  • 自旋锁
    • 线程在获取锁失败时,不立即阻塞,而是自旋(循环尝试)一段时间。(不放弃CPU)
    • 如果自旋期间锁被释放,线程可以直接获取锁。
  • 锁消除
    • JIT 编译器通过逃逸分析判断锁对象是否不会逃逸出当前线程(即不会被其他线程访问)。
    • 如果锁对象是线程私有的,JIT 会移除同步代码。
  • 锁粗化
    • JIT 编译器将相邻的多个同步块合并(减少锁竞争次数)。

ReentrantLock

和synchronized都是可重入锁

volatile

修饰变量。无锁

  • 可见性:变量修改后,刷新到内存
  • 有序性:通过内存屏障 禁止指令重排
  • 不能保证原子性

CAS

Compare And Swap,先比较再替换

原理

  • 包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。并发修改时,先比较A和V中取出的值是否相等,如果相等,则会把值替换成B,否则就不做任何操作。
  • 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

主要应用:实现乐观锁和锁自旋。

操作系统层面

  • 原子性:基于硬件提供的cmpxchg指令,是原子的;
  • 可见性:cmpxchg指令基于CPU缓存一致性协议

AQS

抽象队列同步器。reentrantlock等就是基于AQS

实现:

  • 一个FIFO队列 + 一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量的值修改的动作通过CAS来完成。

ThreadLocal

为每一个线程创建一份共享变量的副本,来保证各个线程之间的变量的访问和修改互相不影响

方法

  • initialValue
  • get
  • set
  • remove

实现原理

  • Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。

内存泄漏

  1. 栈上的ThreadLocal Ref引用不在使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收
    • 弱引用。那么ThreadLocal对象就可以在下次GC的时候被回收掉了。
  2. Thread对象如果一直在被使用,比如在线程池中被重复使用,那么从这条引用链就一直在,那么就会导致ThreadLocalMap无法被回收。
    • 在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。

InheritableThreadLocal:用于主子线程之间参数传递

线程池

提前创建好一批线程,保存在线程池中。当有任务需要执行时,从线程池中选一个线程来执行。

参数

  • corePoolSize: 核心线程数量。正式员工数量,常驻线程数量。
  • maximumPoolSize: 最大的线程数量。公司最多雇佣员工数量,常驻+临时线程数量。
  • workQueue: 多余任务等待队列。再多的人都处理不过来了,需要等着,在这个地方等。
  • keepAliveTime: 非核心线程空闲时间。外包人员等了多久,如果还没有活干,解雇了。
  • threadFactory: 创建线程的工厂。在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一样,在这里设置员工的属性。
  • handler: 线程池拒绝策略。当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?
    • AbortPolicy(默认):当线程池无法接受新任务时,会抛出RejectedExecutionException异常。新任务会被立即拒绝,不会加入到任务队列中,也不会执行。
    • DiscardPolicy:丢弃新的任务而且不会抛出异常。新任务提交后会被默默地丢弃,不会有任何提示或执行。一般用于日志记录、统计等不是非常关键的任务。
    • DiscardOldestPolicy:丢弃任务,但它会先尝试将任务队列中最早的任务删除,然后再尝试提交新任务。如果任务队列已满,且线程池中的线程都在工作,可能会导致一些任务被丢弃。实时性要求较高的场景。
    • CallerRunsPolicy:将任务回退给调用线程,而不会抛出异常。调用线程会尝试执行任务。降低任务提交速度,适用于任务提交者能够承受任务执行的压力,但希望有一种缓冲机制的情况。

实现

  • ThreadPoolExecutor:使用共享阻塞队列(如 LinkedBlockingQueue),所有线程从同一个队列获取任务。如果队列为空,线程会阻塞等待新任务。
  • ForkJoinPool:每个线程有自己的双端队列(Deque),优先执行自己的任务。如果自己的任务队列为空,线程会窃取其他线程的任务(从队列尾部偷取)。自动创建销毁线程,可以递归地将大任务拆分为小任务。

场景

三个线程如何顺序执行?

  • 直接调用start方法:不行
  • 使用join方法:Thread类中的join方法可以让主线程等待一个子线程完成后再继续执行。通过在T2中调用thread1.join(),在T3中调用thread2.join(),可以确保T1完成后T2执行,T2完成后T3执行。
  • 使用CountDownLatch:CountDownLatch是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。通过创建三个CountDownLatch对象,可以在每个线程执行完毕后调用countDown,而主线程则通过await方法等待。
  • 使用CyclicBarrier:CyclicBarrier用于等待所有线程到达一个公共屏障点。通过创建一个CyclicBarrier对象,并在每个线程中调用await方法,可以确保所有线程按顺序执行。
  • 使用Semaphore:Semaphore用于控制同时访问某个特定资源的线程数量。通过创建一个许可数为1的Semaphore,可以确保线程按顺序执行。
  • 使用线程池:通过创建一个单线程的线程池,可以按照任务提交的顺序执行线程,因为线程池内部使用队列存储任务。
  • 使用CompletableFuture:CompletableFuture可以用来实现异步编程,通过链式调用thenRun或thenAccept等方法,可以在前一个任务完成后执行下一个任务。
    • 底层实现:主要依赖于ForkJoinPool线程池。当创建一个CompletableFuture时,它会将任务提交到ForkJoinPool中,由池中的线程异步执行。任务完成后,会触发链式调用的下一个任务。

三个线程分别顺序打印0-100

  • synchronized + wait()/notifyAll()
    • 使用一个共享对象作为锁,并维护一个 currentThread 变量标记当前应该执行的线程。
    • 每个线程检查是否轮到自己执行,如果是则打印并唤醒下一个线程,否则等待。
  • ReentrantLock + Condition
    • 使用 ReentrantLock 和多个 Condition 实现更精细的线程唤醒控制。
    • 每个线程对应一个 Condition,轮到自己时才唤醒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值