Java 高级 —— 高并发

一、基础知识

1、程序、进程和线程的概念1

  • 程序:一些保存在磁盘上的指令的有序集合,通常用某种编程编写,运行于计算机系统上。程序是静态的,和电脑的普通文件一样,没有任何执行的概念。
  • 进程:进程是一个独立的可调度的任务,是一个动态概念,它是程序的一次执行过程,其中包括创建、调度和消亡;线程是系统进行资源分配和调度的基本单位,因此可以理解进程是操作系统结构的基础,是程序执行和资源管理的最小单位。
  • 线程:现成是进程的实际运行单位,是操作系统中最小单位。以个进程至少包含一个线程。是CPU调度和执行的单位。

3、程序、进程和线程之间的联系

在这里插入图片描述

  • 对程序而言:
    • 程序并不能单独执行,是静止的,只有将程序加载到内存中,系统将其分配资源后才能够执行。
    • 一个程序的执行可以包含多个进程。
  • 对进程而言:
    • 程序对一个数据集的动态执行过程,一个进程包含一个或者更多的线程,一个线程同时只能被一个进程所拥有,进程是分配资源的基本单位。
    • 进程拥有独立的内存单元,而多个线程共享内存,从而提高了应用程序的运行效率
  • 对线程而言:
    • 线程是进程内的基本调度单位,线程的换分尺度小于进程,并发性更高,线程本身不拥有系统资源,但是该线程可与同属性其他线程共享该进程所拥有的全部资源。
    • 每一个独立的线程,都有一个程序运行的入口、顺序执行序列和程序的入口。

4、进程和线程的区别

  • 线程比进程更轻量级,创建销毁更快
  • 同一个进程的多个线程共用一份资源,而多个进程之间则拥有独立的内存和资源
  • 进程是自愿分配的基本单位,现成是调度执行的基本单位
  • 进程比线程更加消耗资源

5、并发和并行的区别

  • 并发:单核CPU,多个线程,交替运行。
  • 并行:多核CPU,多个线程同时进行,使用线程池操作。

6、线程的六种状态2

在这里插入图片描述

  • NEW(新生):实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
  • RUNNABLE(运行)
    • READY(就绪状态):调用.start()方法后,线程进入就绪状态,代表你随时等待调度命令,执行程序。
    • RUNNING(运行中):被调度器调用后,程序开始运行的状态。
  • BLOCKED(阻塞):阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
  • WAITING(等待):处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
  • TIMED_WAITING(超时等待):处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  • TERMINATED(终止)
    • 线程的run()完成,或者主线程的main()完成,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。
    • 线程一旦终止,就不能复生。
    • 在一个终止的线程上调用start(),会抛出非法线程状态异常

7、如何停止一个正在运行的线程

  • 使用退出标识,使线程正常退出
  • stop方法,但已经被弃用了,不建议使用
    • stop方法天生就不安全,因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致。
  • 使用interrupt方法中断

、wait和sleep的区别

  • 所属类不同:wait来自Object类,sleep来自Thread类
  • 线程唤醒:wait使线程进入到阻塞状态,调用notify和notifyAll之后,线程会进入就绪状态,sleep使线程进入阻塞状态
  • 锁的释放:wait会释放锁,sleep不会释放锁
  • 使用范围:wait必须在同步代码块,sleep可以在任何地方

8、notify和notifyAll的区别

  • notify会随机唤醒一个线程;notifyAll会唤醒所有线程。
  • notify可能会造成死锁,因为每次唤醒的锁是随机的;notifyAll不会

二、多线程

1、JUC是什么

  • 是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让并发者进行多线程编程时减少竞争条件和死锁问题。
  • 它分为5部分:
    • tools(工具类):又叫做信号量三组工具类,包含有:
      • CountDownLatch(闭锁):

2、多线程的作用

  • 发挥多核优势
  • 防止阻塞
  • 便于建模(大模型拆分为小模型,通过多线程分别执行任务)

3、Java实现多线程的几种方式,分别的优缺点

  • 继承Thread类
    • 优点:编写简单,如果需要访问当前线程,不需要调用Thread.currentThread方法,直接使用this
    • 缺点:如果继承Thread类,就不能继承其他对象了
  • 实现Runnable接口
  • 实现Callable接口
    • 优点:只实现了接口,还可以继承或实现其他接口
    • 缺点:编程稍微复杂些,访问当前线程需要使用Thread.currentThread方法
  • 线程池创建

三、线程池3

