多线程-线程池与java内存模型

发布时间 2023-05-31 15:20:19作者: 小傻孩丶儿

多线程-线程池与java内存模型

线程池的使用(思路:什么是线程池->他的基本构造以及参数含义->如何使用,使用过程中需要注意什么->有哪些好用的工具类)

  1. 线程池的基笨概念:首先看一下的继承关系,其次看他的状态,它是利用int的高三位表示状态,比如111表示能接受任务,具体看下面第二章图

  2. 接下来看他的构造函数,分析其中参数的含义,以及方法,据他介绍看下面的俩个代码段

  3. 常见的executor工具类中的线程池以及作用

    Executors是Java提供的一个工具类,用于创建线程池。它内部封装了一些常见的线程池配置,简化了开发人员使用线程池的复杂度。
    
    该类主要提供以下几种线程池:
    
    newCachedThreadPool()
    该方法返回一个可根据需要创建新线程的线程池,这个线程池中的线程在执行完成后会被回收,并且如果有新任务加入,会优先使用空闲线程执行,而不是去创建新的线程。
    
    注意事项:由于该线程池没有核心线程的限制,所以可能创建大量的线程,当任务队列中的任务过多时,会导致OutOfMemoryError异常。
    
    newFixedThreadPool(int nThreads)
    该方法返回一个固定大小的线程池,该线程池中的线程数量始终保持不变。在任意时间点,最多同时有 nThreads 个线程处于活动状态,如果有更多的任务提交到线程池中,它们会被暂时存储在任务队列中,直到有空闲的线程可以用来执行它们。
    
    注意事项:线程池的大小必须适当,如果线程池设置得太小,则可能因为任务堆积而导致程序运行缓慢;如果设置得太大,则可能会浪费内存资源。
    
    newSingleThreadExecutor()
    该方法返回一个只有一个线程的线程池,所有的任务按照指定顺序在该线程中依次执行,即每次只有一个任务被执行。
    
    注意事项:该线程池不会创建新的线程,如果线程异常终止,会立即创建一个新的线程来代替它。
    
    newScheduledThreadPool(int corePoolSize )
    该方法返回一个固定大小的线程池,支持定时和周期性执行任务。
    
    注意事项:如果线程执行期间出现任何异常,那么这个线程就不能再用于后续的任务执行。此时,线程池会创建一个新的线程来代替原来的异常线程。
    
    总之,使用Executors可以让线程池的配置变得简单方便,但需要注意线程池的大小、任务堆积、异常处理等问题。
    
    注意事项:
    
    尽量避免使用无界的任务队列,否则可能会导致内存溢出。如果任务过多,应该考虑使用有界的任务队列,或者限制提交任务的数量。
    
    在使用submit()方法时,需要格外注意异常的处理。因为submit()方法不能直接捕获Runnable抛出的异常,必须从Future对象中获取抛出的异常。
    
    线程池是一个共享的资源,需要特别小心不要在任务提交时修改线程池中的共享数据。如果必须修改共享数据,可以使用同步机制来保证线程安全。
    
    在线程池关闭时,如果还有等待执行的任务,可以通过调用shutdownNow()方法来尝试立即停止所有的任务。
        
        
      问题: 以上这些线程的队列是无限大吗?可以有无数个线程排队?这些线程池自带拒绝策略吗?是什么?简单来说 都是无界队列
        
        以上线程池中的任务队列并不是无限大的,每个线程池都有一个任务队列用于存放等待执行的任务。不同类型的线程池其任务队列大小也不同:
    
    newCachedThreadPool方法返回的线程池使用SynchronousQueue作为任务队列,该队列是一个无界队列,它不会保存任何提交的任务,而是将任务直接交给工作线程处理。
    
    newFixedThreadPool方法返回的线程池使用LinkedBlockingQueue作为任务队列,默认情况下,它是无界的,可以存放任意数量的任务。
    
    newSingleThreadExecutor方法返回的线程池使用LinkedBlockingQueue作为任务队列,默认情况下,它是无界的,可以存放任意数量的任务。
    
    newScheduledThreadPool方法返回的线程池使用DelayQueue作为任务队列,它是一个无界队列。在这个队列中,任务按照它们的延迟时间排列,从延迟最长的任务开始执行。
    
    虽然LinkedBlockingQueue和DelayQueue是无界队列,但由于线程池数量是有限的,所以实际上线程池能够同时处理的任务数量也是有限的。如果任务数量超过了线程池处理的能力,就需要使用拒绝策略来处理无法处理的任务。所有线程池默认都提供AbortPolicy拒绝策略,也可以通过构造器指定其他的拒绝策略,例如:
    
    ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadPoolExecutor.DiscardOldestPolicy());
    上述代码使用DiscardOldestPolicy拒绝策略,在任务队列满了的情况下,会将当前最早加入到队列中的任务删除掉,然后尝试重新将当前任务加入队列,如果还是无法加入则会进行拒绝处理。其他常见的拒绝策略还包括CallerRunsPolicy、DiscardPolicy等。
    
    public ThreadPoolExecutor(int corePoolSize,//表示核心线程数
    int maximumPoolSize,//表示最大线程数
    long keepAliveTime,//表示空闲线程的存活时间
    TimeUnit unit,//存活时间单位
    BlockingQueue<Runnable> workQueue,//存放线程队列大小
    ThreadFactory threadFactory,//线程工厂
    RejectedExecutionHandler handler)//拒绝策略
    void execute(Runnable command);//执行方法
    Future<?> submit(Runnable task);
     
        
    针对以上参数提出的问题:
    问题1corePoolSize的数量是初始化就创建还是用到了菜创建
    问题2maximumPoolSize这个数量中是否包含1corePoolSize的数量
    问题3threadFactory这个的作用,举个例子
    问题4来了一个新线程如何处理
     
    答案
        corePoolSize的数量是在线程池创建时就被初始化并创建的核心线程数,它不会因为线程池中任务数量的变化而增加或减少。
    
    maximumPoolSize表示线程池中可容纳的最大线程数,包括了corePoolSize和可以新增的非核心线程的数量。当线程池中任务数量超过corePoolSize,并且等待队列已满时,线程池会尝试创建非核心线程来处理任务。
    
    ThreadFactory是用来创建新的线程的工厂类。通过自定义ThreadFactory,我们可以控制线程的属性、名称、优先级等。例如:
    
    class MyThreadFactory implements ThreadFactory {
      public Thread newThread(Runnable r) {
        Thread t = new Thread(r, "MyThread");
        t.setPriority(Thread.MIN_PRIORITY);
        return t;
      }
    }
    这里自定义了一个ThreadFactory实现类MyThreadFactory,它会创建一个名为"MyThread"的线程,并将线程优先级设置为最低优先级(MIN_PRIORITY)。使用自定义的ThreadFactory创建线程池时,可以用如下方式指定:
    
    ExecutorService executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, new MyThreadFactory(), handler);
    这样新创建的线程都会遵循MyThreadFactory类中的规则进行创建。
        
       	工作方式
    线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务
    当线程数达到 核心线程数上限,这时再加入任务,新加的任务会被加入队列当中去
    前提是有界队列,任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程数目
    作为空闲线程来执行任务
    如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略
    
    详细介绍ThreadPoolExecutor提供了两个方法来向线程池中提交任务,分别是execute()和submit()方法。
    
    execute()方法
    execute()方法接受一个Runnable类型的参数并将其提交给线程池执行。如果当前线程池中存在空闲线程,则直接使用之前创建的线程处理该任务。如果当前线程池中没有空闲线程,则将任务放入到工作队列中,等待线程池的线程执行。
    
    例如:
    
    executor.execute(new RunnableTask());
    submit()方法
    submit()方法也接受一个Runnable类型的参数,并返回一个Future类型的对象。Future对象可以用来检查任务是否已经完成,以及获取可能的结果(如果任务产生结果的话)。它还重载了一些方法可以支持Callable类型的任务。
    
    例如:
    
    Future<?> future = executor.submit(new RunnableTask());
    与execute方法不同的是,submit方法返回了一个Future对象,可以用这个对象来获取任务的执行结果或者状态。
    
    区别:
    
    返回值不同:execute()方法没有返回值,submit()方法会返回一个Future对象。
    
    方法调用方式不同:execute()只能接收Runnable类型的任务,submit()既可以接收Runnable类型的任务,也可以接收Callable类型的任务,因此submit()方法更加灵活。
    
    对于异常的处理方式不同:使用execute()方法提交任务时,发生未捕获的异常无法被捕获,程序无法得知;而使用submit()方法提交任务时,发生未捕获的异常可以被Future对象获取到,之后可以根据需要进行处理。
    
    总体而言,如果只是单纯地想将任务提交给线程池来异步运行,可以使用execute();而如果还希望得到任务执行的返回值或者检查任务是否完成,可以使用submit()方法
    

    1685499672386

    1685499843466

