首页 > 图灵资讯 > 技术篇>正文
多线程-线程池与java内存模型
2023-06-01 09:46:39
使用多线程-线程池和java内存模型线程池(思路:什么是线程池->他的基本结构和参数意义->如何使用,在使用过程中需要注意什么?->有哪些好用的工具)
- 线程池的基本愚蠢概念:首先看继承关系,然后看他的状态,它使用int的三个表示状态,如11表示可以接受任务,具体看下面的第二章
- 接下来,我们来看看他的构造函数,分析参数的含义和方法。根据他的介绍,我们来看看以下两个代码段
- executor工具中常见的线程池和功能
Executors是Java为创建线程池而提供的工具类。它包装了一些常见的线程池配置,简化了开发人员使用线程池的复杂性。该类主要提供以下线程池:newCachedThreadPool()该方法返回一个可以根据需要创建新线程的线程池,该线程池中的线程将在执行后回收,如果添加了新任务,将优先考虑空闲线程,而不是创建新线程。注意:由于线程池没有核心线程的限制,可能会创建大量的线程。当任务队列中的任务过多时,会导致OutofmemoryError异常。newFixedThreadPool(int nThreads)该方法返回固定尺寸的线程池,线程池中的线程数量始终保持不变。在任何时间点,最多同时有 nThreads 线程处于活动状态。如果向线程池提交更多任务,它们将暂时存储在任务队列中,直到有空闲线程执行。注:线程池的尺寸必须合适。如果线程池设置过小,由于任务积累,程序可能运行缓慢;如果设置过大,可能会浪费内存资源。newSingleThreadExecutor()该方法返回一个只有一个线程的线程池,所有任务按照指定的顺序在线程中执行,即每次只执行一个任务。注:线程池不会创建新的线程,如果线程异常终止,将立即创建新的线程来代替它。newScheduledThreadPool(int corePoolSize )该方法返回固定尺寸的线程池,支持定期和定期执行任务。注:如果线程执行过程中出现任何异常,则不能再用于后续任务执行。此时,线程池将创建一个新的线程来取代原始的异常线程。简而言之,使用Executors可以使线程池的配置简单方便,但需要注意线程池的大小、任务积累、异常处理等问题。注:尽量避免使用无限任务队列,否则可能会导致内存溢出。若任务过多,应考虑使用有界任务队列,或限制提交任务的数量。使用submit()方法时,应特别注意异常处理。由于Submit()方法不能直接捕获Runnable抛出的异常,因此必须从Future对象中获得抛出的异常。线程池是一种共享资源,在提交任务时,需要特别小心不要修改线程池中的共享数据。如需修改共享数据,可采用同步机制,确保线程安全。在线程池关闭时,如果有任务等待执行,可以通过调用shutdownnow()来尝试立即停止所有任务。 问题: 这些线程的队列是无限的吗?有无数的线程可以排队吗?这些线程池有自己的拒绝策略吗?是什么?简单来说 都是无界队列 上述线程池中的任务队列并非无限大,每个线程池都有一个存储等待执行的任务队列。不同类型的线程池有不同的任务队列大小:NewcachedThreadPool方法返回的线程池使用SynchronousQueue作为任务队列,这是一个无界队列,不会保存任何提交的任务,而是直接将任务交给工作线程。NewfixedThreadPol方法返回的线程池使用LinkedBlockingQueue作为任务队列。默认情况下,它是无限的,可以存储任何数量的任务。newsinglethreadexecutor方法返回的线程池使用linkedBlockingQueue作为任务队列。默认情况下,它是无限的,可以存储任何数量的任务。newscheduledthreadPol方法返回的线程池使用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,//表示核心线程数量intt maximumPoolSize,//表示最大线程long keepAliveTime,///表示空闲线程的存活时间TimeUnitt unit,///生存时间单位BlockingQueue<Runnable> workQueue,//存储线程队列大小ThreadFactory threadFactory,///线程工厂Rejectedexecutionhandler handler)////拒绝voidid的策略 execute(Runnable command);///执行方法Futurere执行方法<?> submit(Runnable task); 针对上述参数提出的问题:问题1corePolsize的数量是初始化创建还是使用菜肴创建问题2maximumpolsize的数量是否包含1corePolsize的数量问题3threadFactory的作用,例如,问题4来了如何处理新的线程 答案 corePolsize的数量是在线程池创建时创建的核心线程数量,不会因为线程池中任务数量的变化而增加或减少。maximumpolsize表示线程池中可容纳的最大线程数,包括corepolsize和可添加的非核心线程数。maximumpolsize表示线程池中可容纳的最大线程数,包括corepolsize和可添加的非核心线程数。当线程池中的任务数量超过corepolsize并等待队列满时,线程池将尝试创建非核心线程来处理任务。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 此时仍有新的任务将执行拒绝策略
ThreadPolexecutor提供了两种向线程池提交任务的方法,即execute()和submit()。execute()Execute()方法接受Runnable类型的参数,并将其提交给线程池执行。如果当前线程池中有空闲线程,则直接使用之前创建的线程来处理任务。若当前线程池中没有空闲线程,则将任务放入工作队列中,等待线程池的线程执行。例如:executor.execute(new RunnableTask());submit()Submit()方法还接受Runnable类型的参数,并返回Future类型的对象。Future对象可用于检查任务是否已完成并获得可能的结果(如果任务产生结果)。它还重载了一些支持Calable类型任务的方法。例如:Future<?> future = executor.submit(new RunnableTask());与execute方法不同,submit方法返回future对象,可以使用此对象获得任务的执行结果或状态。不同:返回值不同:execute()方法没有返回值,submit()方法将返回Future对象。调用方法不同:execute()只能接收Runnable类型的任务,submit()不仅可以接收Runnable类型的任务,还可以接收Calable类型的任务,因此submit()方法更加灵活。不同的异常处理方法:使用execute()方法提交任务时,不能捕获未捕获的异常,程序不能知道;使用submit()方法提交任务时,可以通过future对象获得未捕获的异常,然后可以根据需要进行处理。一般来说,如果您只是想将任务提交给线程池进行异步操作,您可以使用execute();如果您还想获得任务执行的返回值或检查任务是否完成,可以使用submit()方法
Java内存模型和常见问题(什么可见性)->这个问题是怎么产生的?->解决方案,解决原则)- 关于cpu 内存 IO设备之间的速度差异很大,为cpu的平衡增加了缓存,但也导致了数据可见性问题。以下是专业声明
这句话一般都是正确的。CPU、内存和IO设备之间的速度差异确实很大。为了缓解这个问题,CPU通常通过添加多级缓存和其他技术来优化系统性能,以提高访问效率。然而,由于每个缓存层都有其数据与上一层不一致的可能性,因此在多线程并发编程中会出现数据可见性问题。简单地说,当一个线程修改共享变量的值时,其他线程可能无法立即看到变量的最新值,导致意想不到的操作结果。因此,在多线程编程中,要特别注意数据的同步性和可见性,避免线程安全问题。常用的方法有synchronized关键词、volatile关键词、lock等工具,它们能保证多线程操作的原子性和可见性,有效避免数据不一致。
- 什么是可见性问题:在单核cpu上,多个线程操作同一cpu的缓存,所有线程都是可见的,这是可见性;但多核是不可见的;指令重新排列也回到可见性问题,具体原因如下
重排指令可能会导致可见性问题。现代处理器在CPU中使用乱序执行技术,以优化指令的执行效率。它将在不改变程序执行结果的情况下重新安排指令的顺序,即在执行前面的指令之前执行后续指令。这样可以避免一些指令之间依赖关系造成的等待时间,从而提高CPU的利用率和运行效率。然而,在多线程编程中,这种指令的重排可能会带来可见性问题。如果在一个线程完成共享变量的编写操作后,变量没有刷新到主内存中,那么当另一个线程读取共享变量的值时,会发现该值不是最新值,因此存在可见性问题。volatile关键字被引入Java,以解决指令重排带来的可见性问题。它确保所有指令都按照程序源代码的顺序执行,从而避免了指令重排带来的可见性问题。一般来说,在多线程编程中,如果同步机制没有得到适当的使用,包括使用锁或volatile关键字,则很容易出现指令重排的可见性问题,从而影响程序的正确运行。
- jmm是为了解决可见性问题:他的主要做法 是通过 volatile、synchronized 和 final 三个关键词,六个关键词 Happens-Before 规则,具体原因见下面, 核心是按需禁止
众所周知,可见性的原因是缓存,有序性的原因是编译优化。解决可见性和有序性最直接的方法是禁止缓存和编译优化。然而,尽管这些问题得到了解决,但我们程序的性能令人担忧。合理的解决方案应该是按需禁止缓存和编译优化。那怎样才能做到“按需禁用”呢?对于并发程序,只有程序员知道何时禁止缓存和编译优化,所谓的“按需禁止”实际上是指根据程序员的要求禁止。因此,为了解决可见性和有序性问题,程序员只需要提供缓存和编译优化的方法。Java 内存模型是一个非常复杂的规范,可以从不同的角度来解释,从我们程序员的角度来看,本质上可以理解为,Java 内存模型标准化 JVM 如何提供按需缓存和编译优化。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键词,六个关键词 Happens-Before 规则
- 问题:jmm采取了这些方案来解决可见性问题,特别是sychronizeded。 ,volatile,final, 内存屏障和storelodad:
1.sychronized:同时,只有一个线程可以进入临界区域,以避免数据竞争和并发访问 2.volatile volatile修改的所有变量,每次写作操作都会刷新到主存,读取操作从主存中获得最新值 3.final 这个关键字修改的变量是不可变的,没有数据竞争问题 4.内存屏障(memory barrier)确保指令的执行顺序不会重新排列,具体如下;内存屏障可以通过限制处理器对指令重新排序的行为来解决。内存屏障包括写屏障和读屏障,其中写屏障用于约束 store 读屏障用于限制操作的重排序行为 load 操作重排序行为。具体来说,如果是 store 在指令之前插入一个写作屏障,然后屏障将确保所有以前的屏障 store 所有指令都完成了数据写入操作,这使得在 store 其他线程无法看到指令后执行的任何指令。同样,如果是 load 操作后插入读取屏障,保证所有后续屏障 load 所有指令都必须等待屏障之前的指令 load 只有在操作完成后才能执行。同样,如果是 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表示,先读取操作,再写入内存,也可能出现可见性问题。这四种组合都有缺陷,可以通过内存屏障来解决。
- happen-before:a happenBefore b ,a 结果可见于b,如何处理,具体看这篇文章,说的话可以让人理解 https://www.cnblogs.com/niuyourou/p/12398252.html
- 什么是多线程锁,分别是什么,有什么特点?
- aqs的定义,与reentrantlock的关系
- sychronized和lock的性能更好
- lockadd 如何实现内存屏障
- jit 他和字节码引擎,模板引擎 两者之间的差异和特征
- 介绍一下 lock storeload
- 对于高并发 针对qps 解释何时使用io密集型,何时使用cpu密集型