1、什么是线程池?

  • 创建Java线程需要给线程分配堆栈内存以及初始化内存,还需要进行系统调用,频繁地创建和销毁线程会大大降低系统的运行效率,采用线程池来管理线程有以下好处:
    • 提升性能:线程池能独立负责线程的创建、维护和分配。
    • 线程管理:每个Java线程会保持一些基本的线程统计信息,对线程进行有效管理

2、JUC线程池架构

在这里插入图片描述

  1. Executor:它提供了execute()接口来执行已提交的Runnbale执行目标实例,它只有一个方法:void execute(Runnable command)
  2. ExecutorService:继承于Executor,Java异步目标任务的"执行者服务接口",对外提供异步任务的接收服务。
  3. AbstractExecutorService:抽象类,实现了ExecutorService
  4. ThreadPoolExecutor:线程池实现类,继承于AbstractExecutorService,JUC线程池的核心实现类
  5. ScheduledExecutorService:继承于ExecutorService,它是一个可以完成"延时"和"周期性"任务的调度线程池接口
  6. ScheduledThreadPoolExecutor:继承于ThreadPoolExecutor,实现了ExecutorService中延时执行和周期执行等抽象方法
  7. Executors:静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池实例对象。

3、Executors创建线程池的四种方法

  1. newSingleThreadExecutor创建单线程化线程池
    • 代码:
      ExecutorService pool = Executors.newSingleThreadExecutor();
      pool.execute(new TargetTask());
      // 或 pool.submit(new TargetTask());
      
    • 特点:
      • 单线程化的线程池中的任务时按照提交的顺序执行的;
      • 只有一个线程的线程池;
      • 池中的唯一线程的存活时间是无线的;
      • 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
    • 适用场景:
      • 任务按照提交次序,一个任务一个任务地逐个执行。
  2. newSingleThreadScheduledExecutor创建单一定时线程池
    • 代码:
      ExecutorService pool = Executors.newSingleThreadScheduledExecutor();
      pool.schedule(new TargetTask(), 首次执行时间, 执行时间单位);
      /pool.scheduleAtFixedRate(new TargetTask(), 首次执行时间, 周期性执行时间, 执行时间单位);
      
    • 特点:
      • 如果线程数没有达到"固定数量",每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
      • 线程池的大小一旦达到"固定数量"就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
      • 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)
    • 适用场景:
      • 需要任务长期执行的场景
      • CPU密集型任务
  3. newFixedThreadPool创建固定数量的线程池
    • 代码:
      ExecutorService pool = Executors.newFixedThreadPool(数量);
      pool.execute(new TargetTask());
      // 或 pool.submit(new TargetTask());
      
    • 特点:
      • 如果线程数没有达到"固定数量",每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
      • 线程池的大小一旦达到"固定数量"就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
      • 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)
    • 适用场景:
      • 需要任务长期执行的场景
      • CPU密集型任务
    • 缺点:
      • 内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理事,队列无限增大,使服务器资源迅速耗尽。
  4. newCachedThreadPool创建可缓存线程池
    • 代码:
      ExecutorService pool = Executors.newCachedThreadPool();
      pool.execute(new TargetTask());
      // 或 pool.submit(new TargetTask());
      
    • 特点:
      • 给定延迟后按照指定的时间周期性地执行任务
    • 适用场景:
      • 需要延迟执行任务、周期性执行任务的场景,例如定时任务、定时检测
  5. newScheduledThreadPool创建可调度线程池
    • 代码:
      ExecutorService pool = Executors.newScheduledThreadPool(2);
      pool.scheduleAtFixedRate(new TargetTask(), 首次执行任务的延迟时间, 周期执行时间, 时间单位);
      
    • 特点:
      • 此方法具有周期性、延时性
    • 适用场景:
      • 定时任务、周期任务

4、Executors创建线程池存在的问题

  1. 创建固定数量线程池的问题
    • 代码:
      public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    • 问题:
      • 阻塞队列无界,队列很大,很可能导致JVM出现OOM异常,即内存资源耗尽
  2. 创建单线程线程池问题
    • 代码:
       public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    • 问题:
      • 问题和固定数量线程池一样,阻塞队列无界
  3. 创建缓存线程池问题
    • 代码:
        public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    • 问题:
      • 其最大线程数数量不设上限。由于其MaximumPoolSize的值为Integer.MAX_VALUE,可以认为是无线创建线程,如果任务提交较多,就会造成大量的线程被启动,很可能造成OOM异常,甚至导致CPU线程资源耗尽。
  4. 创建可调度线程存在的问题
    • 代码:
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    	return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
    	super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    			new DelayedWorkQueue());
    }
    
    • 问题:
      • 主要问题在于线程数不设上限
  • 总结
    • newFixedThreadPool和newSingleThreadExecutor: 阻塞队列无界,会堆积大量任务导致OOM(内存耗尽)
    • newCachedThreadPool和newScheduledThreadPool: 线程数量无上界,会导致创建大量的线程,从而导致OOM
    • 建议直接使用线程池ThreadPoolExecutor的构造器