Java内存模型与常见的问题(什么可见性->如何产生的这个问题->解决方案,解决的原理)

  1. 关于cpu 内存 io设备之间的速度差异较大,为平衡这个问题cpu加了缓存,但也导致了数据可见性问题,下面是专业的说法

    这句话大体上是正确的。CPU、内存和IO设备之间的速度差异确实很大,为了缓解这个问题,CPU通常会通过加入多级缓存以及其他一些提高访问效率的技术来优化系统性能。
    
    但是,由于每个缓存层次都有其数据与上一层存在不一致的可能性,因此在多线程并发编程中会带来数据可见性问题。简单地说,就是当一个线程修改了共享变量的值时,其他线程不一定能立即看到该变量的最新值,导致程序出现意想不到的运行结果。
    
    所以,在多线程编程中需要特别注意数据的同步和可见性问题,避免出现线程安全问题。常用的手段有synchronized关键字、volatile关键字、Lock等工具类,它们可以保证多线程操作的原子性和可见性,有效避免出现数据不一致的情况。
    
  2. 什么是可见性问题:在单核cpu上,多个线程对同一个cpu的缓存进行操作,所有线程对此都是可见的,这就是可见性;但是对于多核来说就不可见了;同时指令重排也回到可见性问题,具体原因如下

    指令重排可能会导致可见性问题。
    
    在CPU中,为了优化指令的执行效率,现代处理器都会使用乱序执行技术。它会在不改变程序执行结果的前提下,对指令的顺序进行重排,也就是先执行后续指令,再执行前面的指令。这种方式可以避免一些指令之间的依赖关系造成的等待时间,从而提高CPU的利用率和运行效率。
    
    然而,在多线程编程中,这种指令重排可能会带来可见性问题。假如一个线程完成共享变量的写操作后,该变量还没有被刷新到主内存中,这时另一个线程读取共享变量的值,会发现这个值并不是最新的值,因此产生了可见性问题。
    
    为了解决指令重排带来的可见性问题,Java中引入了volatile关键字。其保证了所有的指令都要按照程序源代码的顺序执行,从而避免了指令重排所带来的可见性问题。
    
    总的来说,在多线程编程中,如果没有适当地使用同步机制,包括使用锁或者volatile关键字等方式,就很容易出现指令重排的可见性问题,从而影响程序的正确运行。
    
  3. jmm是为了解决可见性问题的:他的主要做法 是通过 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则,具体原因看下面, 核心是按需禁止

    你已经知道,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接
    的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。合理的方案
    应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及
    编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可
    见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。Java 内存模型是个很复
    杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为,Java 内存模型
    规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、
    synchronized 和 final 三个关键字,以及六项 Happens-Before 规则
    
  4. 问题:jmm在解决可见性问题都采取了那些方案,分别介绍一下,尤其是sychronized ,volatile,final, 内存屏障和storeload:

    1.sychronized:同一时间只有一个线程可以进入临界区域操作,避免了数据竞争和并发访问问题
     
    2.volatile 凡是被volatile修饰的变量,每次写操作会刷新到主存,读操作从主存中拿最新值
        
    3.final 被这个关键字修饰的变量不可变,也就没有数据竞争问题
       
    4.内存屏障(memory barrier)保证指令执行顺序,不会重排,具体如下;内存屏障可以通过限制处理器对指令重排序的行为来解决这个问题。内存屏障包括了写屏障和读屏障,其中写屏障用于约束 store 操作的重排序行为,读屏障则用于限制 load 操作的重排序行为。
    具体来说,如果在 store 指令之前插入一个写屏障,那么该屏障将保障所有之前的 store 指令都已经完成了数据写入操作,使得在 store 指令之后执行的任何指令都无法被其它线程所看到。同理,如果在 load 操作之后插入一个读屏障,那么该屏障将保证所有之后的 load 指令都必须等待该屏障之前的 load 操作完成后才能执行。
    因此,内存屏障可以有效地解决 store-load 重排序问题,提高了程序的正确性和数据的可见性。
     
        
        
    	问题:针对解决方案4,这里内存屏障解决的问题和store load有关,此处详细说说
     store、load是计算机中的指令,store指将一个数据从CPU寄存器写入内存单元中保存;load指将一个数据从内存单元中读取到CPU寄存器中进行操作。store load ;  store store; load load ;load store是一种可能存在于多线程编程中讨论可见性问题常用的指令序列组合。
    
    具体来说,这个指令序列在多线程编程中被用来探讨不同的内存顺序模型下,由于处理器和缓存等系统的缓存,多个线程共享变量时,修改后一定时间内对其他线程是否可见的问题。
    
    其中store load表示一个线程先执行了store存储变量到指定内存地址中,并将其写回内存,然后执行了load从该内存地址中读取该变量值到CPU寄存器中。因此,在另一个线程刚刚执行完load读取该变量时,即使该变量已经被原始线程修改过,在该线程的CPU寄存器中该变量值依然是旧值,导致可见性出现问题。
    
    store store表示一个线程连续两次地存储变量到同一指定内存地址,即先后两次store操作,这个组合主要是为了考虑重排序现象。
    
    load load也表明了类似的情况,其中一个线程已经执行了第一个load操作获取变量值,然后执行了另一个load操作尚未获取该变量值时,但是另一个线程已经修改了该变量的值,同样导致可见性问题。
    
    而load store则表示先读取操作,再写入内存,也可能会产生可见性问题。以上四种组合均存在缺陷,可以采用内存屏障的方式解决这个问题。 
    
    
  5. happen-before:a happenBefore b ,a 的结果对b可见,如何如何处理,具体看这个文章,说的话能让人看懂 https://www.cnblogs.com/niuyourou/p/12398252.html

多线程总结

  1. 多线程的锁有哪些,分别都是什么,都有什么特点

  2. aqs的定义,和reentrantlock的关系

  3. sychronized与lock的性能那个更好

  4. lockadd 如何实现内存屏障

  5. jit 他和字节码引擎,模板引擎 之间的区别和彼此的特点

  6. 介绍一下 lock storeload

  7. 对于高并发 针对qps 解释何时用io密集型,什么时候用cpu密集型