5、向线程池提交任务的两种方法

  • execute方法:Executor接口中的方法
    • void execute(Runnable command)
  • submit方法:ExecutorService接口中的方法
    • <T> Future<T> submit(Callable<T> task);
    • <T> Future<T> submit(Runnable task, T result);
    • Future<?> submit(Runnable task);
  • 两种方法的区别:
    • execute方法只能接收Runnable类型的参数,而submit方法可以接受Callable、Runnable两种类型的参数。
    • Callable类型的任务时可以返回执行结果的,而Runnable类型的任务不可以返回执行结果
    • submit方法提交任务后有返回值,而execute方法没有
    • submit方法方便异常处理

6、创建线程池的参数有哪些?4

  1. 核心线程数(CorePoolSize)
    • 线程中的核心线程数量,线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,它们也不会被销毁,除非设置了allowCoreThreadTimeOut。当正在运行的线程数小于核心线程数时,来一个任务就创建一个核心线程
  2. 最大线程数(MaximumPoolSize)
    • 一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的取创建新线程,它会有一个最大线程数的限制,这个数量由MaximumPoolSize指定。
  3. 空闲线程存活时间(KeepAliveTime)
    • 一个线程如果处于空闲状态,并且当前最大线程数量大于CorePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的空闲时间有KeepAliveTime来设定。
  4. 空闲线程存活时间单位(Unit)
    • KeepAliveTime的计量单位,也就是空闲时间的存活时间单位。
  5. 工作队列(WorkQueue)
    • 当正在运行的线程数大于或等于核心线程数时,任务来了是先进入任务队列中的。任务调度时再从队列中取出任务。常用的五种工作队列:
      • 数组行阻塞队列(ArrayBlockingQueue)
        • 基于数组的有界阻塞队列,按FIFO(先进先出策略)排序。
        • 使用一个重入锁(ReentrantLock),默认使用非公平锁,入队和出队公用一个锁,互斥。
        • 新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中数量达到corePoolSize后,再有新任务进来,则会将任务放在该队队尾,等待被调度。如果队列已经满了,则创建一个新的线程,如果线程数量已经达到MaxPoolSize,则会执行拒绝策略。
      • 链表型阻塞队列(LinkedBlockingQueue)
        • 链表型阻塞队列,基于链表的有界(近似无界)阻塞队列,默认初始化大小为Integer.MAX_VALUE(其实最大容量为Integer.MAX),按照FIFO排序。
        • 使用一个重入锁(ReentrantLock),默认使用非公平锁,入队和出队公用一个锁,互斥。
        • 由于该队列的近似无界性,当线程池中线程数量达到CorePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程知道MaxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
      • 同步移交队列(SynchronousQueue)
        • 容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
        • 一个不缓存任务的阻塞队列,生产者放入一个任务必须等消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,就会创建新线程,如果线程数量达到MaxPoolSize,就会执行拒绝策略。
      • 优先级阻塞队列(PriorityBlockingQueue)
        • 具有优先级的阻塞队列,优先级通过参数Comparator实现。
        • 在put的识货会tryGrow,要说它有界也没问题,因为界是Integer.MAX_VALUE,但其实这个队列是无界的。默认采用元素自然顺序生序排序(可以自定义Comparator)。
        • 使用一个重入锁分别控制元素的入队和出队。
      • 延时队列(DelayQueue)
        • 无界,队列中的元素有过期时间,过期元素才能被驱虎
        • 使用一个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。任务调度的时候可以使用。
  6. 线程工厂(ThreadFactory)
    • 创建一个新线程时使用的工厂,可以用来设定线程名、是否为守护线程、查看创建线程数、给线程设置是否为后台运行、设置线程优先级等。默认使用的是Executors工具类中的DefaultThreadFactory类,这个类有个缺点,创建线程的名称是自动生成的,无法自定义线程名称来区分不同的线程池,且它们都是非守护线程。可以自己实现一个ThreadFactory接口,然后把名称和是否是守护线程当做构造方法的参数传入。
    • Executors为线程池工程,用于快速创建线程池;ThreadFactory为线程工厂,用于创建线程
  7. 任务拒绝策略(Handler)
    • 当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutitonHandler接口,有四种拒绝策略:
      • DiscardPolicy(直接丢弃任务)
        • 该策略下,直接丢弃任务,什么都不做。
        • 使用场景:如果你提交的任务无关紧要,你就可以使用它。因为它就是个空实现,会悄无声息的吞噬你的任务。所以这个策略基本上被弃用了。
      • AbortPolicy(丢弃任务,抛出异常,默认)
        • 该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
        • 注意:ExecutorService接口的系列ThreadPoolExecutor因为都没有显式的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
      • CallerRunsPolicy(由调用者自己处理任务)
        • 该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,否则直接抛弃任务
        • 使用场景:一般在不允许失败的、对性能要求不高、并发量小的场景下使用。因为线程池一般情况下不会关闭,所以提交的任务一定被执行,但是由于是调用者线程是自己执行的,当多次提交任务时,就会阻塞后续任务执行,效率和性能会变慢。
      • DiscardOldestPolicy
        • 抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放到队列
        • 使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息的,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。与其适配的场景:发布消息和修改消息。消息发布出去后,还未执行的时候,新消息就过来了,这时候未执行的消息版本比新消息的版本低,就可以丢弃了。

7、线程池的任务调度流程

在这里插入图片描述

  • 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
  • 如果线程池中总的任务数量大于核心线程数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。
  • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空。
  • 核心线程数量已经用完、阻塞队列也已经满了的情况下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
  • 核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出MaximumPoolSize。如果线程池的线程总数超过MaximumPoolSize,线程池就会拒绝接受任务,当新任务过来时,会为新任务执行拒绝策略。
  • 注意:
    • 核心和最大线程数量、阻塞队列等参数如果配置的不合理,可能会造成异步任务得不到预期的并发执行,造成严重的排队等待现象。
    • 线程池的调度器创建线程的一条重要的规则是:在CorePoolSize已满后,还需要等待阻塞队列满了,才会去创建新的线程,例如:设置核心线程数为1,阻塞队列为100,有五个任务待执行,则只有1个任务可以被执行,其他4个在阻塞队列中,而不是创建新线程进行处理。

8、调度器的钩子方法

  • 钩子方法共有3种,都归属于ThreadPoolExecutor类,这三个方法都是空方法,一般会在子类重写
    • 任务执行前的钩子方法:protected void beforeExecute(Thread t, Runnable r) {}
    • 任务执行后的钩子方法:protected void afterExecute(Runnable r, Throwable t) {}
    • 线程池终止时的钩子方法:protected void terminated() {}
  • 代码:
    ExecutorService pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECOND
    		, new LinkedBlockingQueue<>(2)) {
    	@Override
    	protected void terminated() {
    		System.out.println("调度器已停止...");
    	}
    	
    	@Override
    	protected void beforeExecute(Thread t, Runnable target) {
    		System.out.println("前钩执行...");
    		super.beforeExecute(t, target);
    	}
    	
    	@Override
    	protected void afterExecute(Runnable target, Throwable t) {
    		System.out.println("后钩执行...");
    		super.afterExecute(target, t);
    	}
    }
    

9、线程池的状态及转化流程

  • 线程池的5种状态:
    • RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
    • SHUTDOWN:该状态下线程池不再接收新任务,但是会将工作队列中的任务执行完毕。
    • STOP:该状态下,线程池不会再接收新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
    • TIDYING:该状态下完成任务都已终止或完成,将会执行terminated钩子方法。
    • TERMINATED:执行完terminated钩子方法后的状态。
  • 关闭线程的方法:
    • Shutdown方法
      • 等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后线程池的转换状态为SHUTDOWN,线程池不会再接收新任务。
      public void shutdown() {
      	final ReentrantLock mainLock = this.mainLock;
      	mainLock.lock();
      	try {
      		// 鉴权
      		checkShutdownAccess();
      		// 设置线程池状态
      		advanceRunstate(SHUTDOWN);
      		// 中断空闲线程
      		interruptIdleWorkers();
      		// 钩子函数,用于清理一些资源
      		onShutdown();
      	} finally {
      		mainLock.unlock();
      	}
      	tryTerminate();
      }
      
    • ShutdownNow方法
      • 立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中剩余任务,返回的是尚未执行的任务。
      public List<Runnable> shutdownNow() {
      	List<Runnable> tasks;
      	final ReentrantLock mainLock = this.mainLock;
      	mainLock.lock();
      	try {
      		// 鉴权
      		checkShutdownAccess();
      		// 设置线程池状态
      		advanceRunstate(STOP);
      		// 中断空闲线程
      		interruptIdleWorkers();
      		// 丢弃工作队列中的剩余任务
      		tasks = drainQueue();
      	} finally {
      		mainLock.unlock();
      	}
      	tryTerminate();
      	return tasks;
      }
      
    • AwaitTermination方法
      • 等待线程池完成关闭,Shutdown与ShutdownNow方法执行之后,用户程序都不会主动等待线程池关闭完成。
      public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
      	long nanos = unit.toNanos(timeout);
      	final ReentrantLock mainLock = this.mainLock;
      	mainLock.lock();
      	try {
      		for (;;) {
      			if (runStateAtLeast(ctl.get(), TERMINATED)) {
      				return true;
      			}
      			if (nanos <= 0) {
      				return false;
      			}
      			nanos = termination.awaitNanos(nanos);
      		}
      	} finally {
      		mainLock.unlock();
      	}
      }
      
      • 在设定的时间timeout内,如果线程完成关闭,返回true,否则返回false。
      • 关闭流程:
        在这里插入图片描述
  • 5种状态之间的转换:
    在这里插入图片描述

四、锁

五、CAS5

1、什么是CAS

  • Compare And Swap(比较并交换)的缩写,是一种轻量级的同步机制,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发编程安全性。它可以在不使用锁(如synchronized、Lock)的情况下,对共享数据进行线程安全的操作。
  • CAS操作主要有三个参数:
    • 要更新的内存位置
    • 期望值
    • 新值
  • CAS操作的执行过程:
    • 首先,获取要更新的内存位置的值,记为var
    • 然后,将期望值expected(一个斯派个踢的)与var进行比较
      • 如果两者相等,则将内存位置的值var更新为新值new;
      • 如果两者不相等,则说明其他线程修改了内存位置的var,此时CAS操作失败,需要重新尝试。
  • 第二种说法:
  • CAS是Java中一种轻量级的同步机制,用于实现线程安全的操作,避免使用锁带来的性能开销。它依赖于Unsafe类提供的原子操作,并可能面临ABA问题。为解决ABA问题,Java提供了AtomicStampedReference类。此外,过度的自旋可能导致CPU空转,可以通过自适应自旋锁优化。CAS常用于线程安全计数器、无锁队列等并发场景。

2、什么是Unsafe?

  • Unsafe(安C服)是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地方法(native标识)来访问。不过尽管如此,JVM还是开了个后门,JDK中有一个Unsafe类,它提供了硬件级别的原子操作。
  • Unsafe是Java中的一个底层类,包含了很多基础操作,比如数组操作、对象操作、内存操作、CAS操作、线程(park)操作、栅栏(Fence)操作,JUC包、一些三方框架都使用Unsafe类来保证并发安全。
  • Unsafe提供了硬件级别的原子操作,如CAS原子操作。

3、硬件层面CAS又是如何保证原子性的呢?真的没加锁吗?

  • 用比较常用的x86架构的CPU来说,其实CAS操作通常使用cmpxchg(CMPX change)指令实现的。
  • 可是为什么cmpxchg指令能保证原子性呢?主要有以下几个方面的保障:
    • cmpxchg指令时一个原子指令。在CPU执行cmpxchg指令时,处理器会自动锁定总线,防止其他CPU访问共享变量,然后执行比较和交换操作,最后释放总线。
    • cmpxchg指令在执行期间,CPU会自动禁止中断。这样可以确保CAS操作的原子性,避免中断或其他干扰对操作的影响。
    • cmpxchg指令时硬件实现的,可以保证其原子性和正确性。CPU中的硬件电路确保了cmpxchg指令的正确执行,以及对共享变量的访问是原子的
  • 所以,操作系统层面,CAS还是会加锁,通过加锁的方式锁定总线,避免其他CPU访问共享变量
  • 所以,解决并发问题,归根结底还得靠锁。

4、描述下ABA问题

  • ABA问题指在CAS操作过程中,如果变量值被改为了A、B、再改回A,而CAS操作是能够成功的,这时候可能导致程序出现意外的结果
  • 在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他程序修改为另外的值,然后再被修改返回原值,此时CAS操作会认为这个值没有被修改过,导致数据不一致。

5、如何解决ABA问题?

  • 为了解决ABA问题,Java中提供了AtomicStampedReference(额淘米客-斯太目泼踢的-ruai夫润斯)类,该类通过版本号的方式来解决ABA问题。每个变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变,版本号也会发生改变,即使共享变量被改回原来的值,版本号也不同,因此CAS操作也会失败。

6、为什么会出现CPU空转?

  • 除了ABA问题,CAS操作还可能会受到自旋时间过长的影响,因为如果某个线程一直在自旋等待,会浪费CPU资源

7、如何解决CPU空转问题?

  • 为了解决上述问题,可以采用自适应自旋锁的方式,即在前几次重试时采用忙等待的方式,后面则使用阻塞等待的方式,避免浪费CPU资源。
public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>();
    private int count;

    public void lock() {
        Thread currentThread = Thread.currentThread();
        // 已经获取了锁
        if (owner.get() == currentThread) { 
            count++;
            return;
        }
        // 自旋等待获取锁
        while (!owner.compareAndSet(null, currentThread)) {
            // 自适应自旋
            if (count < 10) {
                count++;
            } else {
                // 阻塞等待
                LockSupport.park(currentThread);
            }
        }
    }

    public void unlock() {
        Thread currentThread = Thread.currentThread();
        // 当前线程持有锁
        if (owner.get() == currentThread) { 
            if (count > 0) {
                count--;
            } else {
                // 释放锁
                owner.compareAndSet(currentThread, null);
                // 唤醒其他线程
                LockSupport.unpark(currentThread);
            }
        }
    }
}
  • 这里解释一下上面代码:
  • 在设计时使用了AtomicReference来保存当前持有锁的线程对象,这样可以保证线程安全。
  • 当一个线程请求获取锁时,如果当前线程已经持有锁,则将计数器加1,否则使用CAS操作来获取锁。这样可以避免了使用synchronized关键字或者ReentrantLock等锁的实现机制。
  • 当线程获取锁失败时,使用自旋等待的方式,这样可以避免线程进入阻塞状态,避免了线程上下文切换的开销。当重试次数小于10时,使用自旋等待的方式,当重试次数大于10时,则使用阻塞等待的方式。这样可以在多线程环境下保证线程的公平性和效率。
  • 在释放锁时,如果计数器大于0,则将计数器减1,否则将锁的拥有者设为null,唤醒其他线程。这样可以确保在有多个线程持有锁的情况下,正确释放锁资源,并唤醒其他等待线程,保证线程的正确性和公平性。

8、CAS应用场景

  • CAS在多线程并发编程中被广泛应用,它通常用于实现乐观锁和无锁算法。以下是CAS的一些应用场景:
    • 线程安全计数器:由于CAS操作是原子性的,因此CAS可以用来实现一个线程安全的计数器;
    • 队列:在并发编程中,队列经常用于多线程之间的数据交换。使用CAS可以实现无锁的非阻塞队列(Lock-Free Queue);
    • 数据库并发控制:乐观锁就是通过CAS实现的,它可以在数据库并发控制中保证多个事务同时访问同一数据时的一致性;
    • 自旋锁:自旋锁是一种非阻塞锁,当线程尝试获取锁时,如果锁已经被其他线程占用,则线程不会进入休眠,而是一直在自旋等待锁的释放。自旋锁的实现可以使用CAS操作;
    • 线程池:在多线程编程中,线程池可以提高线程的使用效率。使用CAS操作可以避免对线程池的加锁,从而提高线程池的并发性能。

六、AQS

1、什么是AQS?

  • AQS是Java中用于构建各种同步器的框架。它提供了一种可扩展的高效的同步机制,可以用于构建各种类型的同步器,比如:独占锁共享锁信号量等。
  • AQS的主要作用在于提供了一种通用的机制,让
  • TODO
    在这里插入图片描述
  • AQS中对state的操作时原子的,且不能被继承。所有的同步机制的视线均依赖于对改变变量的原子操作。为了实现不同的同步机制,我们需要创建一个非共有的(nonpublic internal)扩展了AQS类的内部辅助类来实现相对应的同步逻辑。AQS并不实现任何同步接口,它提供了一些可以被具体实现类直接调用的一些原子操作方法来重写相应的同步逻辑。AQS同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock。

、摘抄文章


  1. 程序、进程、线程的概念、区别与联系 ↩︎

  2. Java线程的6 种状态 ↩︎

  3. Java线程池(超详细) ↩︎

  4. Java线程池参数 ↩︎

  5. 【并发编程】CAS到底是什么 ↩︎

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值