赞
踩
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
减少上下文切换的解决方案
多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。
- public class DeadLockDemo {
- private static String resource_a = "A";
- private static String resource_b = "B";
-
- public static void main(String[] args) {
- deadLock();
- }
-
- public static void deadLock() {
- Thread threadA = new Thread(new Runnable() {
- @Override
- public void run() {
- synchronized (resource_a) {
- System.out.println("get resource a");
- try {
- Thread.sleep(3000);
- synchronized (resource_b) {
- System.out.println("get resource b");
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- });
- Thread threadB = new Thread(new Runnable() {
- @Override
- public void run() {
- synchronized (resource_b) {
- System.out.println("get resource b");
- synchronized (resource_a) {
- System.out.println("get resource a");
- }
- }
- }
- });
- threadA.start();
- threadB.start();
-
- }
- }
在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b并等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。
如上所述,完全可以看出当前死锁的情况。
通常可以用如下方式避免死锁的情况:
内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
并发
并发 指单个cpu同时处理多个线程任务,cpu在反复切换任务线程,实际还是串行化的(串行化是指有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题);其实就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。它只是用户看起来是同时进行的,实际上不上同时进行,而是一个个的的进行(多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行)
并行
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务,例如当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行
这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。
就像上面这张图,只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。
当时用多线程访问同一个资源时,非常容易出现线程安全的问题,引用同步机制保证在多线程并发的情况下共享资源只能被一个线程所持有,从而避免了多线程冲突导致数据混乱
当两个线程(进程)相互持有对方所需要的的资源,又不主动释放,导致所有线程(进程)都无法继续前进,导致程序陷入无尽的阻塞
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
可以提高 CPU 的利用率,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
线程池是一种用于管理和调度线程的机制。它是一个线程的集合,其中包含了多个预先创建的线程,这些线程可以被重复使用来执行任务。
线程池的主要目的是通过复用线程来降低线程创建和销毁的开销,并提供对线程的管理和调度。线程池中的线程可以执行提交的任务,并在任务完成后返回线程池等待下一个任务。
通过使用线程池,可以避免频繁创建和销毁线程的开销,提高系统的性能和资源利用率。线程池可以根据系统的需求来控制线程的数量和调度方式,可以限制线程的最大数量,避免资源耗尽,也可以根据任务的优先级和类型来调整线程的执行顺序。
线程池通常由线程池管理器、工作线程和任务队列组成。线程池管理器负责创建和管理线程池,工作线程负责执行任务,任务队列用于存储等待执行的任务。线程池管理器根据任务的数量和线程池的配置来决定是否创建新线程、执行任务或将任务放入队列中。
1. 降低资源消耗:线程池可以重复利用已创建的线程,避免频繁创建和销毁线程的开销。通过控制线程的数量,可以有效降低系统资源的消耗,提高系统的性能和稳定性。
2. 提高响应速度:线程池可以并发执行多个任务,从而提高系统的响应速度。当有新任务到达时,可以直接使用线程池中的空闲线程来执行任务,避免了线程创建的开销和等待时间。
3. 提供线程管理和调度:线程池提供了对线程的管理和调度。可以通过配置线程池的参数来控制线程的数量、调度方式以及任务的处理能力。可以根据系统的需求来灵活地调整线程池的配置,以优化系统的性能和资源利用率。
4. 提供任务队列:线程池中的任务队列可以存储等待执行的任务。当线程池中的线程已满时,新的任务可以暂时存储在任务队列中,等待线程空闲时执行。通过合理配置任务队列的容量,可以控制任务的排队等待时间,避免任务丢失或系统资源耗尽。
5. 提供线程安全性:线程池中的线程是经过封装和管理的,可以提供线程的安全性。线程池内部会处理线程的创建、启动、执行和销毁等操作,避免了手动管理线程的复杂性和潜在的线程安全问题。
线程池的创建参数主要包括以下几个:
1. corePoolSize(核心线程数):线程池中保持活动状态的线程数,即线程池的基本大小。即使线程处于空闲状态,核心线程也不会被回收。当有新任务提交时,如果核心线程数未达到上限,线程池会优先创建核心线程来执行任务。
2. maximumPoolSize(最大线程数):线程池允许创建的最大线程数。当任务队列已满且核心线程数未达到上限时,线程池会创建新的线程来执行任务,直到线程数达到最大线程数。超过最大线程数的任务将根据拒绝策略进行处理。
3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。如果线程在空闲时间内没有执行任务,则会被回收,直到线程数等于核心线程数。
4. unit(时间单位):用于指定线程空闲时间的单位,例如毫秒、秒、分钟等。
5. workQueue(任务队列):用于存储等待执行的任务的队列。线程池会按照先进先出的顺序执行任务。任务队列可以是有界队列(如`ArrayBlockingQueue`)或无界队列(如`LinkedBlockingQueue`)。
6. threadFactory(线程工厂):用于创建新线程的工厂类。可以自定义线程的名称、优先级、线程组等属性。
7. handler(拒绝策略):当任务无法被接受时的处理策略。可以选择使用默认的拒绝策略(如抛出异常、丢弃任务等),或自定义拒绝策略。
这些参数可以通过`ThreadPoolExecutor`的构造方法来指定,或通过`Executors`工具类提供的静态方法来创建线程池。根据具体的业务需求和系统资源情况,合理配置这些参数可以提高线程池的性能和资源利用率。
Java中常用的线程池有以下几种:
1. FixedThreadPool(固定大小线程池):该线程池维护固定数量的线程,适用于执行长期的任务。当所有线程都处于活动状态时,其他任务需要等待。
- ExecutorService executor = Executors.newFixedThreadPool(5);
- executor.execute(new MyTask());
- executor.shutdown();
2. CachedThreadPool(缓存线程池):如果没有线程可用时,该线程池根据需要创建新线程。适用于执行大量短期任务的场景。
- ExecutorService executor = Executors.newCachedThreadPool();
- executor.execute(new MyTask());
- executor.shutdown();
3. SingleThreadExecutor(单线程线程池):该线程池只有一个线程在执行任务,保证任务按照顺序执行。适用于需要顺序执行任务的场景。
-
- ExecutorService executor = Executors.newSingleThreadExecutor();
- executor.execute(new MyTask());
- executor.shutdown();
4. ScheduledThreadPool(定时任务线程池):该线程池用于执行定时任务或周期性任务。
- ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
- executor.schedule(new MyTask(), 1, TimeUnit.SECONDS);
- executor.shutdown();
以上是使用`Executors`工具类创建线程池的简单示例。在实际应用中,可以根据具体需求来选择合适的线程池类型,并根据需要配置线程池的参数,如核心线程数、最大线程数、任务队列容量等,以满足系统的需求。
线程池的实现原理如下:
1. 当一个任务被提交到线程池时,线程池会首先检查核心线程池中是否有空闲线程。如果有,空闲线程会立即执行该任务。如果没有空闲线程,则进入下一步。
2. 线程池会将任务添加到任务队列中。任务队列可以是有界队列或无界队列,用于存储等待执行的任务,新创建的线程数量不会超过线程池的最大线程数。
3.如果任务队列已满且线程池中的线程数未达到最大线程数,线程池会创建新的线程来执行任务。
4. 如果任务队列已满且线程池中的线程数已达到最大线程数,线程池会根据指定的拒绝策略来处理无法接受的任务。拒绝策略可以是抛出异常、丢弃任务、丢弃队列中最早的任务或由提交任务的线程来执行任务。
5. 当一个线程完成任务后,它会从任务队列中获取下一个任务来执行。这个过程会一直进行,直到线程池被关闭或发生异常。
线程池的实现通常基于`ThreadPoolExecutor`类,该类提供了对线程池的管理和调度。它维护一个线程池的状态,包括线程池的大小、线程的状态、任务队列等,并提供了线程池的执行、关闭、拒绝策略等功能。
通过合理配置线程池的参数,可以控制线程池中线程的数量、任务的调度方式以及任务的处理能力,以提高系统的性能和资源利用率。
需要注意的是,线程池的实现可能会因具体的线程池类型和配置参数而有所不同,但以上是线程池的一般实现原理。
当线程池中正在运行的线程已经达到了指定的最大线程数量maximumPoolSize且线程池的阻塞队列也已经满了时,向线程池提交任务将触发拒绝处理逻辑。提供了四种拒绝策略,它们分别是AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy和DiscardPolicy
1. Abort Policy(默认策略):当任务无法被接受时,直接抛出`RejectedExecutionException`异常,阻止任务的提交。
2. Discard Policy:当任务无法被接受时,直接丢弃该任务,不做任何处理。
3. Discard Oldest Policy:当任务无法被接受时,丢弃队列中最早的一个任务,并尝试再次提交任务。
4. Caller Runs Policy:当任务无法被接受时,由提交任务的线程自己执行该任务。也就是说,任务由调用`execute()`方法的线程来执行,这样可以降低任务提交速度,但可能会影响调用线程的性能。
除了以上内置的拒绝策略,还可以自定义拒绝策略。自定义拒绝策略需要实现`RejectedExecutionHandler`接口,并重写`rejectedExecution()`方法。在该方法中,可以根据具体需求实现自定义的拒绝逻辑,例如将任务放入队列中、记录日志、抛出异常等。
在创建线程池时,可以通过`ThreadPoolExecutor`的构造方法或`Executors`工具类提供的静态方法来指定拒绝策略。例如:
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- corePoolSize,
- maximumPoolSize,
- keepAliveTime,
- TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<>(queueCapacity),
- new MyRejectedExecutionHandler()
- );
其中,`MyRejectedExecutionHandler`是自定义的拒绝策略类。
选择合适的拒绝策略取决于具体的业务需求和场景。需要根据任务的重要性、系统的负载情况、任务的处理能力等因素来选择适合的拒绝策略,以保证线程池的稳定运行和任务的正常处理。
当线程池的任务队列已满时,如果继续提交任务,会发生以下情况:
1. 如果线程池是有界队列(例如`ArrayBlockingQueue`),则尝试将任务添加到队列中,如果线程数小于最大线程数(例如通过`ThreadPoolExecutor`的`maximumPoolSize`参数设置)并且任务队列已满,此时会创建新的线程来执行任务,直到达到最大线程数。如果已达到最大线程数,且队列已满,则任务无法提交,并抛出`RejectedExecutionException`异常。
2. 如果线程池是无界队列(例如`LinkedBlockingQueue`),则任务会被成功添加到队列中,直到内存耗尽为止。这意味着队列会持续增长,可能导致应用程序的内存占用过高,甚至引发内存溢出异常。因此,使用无界队列时需要特别注意内存的使用情况。
在面对线程池队列已满的情况下,可以采取以下策略:
- 调整线程池的参数:可以增加线程池的核心线程数、最大线程数或队列容量,以适应更多的任务提交。
- 使用有界队列:通过使用有界队列,可以限制任务的提交速度,避免无限制的任务堆积。
- 使用拒绝策略:可以自定义拒绝策略,例如丢弃最早的任务、丢弃当前任务、抛出异常等。可以通过`RejectedExecutionHandler`接口来实现自定义的拒绝策略。
综上所述,当线程池队列已满时,需要根据具体情况采取相应的策略来处理任务的提交,以保证线程池的稳定运行和任务的正常执行。
线程池在执行任务的过程中,可以处于以下几种状态:
1. Running(运行状态):线程池处于正常运行状态,可以接受新的任务并执行。
2. Shutdown(关闭状态):线程池不再接受新的任务,但会继续执行已提交的任务,直到所有任务执行完成。
3. Stop(停止状态):线程池不再接受新的任务,并且会中断正在执行的任务,尽快停止线程池的运行。
4. Tidying(整理状态):线程池中的任务已经全部执行完成,正在进行线程池的资源清理工作。
5. Terminated(终止状态):线程池已经完全终止,不再执行任何任务。
这些状态可以通过线程池的`isShutdown()`和`isTerminated()`方法来判断。`isShutdown()`方法用于判断线程池是否处于关闭状态,`isTerminated()`方法用于判断线程池是否已经终止。
线程池的状态转换通常是由线程池自身的执行过程决定的,也可以通过调用线程池的`shutdown()`、`shutdownNow()`和`awaitTermination()`等方法来显式地改变线程池的状态。
需要注意的是,线程池的状态是线程安全的,可以在多线程环境下进行访问和修改。在使用线程池时,可以根据线程池的状态进行相应的操作和控制,以确保线程池的正常运行和任务的正确执行。
Executor线程池框架是Java中用于管理和执行任务的线程管理机制。它提供了一种可重用的线程池,用于执行提交的任务,并提供了一些管理和控制线程池的方法。
Executor线程池框架的核心类是`ThreadPoolExecutor`,它实现了`ExecutorService`接口,提供了丰富的线程池管理和任务执行功能。此外,Java还提供了`Executors`工具类,用于快速创建不同类型的线程池,如固定大小线程池、缓存线程池、单线程池等。
通过使用Executor线程池框架,可以更好地管理和控制线程的使用,提高应用程序的性能、可伸缩性和可靠性。同时,它也提供了一些高级功能,如定时任务执行、任务结果获取等,使任务的编写和管理更加方便和灵活。
Executor框架是Java中用于管理和执行任务的高级线程管理机制。它的结构如下:
1. 任务(Task):任务是需要执行的具体操作,通常实现了`Runnable`接口或`Callable`接口。任务可以是简单的一次性操作,也可以是需要周期性执行的定时任务。
2. 任务提交器(Task Submitter):任务提交器负责将任务提交给线程池执行。它可以是应用程序的其他组件,也可以是框架提供的工具类,如`ExecutorService`的`submit()`方法。
3. 线程池(Thread Pool):线程池是Executor框架的核心部分,负责管理和执行任务。它维护了一组工作线程,这些线程可以重复使用来执行提交的任务。线程池可以根据配置的参数控制线程的数量、创建和销毁线程,以及管理任务队列等。
4. 任务队列(Task Queue):任务队列用于存储待执行的任务。当线程池中的工作线程空闲时,它们会从任务队列中获取任务并执行。任务队列可以是有界队列或无界队列,根据具体需求选择合适的队列实现。
5. 工作线程(Worker Threads):工作线程是线程池中实际执行任务的线程。线程池中维护了一组工作线程,它们不断地从任务队列中获取任务并执行。执行完任务后,工作线程可以继续等待新的任务或被销毁。
通过这种结构,Executor框架实现了任务提交与执行的解耦,提供了一种高效、可控的线程管理机制。它可以根据任务的数量和类型,自动调整线程池的大小和资源分配,提高应用程序的性能和响应能力。同时,Executor框架还提供了丰富的任务执行和管理方法,如异步获取任务结果、取消任务、定时任务执行等,使任务的编写和管理更加灵活和方便。
骚戴理解:Executor框架实现了任务提交与执行的解耦这句话我是这样理解的,这个框架里面有Executor和ExecutorService,Executor只负责执行任务,ExecutorService是负责提交任务,所以才说是解耦合
Executor框架的成员及其关系可以用一下的关系图表示
Executor框架是Java中用于管理和执行任务的高级线程管理机制。它包含以下几个主要成员:
1. Executor接口:定义了执行任务的基本方法`execute(Runnable command)`,是Executor框架的核心接口。
2. ExecutorService接口:继承自Executor接口,提供了更丰富的任务执行和管理功能。它扩展了Executor接口,添加了一些方法,如提交任务、获取Future对象等。
3. ThreadPoolExecutor类:是ExecutorService接口的一个实现类,是线程池的核心实现。该类提供了对线程池的管理和调度。它维护一个线程池的状态,包括线程池的大小、线程的状态、任务队列等,并提供了线程池的执行、关闭、拒绝策略等功能。
4. ScheduledExecutorService接口:继承自ExecutorService接口,提供了定时任务执行的功能。它可以在指定的延迟时间后执行任务,或者按照固定的时间间隔周期性地执行任务。
5. Executors类:是一个工具类,提供了一些静态方法来创建不同类型的线程池。它封装了线程池的创建和配置过程,简化了线程池的使用。
这些成员之间的关系如下:
- Executor接口是ExecutorService接口的父接口,定义了执行任务的基本方法。
- ExecutorService接口继承自Executor接口,并添加了一些任务管理和执行的方法。
- ThreadPoolExecutor类是ExecutorService接口的一个实现类,是线程池的核心实现。
- ScheduledExecutorService接口继承自ExecutorService接口,并添加了定时任务执行的功能。
- Executors类是一个工具类,提供了创建不同类型线程池的静态方法,内部使用了ThreadPoolExecutor和ScheduledThreadPoolExecutor来创建线程池。
这些成员相互配合,提供了一个强大而灵活的线程管理框架,可以满足不同场景下的任务执行需求。
Executor框架的使用示意图
1. 简化线程管理:Executor 线程池框架提供了高级的线程管理功能,可以帮助开发人员更方便地管理和控制线程。通过使用线程池,可以避免手动创建和管理线程的复杂性,减少了线程的创建和销毁开销,提高了线程的复用性。
2. 提高系统性能:线程池可以控制并发线程的数量,避免线程过多导致系统资源耗尽和性能下降的问题。通过合理设置线程池的大小,可以有效地利用系统资源,提高系统的吞吐量和响应速度。
3. 提供线程的生命周期管理:Executor 线程池框架提供了线程的生命周期管理功能,包括线程的创建、运行、暂停、恢复和销毁等操作。开发人员可以通过线程池框架来管理线程的状态和行为,使线程的管理更加灵活和可控。
4. 支持任务调度和异步执行:Executor 线程池框架支持任务的调度和异步执行。可以将任务提交给线程池,线程池会根据配置的调度策略和线程池的状态来调度任务的执行。通过异步执行任务,可以提高系统的并发性和吞吐量,提升用户体验。
5. 提供线程安全性:线程池框架提供了线程安全的执行环境,可以保证任务的执行是线程安全的。多个任务可以并发地在不同的线程中执行,而不需要开发人员手动处理线程同步和互斥的问题。
综上所述,使用 Executor 线程池框架可以简化线程管理、提高系统性能、提供线程的生命周期管理、支持任务调度和异步执行,并提供线程安全性。这些优点使得 Executor 线程池框架成为开发多线程应用程序的首选工具。
在Java中,Executor
和Executors
都是用于管理和执行线程的工具类,但它们有一些区别。
Executor
是一个接口,定义了执行任务的基本方法execute(Runnable command)
。它提供了一种将任务提交给线程执行的方式,但不提供具体的线程管理和控制功能。使用Executor
可以将任务的提交和执行解耦,从而更好地控制线程的使用和资源管理。
Executors
是一个工具类,提供了一些静态方法来创建不同类型的线程池。它是Executor
的实现类,通过封装和预定义一些线程池的创建方式,简化了线程池的使用。Executors
提供了一些常用的线程池创建方法,如newFixedThreadPool()
、newCachedThreadPool()
、newSingleThreadExecutor()
等,可以根据需求选择合适的线程池类型。
总结来说,Executor
是一个接口,定义了执行任务的基本方法,而Executors
是一个工具类,提供了创建和管理线程池的静态方法。通过使用Executors
提供的方法,可以方便地创建不同类型的线程池,而不需要手动配置和管理线程池的细节。
在线程池中,`submit()`方法和`execute()`方法都可以用于提交任务,但它们在返回结果和异常处理上有一些区别。
1. 返回结果:`submit()`方法可以接收并返回`Future`对象,该对象可以用于获取任务的执行结果。通过`Future`对象,可以异步获取任务的执行结果,也可以通过`get()`方法阻塞等待任务的执行结果。而`execute()`方法没有返回结果,无法获取任务的执行结果。
2. 异常处理:`submit()`方法可以捕获任务执行过程中抛出的异常,并将异常封装到`Future`对象中。通过`Future`对象的`get()`方法获取任务的执行结果时,如果任务抛出了异常,`get()`方法将会抛出`ExecutionException`,并将原始异常作为其`cause`。而`execute()`方法无法捕获任务执行过程中的异常,因此无法直接处理任务的异常。
综上所述,`submit()`方法比`execute()`方法更加灵活和功能丰富。它可以通过`Future`对象获取任务的执行结果,也可以捕获任务执行过程中的异常。如果需要获取任务的执行结果或处理任务的异常,建议使用`submit()`方法。而如果只是简单地提交任务而不关心任务的执行结果和异常处理,可以使用`execute()`方法。
线程组是一种用于组织和管理多个线程的机制。它可以将一组相关的线程放在一个组中,并对这些线程进行集中控制和管理。线程组可以方便地对线程进行批量操作,比如同时启动、停止、暂停、恢复等。
线程组可以提供更好的可读性和可维护性,特别是在应对复杂的多线程应用程序时。通过将相关的线程组织在一起,我们可以更容易地理解和管理这些线程。此外,线程组还可以设置一些属性,如优先级、守护线程等,以便更好地控制线程的行为。
需要注意的是,线程组并不是必需的,可以根据具体情况来决定是否使用线程组。在某些情况下,简单的线程管理方式可能更加适用。
线程组和线程池是两个不同的概念,虽然它们都与线程相关,但有着不同的作用和功能。
线程组(Thread Group)是用于组织和管理多个线程的机制。它可以将一组相关的线程放在一个组中,并对这些线程进行集中控制和管理。线程组可以方便地对线程进行批量操作,比如同时启动、停止、暂停、恢复等。线程组提供了一种逻辑上的组织方式,用于更好地理解和管理线程。
而线程池(Thread Pool)是一种用于管理和复用线程的机制。它维护了一组预先创建的线程,这些线程可以被重复使用来执行多个任务。线程池可以有效地管理线程的生命周期,避免了频繁创建和销毁线程的开销。通过控制线程池的大小和配置,可以更好地管理系统资源,提高应用程序的性能和响应速度。
在一些简单的多线程应用中,线程组可能提供了一种方便的组织和管理方式。但在复杂的多线程应用中,可能需要更灵活和高级的线程管理机制来满足需求。
最原始的创建线程池的方式,它包含了 7 个参数可供设置。
-
- public static void myThreadPoolExecutor() {
- // 创建线程池
- ThreadPoolExecutor threadPool =
- new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
- // 执行任务
- for (int i = 0; i < 10; i++) {
- final int index = i;
- threadPool.execute(() -> {
- System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
- }
- }
-
-
执行结果如下:
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- }
7 个参数代表的含义如下:
参数 1:corePoolSize
核心线程数,线程池中始终存活的线程数。
参数 2:maximumPoolSize
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
参数 3:keepAliveTime
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
参数 4:unit
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
参数 5:workQueue
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
参数 6:threadFactory
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
参数 7:handler
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
默认策略为 AbortPolicy。
- public static void main(String[] args) {
- // 任务的具体方法
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- System.out.println("当前任务被执行,执行时间:" + new Date() +
- " 执行线程:" + Thread.currentThread().getName());
- try {
- // 等待 1s
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- };
- // 创建线程,线程的任务队列的长度为 1
- ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
- 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
- new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- // 执行自定义拒绝策略的相关操作
- System.out.println("我是自定义拒绝策略~");
- }
- });
- // 添加并执行 4 个任务
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- threadPool.execute(runnable);
- }
最推荐使用的是 ThreadPoolExecutor 的方式进行线程池的创建,ThreadPoolExecutor 最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor 当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略
是的,创建线程池可以使用 Java 提供的 `ThreadPoolExecutor` 类或 `Executors` 工具类。
1. 使用 `ThreadPoolExecutor` 类创建线程池:
- int corePoolSize = 5; // 核心线程数
- int maxPoolSize = 10; // 最大线程数
- long keepAliveTime = 60; // 线程空闲时间(单位:秒)
- BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 任务队列
- ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
- RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
-
- ExecutorService executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, threadFactory, handler);
2. 使用 `Executors` 工具类创建线程池:
- int corePoolSize = 5; // 核心线程数
- int maxPoolSize = 10; // 最大线程数
-
- ExecutorService executor = Executors.newFixedThreadPool(corePoolSize); // 创建固定大小的线程池
- // 或
- ExecutorService executor = Executors.newCachedThreadPool(); // 创建可缓存线程池
- // 或
- ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建单线程池
- // 或
- ExecutorService executor = Executors.newScheduledThreadPool(corePoolSize); // 创建定时任务线程池
以上代码中,`ExecutorService` 是线程池的接口,可以用于提交任务和管理线程池。根据具体的需求,可以选择不同的线程池类型和参数配置来创建线程池。
需要注意的是,在程序结束时,应使用 `executor.shutdown()` 方法来关闭线程池,释放线程资源。
使用`Executors`工具类创建线程池是一种简单方便的方式,但是它有一些缺点,因此在一些情况下不推荐使用。
1. 隐藏了线程池的配置参数:`Executors`提供的方法有固定线程数、缓存线程数、单线程等,但是它们都使用了默认的配置参数。这意味着你不能灵活地配置线程池的核心线程数、最大线程数、任务队列大小等关键参数,可能会导致线程池在高负载情况下无法正常工作。
2. 使用无界队列:`Executors.newCachedThreadPool()`和`Executors.newFixedThreadPool()`方法使用了无界队列`LinkedBlockingQueue`,这意味着任务队列可以无限增长,可能会导致内存溢出。在高负载情况下,如果任务提交速度大于线程处理速度,会导致任务队列无限增长,最终导致系统资源耗尽。
3. 没有合适的拒绝策略:`Executors`提供的方法没有提供自定义拒绝策略的选项。当任务队列已满且线程池中的线程数达到最大线程数时,新提交的任务将被默认的拒绝策略抛弃,而没有给出任何警告或处理方式。
相比之下,使用`ThreadPoolExecutor`类可以更灵活地配置线程池的参数,包括核心线程数、最大线程数、任务队列大小、拒绝策略等。这样可以根据实际需求进行调整和优化,以满足系统的需求,并提供更好的控制和可靠性。
因此,尽管`Executors`提供了一种简单的方式来创建线程池,但在一些情况下,使用`ThreadPoolExecutor`可以更好地满足需求。
Executors
和 ThreadPoolExecutor
都可以用来创建线程池,但它们在创建线程池的方式和灵活性上有一些区别。
Executors
工具类创建线程池:
Executors
提供了一些静态方法来创建不同类型的线程池,如 newFixedThreadPool()
、newCachedThreadPool()
、newSingleThreadExecutor()
和 newScheduledThreadPool()
。newCachedThreadPool()
使用了无界的任务队列,可能导致内存溢出问题。ThreadPoolExecutor
类创建线程池:
ThreadPoolExecutor
是线程池的具体实现类,可以直接使用它来创建线程池。总的来说,Executors
工具类提供了一种简单快捷的方式来创建线程池,适用于一些简单的场景,但对于复杂的需求可能不够灵活。而 ThreadPoolExecutor
类则提供了更多的参数和配置选项,可以更精确地控制线程池的行为,适用于更复杂的场景。在实际使用中,需要根据具体需求选择合适的方式来创建线程池。
Executors类和ThreadPoolExecutor都是util.concurrent并发包下面的类, Executos下面的newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newCachedThreadPool底层的实现都是用的ThreadPoolExecutor实现的,所以ThreadPoolExecutor更加灵活。
`ScheduledThreadPoolExecutor` 是 `ThreadPoolExecutor` 的子类,它是一种定时任务线程池,可以用于执行定时任务和周期性任务。
`ScheduledThreadPoolExecutor` 提供了以下主要特性:
1. 定时执行任务:可以使用 `schedule()` 方法来安排任务在指定的延迟时间后执行,或者使用 `scheduleAtFixedRate()` 或 `scheduleWithFixedDelay()` 方法来安排任务以固定的速率或固定的延迟周期性执行。
2. 核心线程数控制:与普通的线程池不同,`ScheduledThreadPoolExecutor` 的核心线程数可以动态调整。当任务数量不足以占用所有核心线程时,空闲的线程可能会被回收,从而减少资源消耗。
3. 任务取消:可以使用 `cancel()` 方法取消尚未执行的任务。被取消的任务将不会被执行。
4. 异常处理:`ScheduledThreadPoolExecutor` 可以通过重写 `afterExecute()` 方法来处理任务执行过程中抛出的异常。
5. 可监控性:`ScheduledThreadPoolExecutor` 提供了一些方法来获取线程池的状态信息,如已完成任务数、活动线程数、任务队列大小等。
使用 `ScheduledThreadPoolExecutor` 可以方便地执行定时任务和周期性任务,例如定时的数据备份、定时的任务调度等。通过合理配置核心线程数、任务延迟和周期,可以满足不同的定时任务需求。
需要注意的是,使用完 `ScheduledThreadPoolExecutor` 后,应调用 `shutdown()` 方法来关闭线程池,释放资源。
`FutureTask` 是 Java 提供的一个实现了 `RunnableFuture` 接口的类,它可以用于包装一个 `Callable` 或 `Runnable` 对象,并提供了获取任务执行结果的能力。
`FutureTask` 具有以下主要特点:
1. 封装任务:`FutureTask` 可以将一个 `Callable` 或 `Runnable` 对象封装起来,使其具备异步执行和获取结果的能力。
2. 异步执行:通过将 `FutureTask` 提交给线程池执行,任务可以在后台异步执行,不会阻塞主线程。
3. 获取结果:可以通过 `get()` 方法获取任务的执行结果。如果任务尚未完成,`get()` 方法将会阻塞等待任务完成并返回结果。如果任务抛出异常,`get()` 方法将会抛出 `ExecutionException`。
4. 取消任务:可以使用 `cancel()` 方法取消任务的执行。被取消的任务将不会被执行,并且 `get()` 方法将会返回取消状态。
5. 状态判断:可以使用 `isDone()` 方法判断任务是否已经完成,使用 `isCancelled()` 方法判断任务是否已经被取消。
`FutureTask` 在多线程编程中非常有用,特别是当需要获取任务的执行结果或取消任务时。它可以与线程池配合使用,将任务提交给线程池执行,并通过 `FutureTask` 获取任务的执行结果或取消任务的执行。
需要注意的是,`FutureTask` 只能执行一次,执行完成后不能重新执行。如果需要多次执行任务,可以使用 `ExecutorService` 提交多个 `FutureTask` 实例。
1. 异步执行:`FutureTask` 可以将任务提交给线程池异步执行,不会阻塞主线程。这样可以提高程序的并发性和响应性,充分利用系统资源。
2. 获取任务结果:`FutureTask` 提供了 `get()` 方法来获取任务的执行结果。通过 `get()` 方法可以等待任务完成并获取其结果,或者在任务完成前进行等待超时处理。这对于需要等待任务执行完成后才能继续后续操作的情况非常有用。
3. 取消任务:`FutureTask` 提供了 `cancel()` 方法来取消任务的执行。可以在任务执行过程中或者尚未开始执行时取消任务。这对于需要在一定条件下终止任务执行的情况非常有用。
4. 状态判断:`FutureTask` 提供了一些方法来获取任务的状态信息,如是否已完成、是否已取消等。可以根据任务的状态进行相应的处理,以便更好地控制任务的执行。
5. 异常处理:`FutureTask` 可以捕获任务执行过程中抛出的异常,并通过 `get()` 方法抛出 `ExecutionException`。这样可以更好地处理任务执行过程中的异常情况。
6. 可复用性:`FutureTask` 可以在多个线程中共享使用,多个线程可以共同等待同一个 `FutureTask` 的执行结果。这样可以避免重复执行相同的任务,提高效率。
综上所述,使用 `FutureTask` 可以更方便地处理异步任务的执行和结果获取,提供了更好的控制和灵活性。它在多线程编程中非常有用,特别是需要获取任务结果或取消任务执行的情况下。
异步计算是一种计算模式,它允许程序在执行某个任务时,不必等待该任务完成,而是可以继续执行其他任务或操作。在异步计算中,任务的执行和结果的获取是分离的,任务的执行可以在后台进行,而主线程可以继续执行其他操作。
传统的同步计算模式中,程序会按照顺序执行任务,当一个任务执行完毕后,才会执行下一个任务。这种方式可能会导致程序在等待某些耗时的操作完成时出现阻塞,从而影响整个程序的性能和响应性。
而异步计算模式中,程序可以提交任务并立即返回,而不需要等待任务的完成。任务的执行可以在后台线程或线程池中进行,不会阻塞主线程。当任务执行完成后,可以通过回调、轮询或者使用 `Future` 或 `Promise` 等机制获取任务的结果。
异步计算适用于那些可能耗时的操作,如网络请求、文件读写、数据库查询等。通过将这些操作放在后台线程中执行,可以提高程序的并发性和响应性,充分利用系统资源。
异步计算的优势在于能够提高程序的性能和响应性,避免阻塞主线程,提高用户体验。但同时也需要注意合理管理异步任务的执行和结果获取,避免出现线程安全和资源竞争等问题。
创建 `FutureTask` 实例的两种方式如下:
1. 通过 `Callable` 对象创建 `FutureTask`:
- Callable<Integer> callable = new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- // 执行任务逻辑,返回结果
- return 42;
- }
- };
-
- FutureTask<Integer> futureTask = new FutureTask<>(callable);
在这种方式下,我们首先创建一个实现了 `Callable` 接口的匿名内部类对象,并实现其 `call()` 方法来定义任务的逻辑。然后,将该 `Callable` 对象传入 `FutureTask` 的构造函数中创建 `FutureTask` 实例。
2. 通过 `Runnable` 对象和结果值创建 `FutureTask`:
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 执行任务逻辑
- }
- };
-
- Integer result = 42;
- FutureTask<Integer> futureTask = new FutureTask<>(runnable, result);
在这种方式下,我们首先创建一个实现了 `Runnable` 接口的匿名内部类对象,并实现其 `run()` 方法来定义任务的逻辑。然后,将该 `Runnable` 对象和结果值传入 `FutureTask` 的构造函数中创建 `FutureTask` 实例。
无论是哪种方式,创建 `FutureTask` 实例后,可以将其提交给线程池执行,或者在需要的时候调用 `run()` 方法手动执行任务。通过 `get()` 方法可以获取任务的执行结果,或者使用其他方法来取消任务的执行、判断任务的状态等。
FutureTask
类提供了取消任务的方法,即cancel(boolean mayInterruptIfRunning)
。这个方法允许我们取消一个尚未开始执行的任务,或者中断正在执行的任务。
cancel(boolean mayInterruptIfRunning)
方法接受一个布尔值参数mayInterruptIfRunning
,用于指定是否中断正在执行的任务。它有以下几种情况:
cancel()
方法将返回false
,并且任务不会被取消。cancel(true)
方法将中断等待的线程,并且任务将被取消。mayInterruptIfRunning
参数的值来决定是否中断执行的线程。如果mayInterruptIfRunning
为true
,则中断执行的线程;如果为false
,则任务不会被取消。需要注意的是,即使调用了cancel(true)
中断了任务的执行,任务本身也需要在执行过程中检查中断状态,并做相应的处理。
示例代码如下所示:
- FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
- @Override
- public Integer call() throws Exception {
- // 执行耗时的任务
- return 42;
- }
- });
-
- // 启动任务
- Thread thread = new Thread(task);
- thread.start();
-
- // 取消任务
- boolean cancelled = task.cancel(true);
- if (cancelled) {
- // 任务被成功取消
- } else {
- // 任务无法取消或已经完成
- }
在上面的示例中,我们创建了一个FutureTask
对象,并使用一个Callable
对象初始化它。然后,我们启动了一个新的线程来执行任务。最后,我们调用cancel(true)
方法来取消任务,并根据返回值判断任务是否被成功取消。
需要注意的是,cancel()
方法只能取消尚未开始执行的任务或者正在执行的任务。对于已经完成的任务,调用cancel()
方法将不会有任何效果。
要检索 `FutureTask` 的执行结果值,可以使用 `get()` 方法。
- try {
- Integer result = futureTask.get();
- // 处理结果值
- } catch (InterruptedException e) {
- // 处理中断异常
- } catch (ExecutionException e) {
- // 处理任务执行异常
- }
`get()` 方法会阻塞当前线程,直到任务执行完成并返回结果值。如果任务已经完成,`get()` 方法会立即返回结果值。如果任务尚未完成,`get()` 方法会一直阻塞等待,直到任务完成。
需要注意的是,`get()` 方法可能会抛出 `InterruptedException` 和 `ExecutionException` 异常。`InterruptedException` 表示在等待任务完成的过程中,当前线程被中断。`ExecutionException` 表示任务执行过程中抛出了异常。
在使用 `get()` 方法时,可以根据具体的业务逻辑进行异常处理。例如,可以捕获 `InterruptedException` 并进行相应的处理,或者捕获 `ExecutionException` 并处理任务执行过程中抛出的异常。
另外,还可以使用 `get(long timeout, TimeUnit unit)` 方法来设置等待超时时间。如果任务在指定的超时时间内未完成,`get()` 方法会抛出 `TimeoutException` 异常。这可以用于避免无限期等待任务的完成。
`Callable` 是一个泛型接口,它是 Java 并发编程中的一部分,位于 `java.util.concurrent` 包中。`Callable` 接口定义了一个具有返回值的任务,可以通过 `ExecutorService` 提交并执行。
`Callable` 接口包含一个单独的方法 `call()`,该方法没有参数,返回一个泛型类型的结果。在任务执行完成后,`call()` 方法会返回该任务的结果。
下面是一个简单的示例,展示了如何实现 `Callable` 接口:
- import java.util.concurrent.Callable;
-
- public class MyCallable implements Callable<Integer> {
- @Override
- public Integer call() throws Exception {
- // 执行任务逻辑,返回结果
- return 42;
- }
- }
在上述示例中,`MyCallable` 类实现了 `Callable<Integer>` 接口,并重写了 `call()` 方法。在 `call()` 方法中,可以编写具体的任务逻辑,并返回一个 `Integer` 类型的结果。
`Callable` 接口通常与 `Future` 和 `FutureTask` 结合使用。通过将 `Callable` 对象提交给线程池执行,可以获得一个 `Future` 对象,通过该对象可以获取任务的执行结果或取消任务的执行。
需要注意的是,`Callable` 接口与 `Runnable` 接口类似,但有一个明显的区别:`Runnable` 的 `run()` 方法没有返回值,而 `Callable` 的 `call()` 方法有返回值。这使得 `Callable` 更适合于需要返回结果的任务。
- @FunctionalInterface
- public interface Callable<V> {
- /**
- * Computes a result, or throws an exception if unable to do so.
- *
- * @return computed result
- * @throws Exception if unable to compute a result
- */
- V call() throws Exception;
- }
- @FunctionalInterface
- public interface Runnable {
- /**
- * When an object implementing interface <code>Runnable</code> is used
- * to create a thread, starting the thread causes the object's
- * <code>run</code> method to be called in that separately executing
- * thread.
- * <p>
- * The general contract of the method <code>run</code> is that it may
- * take any action whatsoever.
- *
- * @see java.lang.Thread#run()
- */
- public abstract void run();
- }
由源码就能够看出它们的区别:
它们的相同点
线程和进程是操作系统中的两个基本概念,用于执行程序和实现并发执行
线程和进程的使用场景和优势不同。进程适合用于执行独立的任务,各个进程之间相互独立,可以实现更好的隔离性和安全性;而线程适合用于执行并发任务,可以提高程序的响应性和效率,但需要注意线程安全和资源共享的问题。
我们都知道只有多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?
- public class NoStatusService {
-
- public void add(String status) {
- System.out.println("add status:" + status);
- }
-
- public void update(String status) {
- System.out.println("update status:" + status);
- }
- }
这个例子中NoStatusService没有定义公共资源,换句话说是无状态的。
这种场景中,NoStatusService类肯定是线程安全的。
如果多个线程访问的公共资源是不可变的,也不会出现数据的安全性问题。
- public class NoChangeService {
- public static final String DEFAULT_NAME = "abc";
-
- public void add(String status) {
- System.out.println(DEFAULT_NAME);
- }
- }
DEFAULT_NAME被定义成了static final的常量,在多线程中环境中不会被修改,所以这种情况,也不会出现线程安全问题。
有时候,我们定义了公共资源,但是该资源只暴露了读取的权限,没有暴露修改的权限,这样也是线程安全的。
- public class SafePublishService {
- private String name;
-
- public String getName() {
- return name;
- }
-
- public void add(String status) {
- System.out.println("add status:" + status);
- }
- }
这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。
使用JDK内部提供的同步机制,这也是使用比较多的手段,分为:同步方法 和 同步代码块。我们优先使用同步代码块,因为同步方法的粒度是整个方法,范围太大,相对来说,更消耗代码的性能。
其实,每个对象内部都有一把锁,只有抢到那把锁的线程,才被允许进入对应的代码块执行相应的代码。当代码块执行完之后,JVM底层会自动释放那把锁。
- public class SyncService {
- private int age = 1;
- private Object object = new Object();
-
- //同步方法
- public synchronized void add(int i) {
- age = age + i;
- System.out.println("age:" + age);
- }
-
-
- public void update(int i) {
- //同步代码块,对象锁
- synchronized (object) {
- age = age + i;
- System.out.println("age:" + age);
- }
- }
-
- public void update(int i) {
- //同步代码块,类锁
- synchronized (SyncService.class) {
- age = age + i;
- System.out.println("age:" + age);
- }
- }
- }
除了使用synchronized关键字实现同步功能之外,JDK还提供了Lock接口,这种显示锁的方式。
通常我们会使用Lock接口的实现类:ReentrantLock,它包含了:公平锁、非公平锁、可重入锁、读写锁 等更多更强大的功能。
- public class LockService {
- private ReentrantLock reentrantLock = new ReentrantLock();
- public int age = 1;
-
- public void add(int i) {
- try {
- reentrantLock.lock();
- age = age + i;
- System.out.println("age:" + age);
- } finally {
- reentrantLock.unlock();
- }
- }
- }
但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁。
不过说句实话,在使用Lock显示锁的方式,解决线程安全问题,给开发人员提供了更多的灵活性。
如果是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的。
但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全。
这就需要使用:分布式锁了。
分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。
其中我个人更推荐使用redis分布式锁,其效率相对来说更高一些。
- try{
- String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
- if ("OK".equals(result)) {
- return true;
- }
- return false;
- } finally {
- unlock(lockKey);
- }
同样需要在finally代码块中释放锁。
有时候,我们有这样的需求:如果在多个线程中,有任意一个线程,把某个开关的状态设置为false,则整个功能停止。
简单的需求分析之后发现:只要求多个线程间的可见性,不要求原子性。
如果一个线程修改了状态,其他的所有线程都能获取到最新的状态值。
这样一分析这就好办了,使用volatile就能快速满足需求。
- @Service
- public CanalService {
- private volatile boolean running = false;
- private Thread thread;
-
- @Autowired
- private CanalConnector canalConnector;
-
- public void handle() {
- //连接canal
- while(running) {
- //业务处理
- }
- }
-
- public void start() {
- thread = new Thread(this::handle, "name");
- running = true;
- thread.start();
- }
-
- public void stop() {
- if(!running) {
- return;
- }
- running = false;
- }
- }
需要特别注意的地方是:volatile不能用于计数和统计等业务场景。因为volatile不能保证操作的原子性,可能会导致数据异常。
除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。
当然ThreadLocal并不能完全取代锁,特别是在一些秒杀更新库存中,必须使用锁。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
温馨提醒一下:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在finally代码块中,调用它的remove方法清空数据,不然可能会出现内存泄露问题。
- public class ThreadLocalService {
- private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
-
- public void add(int i) {
- Integer integer = threadLocal.get();
- threadLocal.set(integer == null ? 0 : integer + i);
- }
- }
有时候,我们需要使用的公共资源放在某个集合当中,比如:ArrayList、HashMap、HashSet等。
如果在多线程环境中,有线程往这些集合中写数据,另外的线程从集合中读数据,就可能会出现线程安全问题。
为了解决集合的线程安全问题,JDK专门给我们提供了能够保证线程安全的集合。
比如:CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet、ArrayBlockingQueue等等。
- public class HashMapTest {
-
- private static ConcurrentHashMap<String, Object> hashMap = new ConcurrentHashMap<>();
-
- public static void main(String[] args) {
-
- new Thread(new Runnable() {
- @Override
- public void run() {
- hashMap.put("key1", "value1");
- }
- }).start();
-
- new Thread(new Runnable() {
- @Override
- public void run() {
- hashMap.put("key2", "value2");
- }
- }).start();
-
- try {
- Thread.sleep(50);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(hashMap);
- }
- }
在JDK底层,或者spring框架当中,使用ConcurrentHashMap保存加载配置参数的场景非常多。
比较出名的是spring的refresh方法中,会读取配置文件,把配置放到很多的ConcurrentHashMap缓存起来。
JDK除了使用锁的机制解决多线程情况下数据安全问题之外,还提供了CAS机制。
这种机制是使用CPU中比较和交换指令的原子性,JDK里面是通过Unsafe类实现的。
CAS内部包含了3个值:旧数据、期望数据、新数据 ,比较旧数据 和 期望的数据,如果一样的话,就把旧数据改成新数据。如果不一样的话,当前线程不断自旋,一直到成功为止。
不过,使用CAS保证线程安全,可能会出现ABA问题,需要使用AtomicStampedReference增加版本号解决。
其实,实际工作中很少直接使用Unsafe类的,一般用atomic包下面的类即可。
- public class AtomicService {
- private AtomicInteger atomicInteger = new AtomicInteger();
-
- public int add(int i) {
- return atomicInteger.getAndAdd(i);
- }
- }
有时候,我们在操作集合数据时,可以通过数据隔离,来保证线程安全。
- public class ThreadPoolTest {
-
- public static void main(String[] args) {
-
- ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数
- 10, //maximumPoolSize 线程池中最大线程数
- 60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收
- TimeUnit.SECONDS,//时间单位
- new ArrayBlockingQueue(500), //队列
- new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略
-
- List<User> userList = Lists.newArrayList(
- new User(1L, "苏三", 18, "成都"),
- new User(2L, "苏三说技术", 20, "四川"),
- new User(3L, "技术", 25, "云南"));
-
- for (User user : userList) {
- threadPool.submit(new Work(user));
- }
-
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(userList);
- }
-
- static class Work implements Runnable {
- private User user;
-
- public Work(User user) {
- this.user = user;
- }
-
- @Override
- public void run() {
- user.setName(user.getName() + "测试");
- }
- }
- }
这个例子中,使用线程池处理用户信息。
每个用户只被线程池中的一个线程处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。
数据隔离还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。
这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。
创建线程有几种常见的方式,包括:
1. 继承Thread类:创建一个继承自Thread类的子类,并重写run()方法来定义线程的任务。
- public class MyThread extends Thread {
- @Override
- public void run() {
- // 线程的任务逻辑
- System.out.println("Thread is running.");
- }
- }
-
- // 创建并启动线程
- public static void main(String[] args) {
- MyThread thread = new MyThread();
- thread.start();
- }
2. 实现Runnable接口:创建一个实现了Runnable接口的类,并实现run()方法,然后将该类的实例传递给Thread类的构造方法。
- public class MyRunnable implements Runnable {
- @Override
- public void run() {
- // 线程的任务逻辑
- System.out.println("Thread is running.");
- }
- }
-
- // 创建并启动线程
- public static void main(String[] args) {
- MyRunnable runnable = new MyRunnable();
- Thread thread = new Thread(runnable);
- thread.start();
- }
3.实现Callable接口
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.FutureTask;
-
- public class MyCallable implements Callable<Integer> {
- @Override
- public Integer call() throws Exception {
- // 线程的任务逻辑
- int sum = 0;
- for (int i = 1; i <= 10; i++) {
- sum += i;
- }
- return sum;
- }
- }
-
- // 创建并启动线程
- public static void main(String[] args) {
- Callable<Integer> callable = new MyCallable();
- FutureTask<Integer> futureTask = new FutureTask<>(callable);
- Thread thread = new Thread(futureTask);
- thread.start();
-
- try {
- // 获取线程执行结果
- int result = futureTask.get();
- System.out.println("Thread result: " + result);
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
- }
4.使用Executor框架
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
-
- public class MyTask implements Runnable {
- @Override
- public void run() {
- // 线程的任务逻辑
- System.out.println("Thread is running.");
- }
- }
-
- // 创建并启动线程
- public static void main(String[] args) {
- ExecutorService executor = Executors.newFixedThreadPool(1);
- executor.execute(new MyTask());
- executor.shutdown();
- }
5. 使用匿名类:使用匿名类来创建线程,可以简化代码。
- // 创建并启动线程
- public static void main(String[] args) {
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- // 线程的任务逻辑
- System.out.println("Thread is running.");
- }
- });
- thread.start();
- }
6. 使用Lambda表达式:使用Lambda表达式来创建线程,可以进一步简化代码。
- // 创建并启动线程
- public static void main(String[] args) {
- Thread thread = new Thread(() -> {
- // 线程的任务逻辑
- System.out.println("Thread is running.");
- });
- thread.start();
- }
以上是几种常见的创建线程的方式,每种方式都有其适用的场景和优势。在选择创建线程的方式时,需要根据具体的需求和代码结构来决定使用哪种方式。
线程安全是指当多个线程同时访问某个共享资源时,不会出现不正确的结果或不一致的状态。在多线程环境下,如果程序能够正确地处理并保护共享资源,即使多个线程同时访问,也不会导致数据的损坏或程序的错误。
线程安全的实现通常需要考虑以下几个方面:
1. 原子性(Atomicity):确保某个操作是不可分割的,要么完全执行,要么完全不执行。可以使用同步机制(如锁、原子类等)来保证原子性。
2. 可见性(Visibility):确保一个线程对共享变量的修改对其他线程是可见的。可以使用 `volatile` 关键字、锁或并发容器等来保证可见性。
3. 有序性(Ordering):确保程序执行的顺序符合预期。可以使用同步机制、`volatile` 关键字或显式的顺序控制来保证有序性。
在编写多线程程序时,需要注意保证共享资源的线程安全性。常见的线程安全问题包括竞态条件(Race Condition)、死锁(Deadlock)、活锁(Livelock)等。为了避免这些问题,可以采用合适的同步机制、使用线程安全的数据结构、避免对共享资源的直接修改等措施。
需要注意的是,并非所有的代码都需要考虑线程安全性。只有当多个线程并发访问同一份共享资源时,才需要考虑线程安全。对于只在单线程环境下执行的代码,不需要特别关注线程安全性。
上下文切换(Context Switching)是操作系统在多任务(多线程或多进程)环境下,将当前执行的任务(线程或进程)的状态(上下文)保存起来,并恢复另一个任务的状态的过程。
在多任务环境下,操作系统将 CPU 的时间片(时间片轮转调度)分配给不同的任务,使得它们交替执行。当一个任务的时间片用完或发生阻塞事件时,操作系统会暂停该任务的执行,并将其当前的上下文(包括寄存器的值、程序计数器、堆栈指针等)保存起来,然后从就绪队列中选择另一个任务来执行。这个过程就是上下文切换。
上下文切换的过程包括以下几个步骤:
1. 保存当前任务的上下文:将当前任务的寄存器的值、程序计数器、堆栈指针等保存到内存中的上下文数据结构中。
2. 切换到新任务的上下文:从就绪队列中选择一个新任务,并将其上下文数据结构从内存中恢复到寄存器中。
3. 更新调度信息:更新任务的状态和调度信息,准备下一次调度。
上下文切换是操作系统实现多任务并发的关键机制之一。它使得多个任务能够共享 CPU 的时间片,从而实现同时运行的错觉。然而,上下文切换也会带来一定的开销,因为切换过程需要保存和恢复大量的上下文数据。因此,在设计和优化多任务系统时,需要合理控制上下文切换的频率,以提高系统的性能和效率。
用户线程是默认类型的线程,它不会影响JVM的退出。当所有用户线程执行完毕后,即使守护线程仍在运行,JVM也会正常退出。用户线程通常用于执行应用程序的核心业务逻辑。
守护线程是一种特殊类型的线程,它的存在依赖于用户线程。当所有用户线程结束时,守护线程会自动退出。守护线程通常用于执行一些后台任务,如垃圾回收(Garbage Collection)等。
JVM退出的影响:当只剩下守护线程时,JVM会自动退出,而用户线程的存在不会阻止JVM的退出。
资源访问限制:守护线程不能访问用户线程创建的资源,因为它们可能会在任何时候被终止。守护线程通常不应该依赖于共享资源的状态。
执行优先级:守护线程的优先级较低,通常会被用户线程抢占执行。
任务类型:用户线程通常用于执行应用程序的核心业务逻辑,而守护线程通常用于执行一些后台任务,如垃圾回收(Garbage Collection)等。
创建方式:用户线程和守护线程的创建方式是一样的,都可以通过继承Thread类、实现Runnable接口或使用Executor框架来创建。
需要注意的是,守护线程的存在是为了支持用户线程的工作,当所有用户线程结束时,守护线程会自动退出。因此,在使用守护线程时,需要确保用户线程已经完成了它们的任务,否则可能会导致守护线程提前退出而导致任务未完成。
在 Windows 和 Linux 上,可以使用不同的工具来查找哪个线程的 CPU 利用率最高。以下是针对两个操作系统的示例说明:
在 Windows 上:
1. 打开任务管理器(可以通过按下 Ctrl + Shift + Esc 快捷键或右键点击任务栏并选择“任务管理器”来打开)。
2. 切换到“详细信息”选项卡。
3. 在“详细信息”选项卡中,可以看到列出了所有正在运行的进程和线程。点击“CPU”列的标题,按照 CPU 利用率进行排序。
4. 查找 CPU 利用率最高的线程,可以根据线程 ID 或线程名称进行识别。
在 Linux 上:
1. 打开终端。
2. 使用 `top` 命令查看当前系统的进程和线程信息,按下“Shift + P”按照 CPU 利用率进行排序。
3. 查找 CPU 利用率最高的线程,可以根据线程 ID 或线程名称进行识别。
另外,在 Linux 上还可以使用 `htop` 命令进行更直观的查看。在终端中输入 `htop` 命令,然后按下“F6”键选择按照 CPU 利用率进行排序。
需要注意的是,CPU 利用率最高的线程并不一定代表它是问题的根源,可能是正常的系统行为或其他因素导致的。在分析和解决问题时,还需要结合其他指标和上下文信息来进行综合判断。
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态,这些永远在互相等待的进程(线程)称为死锁进程(线程)
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
形成死锁的四个必要条件是:
互斥条件(Mutual Exclusion):至少有一个资源被标记为独占性,即一次只能被一个进程或线程占用。当资源被占用时,其他进程或线程无法访问该资源。
请求与保持条件(Hold and Wait):一个进程或线程在持有某个资源的同时,又请求其他进程或线程所持有的资源。换句话说,进程或线程在等待其他资源的同时,仍然保持着已经获得的资源。
不可剥夺条件(No Preemption):已经获得的资源不能被强制性地剥夺,只能在使用完毕后自愿释放。
循环等待条件(Circular Wait):存在一个进程或线程的资源请求链,使得每个进程或线程都在等待下一个进程或线程所持有的资源。
这四个条件同时满足时,就可能导致死锁的发生。当发生死锁时,各个进程或线程将无法继续执行,造成系统无法正常工作。
为了避免死锁的发生,可以通过破坏其中一个或多个必要条件来解决。例如,通过合理的资源分配和调度策略,可以破坏请求与保持条件和循环等待条件。另外,还可以使用死锁检测和死锁恢复等机制来处理潜在的死锁情况。
要避免线程死锁,可以采取以下几种常见的策略和技巧:
1. 避免循环等待:尽量避免线程之间形成循环的资源依赖关系。如果必须存在资源依赖关系,可以通过按照统一的顺序获取资源来避免循环等待。
2. 使用资源分配策略:可以采用资源分配策略,如银行家算法,来避免线程请求资源时发生死锁。该算法可以预先评估资源请求的安全性,只允许安全的资源分配。
3. 避免长时间持有锁:尽量减少持有锁的时间,避免一个线程在等待其他资源时持有锁。可以通过合理的锁粒度划分和使用局部变量来减少锁的持有时间。
4. 使用超时机制:在获取锁时,可以使用超时机制,即尝试获取锁一段时间后放弃,避免长时间等待锁而导致死锁。
5. 避免嵌套锁:尽量避免在持有一个锁的情况下又去申请另一个锁,这样容易导致死锁。如果确实需要多个锁,可以按照相同的顺序获取锁,避免嵌套锁。
6. 使用死锁检测工具:可以使用专门的死锁检测工具来帮助发现和解决潜在的死锁问题。这些工具可以分析线程间的依赖关系和锁的获取情况,提供死锁检测和解决的建议。
7. 进行合理的资源规划和设计:在系统设计和资源规划时,考虑到并发访问的情况,合理分配和管理资源,避免资源竞争和死锁的发生。
在多线程编程中,线程的 run()
方法和 start()
方法有以下区别:
run()
方法:
run()
方法是线程的实际执行代码,包含了线程要执行的任务逻辑。run()
方法时,线程的任务逻辑会在当前线程中同步执行,而不会创建新的线程。start()
方法:
start()
方法用于启动一个新的线程,创建一个新的线程并使其进入就绪状态。start()
方法时,会创建一个新的线程,并在新线程中调用 run()
方法来执行线程的任务逻辑。start()
方法会异步地启动新线程,使其在后台并发执行,不会阻塞当前线程。总结起来,run()
方法是线程的实际任务逻辑,而 start()
方法用于启动一个新的线程并异步执行线程的任务逻辑。直接调用 run()
方法只会在当前线程中同步执行任务,而调用 start()
方法会创建新的线程并在新线程中执行任务。在多线程编程中,通常使用 start()
方法来启动线程,以实现并发执行的效果。
骚戴理解:多线程原理相当于玩游戏机,只有一个游戏机(CPU),start是排队,等CPU轮到你,你就run。调用start后,线程会被放入到等待队列中,也就是上面说的就绪状态,等待CPU调用,并不是马上运行。一旦得到cpu时间片就通过java虚拟机在运行相应线程时直接调用run方法来执行本线程的线程体,先调用start,后调用run。
在多线程编程中,我们调用 `start()` 方法来启动一个新的线程并执行线程的任务逻辑,而不能直接调用 `run()` 方法的主要原因是为了实现并发执行。
当我们调用 `start()` 方法时,会创建一个新的线程,并在新线程中调用 `run()` 方法来执行线程的任务逻辑。这样可以实现多个线程同时执行任务,从而实现并发的效果。每个线程都有自己独立的执行上下文和调用栈,它们可以并行执行,提高程序的性能和效率。
如果我们直接调用 `run()` 方法,那么线程的任务逻辑将在当前线程中同步执行,而不会创建新的线程。这样就无法实现多个线程的并发执行,而只是在当前线程中按顺序执行任务逻辑。这样做的结果是,任务的执行将会阻塞当前线程,影响整个程序的执行效率。
因此,为了实现多线程的并发执行,我们应该使用 `start()` 方法来启动新的线程,让每个线程在独立的执行上下文中执行任务逻辑,而不是直接调用 `run()` 方法。这样可以充分利用计算资源,提高程序的并发性和性能。
线程的生命周期分为新建(new)、就绪(Runnable)、运行(running)、阻塞(Blocked)、死亡(Dead)五种状态
新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
线程调度器选择优先级最高的线程运行,但如果发生以下情况就会让出CPU
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片。
抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看, 各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
与线程同步相关的方法有以下几种:
synchronized关键字:通过在方法或代码块前加上synchronized关键字,可以实现对共享资源的互斥访问。当一个线程获取到对象的锁时,其他线程需要等待锁释放后才能访问。
ReentrantLock类:它是Java提供的可重入锁,通过lock()和unlock()方法来实现对共享资源的互斥访问。相比于synchronized关键字,ReentrantLock提供了更灵活的锁控制,可以实现公平锁和非公平锁。
wait()、notify()和notifyAll()方法:这些方法是Object类中的方法,用于实现线程之间的等待和唤醒机制。wait()方法使当前线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒它。这些方法通常与synchronized关键字一起使用。
与线程调度相关的方法有以下几种:
yield()方法:调用yield()方法会让当前线程让出CPU资源,使得其他具有相同优先级的线程有机会执行。但是,并不能保证yield()方法一定会让出CPU,它仅是一个建议。
sleep()方法:调用sleep()方法会让当前线程进入休眠状态,暂停一段时间。在指定的时间过去后,线程会自动唤醒并继续执行。
join()方法:调用join()方法会让当前线程等待被调用线程执行完毕。在被调用线程执行完毕后,当前线程才会继续执行。
setPriority()方法:通过setPriority()方法可以设置线程的优先级。优先级较高的线程在竞争CPU资源时更有可能被调度执行。
这些方法可以用来控制线程的执行顺序和优先级,以满足不同的需求和场景。
`yield()` 方法是一个静态方法,它属于 `Thread` 类,用于在多线程编程中控制线程的执行。`yield()` 方法的作用是让当前线程让步,暂停当前线程的执行,让其他具有相同或更高优先级的线程有机会执行。
当一个线程调用 `yield()` 方法时,它会暂停当前线程的执行,并将执行机会让给其他具有相同或更高优先级的线程。此时,线程调度器会从就绪状态的线程中选择一个来执行,具体选择哪个线程是不确定的,取决于线程调度器的具体实现。
`yield()` 方法的主要作用是提高线程的执行效率和公平性。通过让步,它可以使具有相同或更高优先级的其他线程有更多的机会执行,避免某个线程长时间占用CPU资源,提高多线程程序的整体性能。
需要注意的是,`yield()` 方法只是一种建议,它不能保证当前线程立即被暂停,也不能保证其他线程立即获得执行。实际上,`yield()` 方法的执行效果是依赖于操作系统和线程调度器的具体实现的。
sleep()
方法是一个静态方法,它属于 Thread
类,用于在多线程编程中暂停当前线程的执行一段时间。sleep()
方法的作用是让当前线程进入阻塞状态,暂停执行指定的时间,然后再继续执行。
sleep()
方法有以下几个特点和用途:
指定时间段的暂停:sleep()
方法可以接收一个参数,指定线程暂停的时间,单位是毫秒(ms)。
Thread.sleep(1000)
表示当前线程暂停执行 1000 毫秒(即 1 秒)。阻塞当前线程:sleep()
方法会使当前线程进入阻塞状态,暂停执行指定的时间。
处理定时任务:sleep()
方法常用于处理定时任务,例如定时刷新数据、定时发送消息等。
sleep()
方法,可以控制线程在指定的时间间隔内执行任务。需要注意的是,sleep()
方法可能会抛出 InterruptedException
异常,该异常在线程被中断时抛出,可以通过捕获该异常来处理中断事件。
总结起来,sleep()
方法的作用是暂停当前线程的执行一段时间,让出 CPU 时间片给其他线程执行。它常用于实现定时任务和处理需要暂停执行的场景。
yield()
方法和 sleep()
方法在多线程编程中有以下几个区别:
功能不同:
yield()
方法的作用是让当前线程让步,暂停当前线程的执行,让其他具有相同或更高优先级的线程有机会执行。sleep()
方法的作用是暂停当前线程的执行一段时间,让出 CPU 时间片给其他线程执行。使用方式不同:
yield()
方法是一个静态方法,属于 Thread
类,可以通过 Thread.yield()
调用。sleep()
方法是一个静态方法,属于 Thread
类,可以通过 Thread.sleep()
调用。影响范围不同:
yield()
方法只会影响具有相同或更高优先级的其他线程的执行,它会让出 CPU 时间片给其他线程。sleep()
方法会暂停当前线程的执行,不影响其他线程的执行。阻塞状态不同:
yield()
方法并不会将当前线程进入阻塞状态,只是让出 CPU 时间片给其他线程执行,然后重新进入就绪状态等待执行。sleep()
方法会将当前线程进入阻塞状态,暂停执行指定的时间,然后重新进入就绪状态等待执行。时间控制方式不同:
yield()
方法无法精确控制线程的暂停时间,它只是建议让出 CPU 时间片给其他线程,具体的执行时间由线程调度器决定。sleep()
方法可以精确控制线程的暂停时间,可以指定线程暂停的时间段,以毫秒为单位。需要根据具体的需求选择使用 yield()
方法还是 sleep()
方法。如果想让出 CPU 时间片给其他线程,提高线程的执行效率和公平性,可以使用 yield()
方法。如果需要暂停当前线程的执行一段时间,可以使用 sleep()
方法。
sleep()
方法和 wait()
方法在多线程编程中有以下几个区别:
调用方式不同:
sleep()
方法是一个静态方法,属于 Thread
类,可以通过 Thread.sleep()
调用。wait()
方法是一个实例方法,属于 Object
类,需要在对象上调用,例如 object.wait()
。所属类不同:
sleep()
方法属于 Thread
类,用于控制线程的执行。wait()
方法属于 Object
类,用于线程间的协调和通信。锁的释放不同:
sleep()
方法在执行期间不会释放线程所持有的锁。wait()
方法在执行期间会释放线程所持有的锁,使得其他线程可以获取该对象的锁并执行。唤醒方式不同:
sleep()
方法会在指定的时间到达后自动唤醒线程,线程会从阻塞状态恢复到就绪状态。wait()
方法需要通过其他线程调用相同对象上的 notify()
或 notifyAll()
方法来唤醒等待的线程。使用场景不同:
sleep()
方法主要用于暂停当前线程的执行一段时间,用于实现定时任务、延时操作等。wait()
方法主要用于线程间的协调和通信,等待其他线程满足特定条件后再继续执行。需要注意的是,wait()
方法必须在同步代码块或同步方法中调用,否则会抛出 IllegalMonitorStateException
异常。而 sleep()
方法可以在任何地方调用。
总结起来,sleep()
方法用于控制线程的执行,可以暂停当前线程的执行一段时间;wait()
方法用于线程间的协调和通信,使线程等待其他线程满足特定条件后再继续执行。
线程调度器(Thread Scheduler)是操作系统或虚拟机的一部分,负责决定哪个线程在特定时刻运行。它是多线程环境中的一个重要组件,用于分配和管理系统资源,以便有效地执行多个线程。
线程调度器的主要任务是根据一定的调度算法,决定在给定的时间片内将 CPU 时间分配给哪个线程执行。它根据线程的优先级、状态和调度策略等因素,决定线程的执行顺序和时间片分配。
线程调度器的作用包括:
1. 切换线程执行:线程调度器负责切换不同线程之间的执行,使得多个线程可以交替执行,共享 CPU 资源。
2. 分配时间片:线程调度器将 CPU 时间划分为一小段段的时间片,然后按照一定的策略将时间片分配给不同的线程,使得每个线程都能获得一定的执行时间。
3. 调整线程优先级:线程调度器根据线程的优先级来决定线程的执行顺序,高优先级的线程会优先获得 CPU 时间片。
4. 处理阻塞和唤醒:线程调度器会处理线程的阻塞和唤醒操作,当线程被阻塞时,调度器会将其从运行状态切换到阻塞状态,当线程被唤醒时,调度器会将其从阻塞状态切换到就绪状态。
线程调度器的具体实现方式和策略会因操作系统和虚拟机的不同而有所差异。常见的调度算法包括先来先服务(FCFS)、时间片轮转(Round Robin)、优先级调度等。
时间分片(Time Slicing)是线程调度器(Thread Scheduler)的一种策略,用于将 CPU 时间划分为一小段段的时间片,并按照一定的规则将时间片分配给不同的线程执行。
在多线程环境下,有多个线程竞争执行,但实际上 CPU 只能同时执行一个线程的指令。为了公平地分配 CPU 资源,避免某个线程长时间独占 CPU,线程调度器采用时间分片策略。
时间分片的基本原理是,将 CPU 时间划分为固定长度的时间片,每个线程在每个时间片内获得一定的执行时间。当一个线程的时间片用完后,线程调度器会暂停该线程的执行,并将 CPU 时间片分配给其他等待执行的线程。这样,多个线程可以交替执行,共享 CPU 资源。
时间分片策略的优势在于:
- 公平性:每个线程都能获得一定的执行时间,避免某个线程长时间独占 CPU 资源。
- 响应性:即使有某个线程需要长时间执行,其他线程仍然能够在时间片内得到执行,提高程序的响应性。
时间分片的长度可以根据具体的系统和应用需求进行调整。较短的时间片可以提高线程切换的频率,增加系统的响应速度,但也会增加线程切换的开销;较长的时间片可以减少线程切换的开销,但可能会导致某些线程长时间无法得到执行。
在使用 `wait()` 方法时,通常应该使用 `while` 循环而不是 `if` 块来进行条件判断。
`wait()` 方法用于线程间的协调和通信,它使线程进入等待状态,直到其他线程调用相同对象上的 `notify()` 或 `notifyAll()` 方法来唤醒等待的线程。在等待状态中,线程会释放对象的锁。
使用 `while` 循环进行条件判断的主要原因是防止虚假唤醒(Spurious Wakeup)。虚假唤醒是指在没有收到 `notify()` 或 `notifyAll()` 通知的情况下,线程从等待状态中被唤醒。虽然这种情况比较罕见,但在多线程环境中仍然可能发生。
通过使用 `while` 循环进行条件判断,可以在线程被唤醒后重新检查条件,确保线程被正确地唤醒。如果使用 `if` 块进行条件判断,当线程被虚假唤醒时,可能会继续执行后续的操作,而不是等待满足特定条件。
以下是使用 `wait()` 方法时的典型代码模式:
- synchronized (lock) {
- while (condition) {
- lock.wait();
- }
- // 执行相应的操作
- }
在这个示例中,线程在 `while` 循环中调用 `wait()` 方法等待条件满足。如果线程被虚假唤醒,它会重新检查条件,如果条件仍然不满足,线程会继续等待。只有当条件满足时,线程才会执行后续的操作。
总结起来,使用 `while` 循环进行条件判断可以防止虚假唤醒,确保线程被正确地唤醒并执行相应的操作。
wait()
, notify()
, 和 notifyAll()
方法被定义在 Object
类中的主要原因是线程通信是与对象相关的操作,而每个 Java 对象都具有这些基本的线程同步方法。
在 Java 中,线程通信是通过对象的监视器(monitor)来实现的。每个对象都有一个与之关联的监视器,用于协调线程之间的同步和通信。因此,为了实现线程通信,Java 将这些方法定义在所有对象的共同基类 Object
中,以便所有对象都可以使用这些方法。
具体来说,这些方法的作用如下:
wait()
: 调用该方法会使当前线程进入等待状态,直到其他线程调用相同对象上的 notify()
或 notifyAll()
方法来唤醒等待的线程。notify()
: 调用该方法会唤醒在相同对象上调用 wait()
方法进入等待状态的一个线程。如果有多个线程在等待,只会唤醒其中的一个线程,具体唤醒哪个线程是不确定的。notifyAll()
: 调用该方法会唤醒在相同对象上调用 wait()
方法进入等待状态的所有线程。将这些方法定义在 Object
类中的好处是所有的对象都可以直接使用这些方法,无需额外的接口或继承。这样,任何对象都可以作为线程通信的对象,并且可以方便地进行同步和通信操作。
需要注意的是,这些方法只能在同步代码块或同步方法中使用,因为它们依赖于对象的监视器锁来实现线程同步。如果在非同步的上下文中使用这些方法,会导致 IllegalMonitorStateException
异常的抛出。
wait()
, notify()
, 和 notifyAll()
方法必须在同步方法或同步块中被调用,是因为它们依赖于对象的监视器锁(monitor lock)来实现线程同步和通信。
在 Java 中,每个对象都有一个与之关联的监视器,也称为内部锁或监视器锁。同步方法和同步块都可以使用 synchronized
关键字来获取和释放对象的监视器锁。
当一个线程执行同步方法或同步块时,它会首先尝试获取对象的监视器锁。如果锁是可用的,线程会获取锁并进入临界区,执行同步代码。如果锁被其他线程持有,线程将被阻塞,直到锁被释放。
在这种同步的上下文中,wait()
, notify()
, 和 notifyAll()
方法才能正常工作,具体原因如下:
wait()
方法会使当前线程释放对象的监视器锁,并进入等待状态,直到其他线程调用相同对象上的 notify()
或 notifyAll()
方法来唤醒等待的线程。如果在非同步的上下文中调用 wait()
方法,线程没有持有锁,无法释放锁,也无法被其他线程唤醒,因此会导致错误。notify()
和 notifyAll()
方法用于唤醒在相同对象上调用 wait()
方法进入等待状态的线程。这些方法需要在持有对象的监视器锁的线程中调用,以确保正确地通知等待的线程。如果在非同步的上下文中调用这些方法,将无法正确地唤醒等待的线程。因此,为了正确使用 wait()
, notify()
, 和 notifyAll()
方法,必须在同步方法或同步块中调用,以确保线程在正确的上下文中获取和释放对象的监视器锁,从而实现线程同步和通信。
Thread
类的 sleep()
和 yield()
方法是静态的,是因为它们是与线程调度相关的操作,不依赖于具体的线程实例。
下面是对这两个方法的解释:
sleep()
方法:
sleep()
方法用于使当前线程暂停执行一段指定的时间。调用 sleep()
方法时,当前线程会进入阻塞状态,暂停执行指定的时间间隔。sleep()
方法是与线程调度相关的操作,它不依赖于具体的线程实例。因此,它被定义为静态方法,可以直接通过 Thread
类来调用。yield()
方法:
yield()
方法用于提示线程调度器当前线程愿意让出 CPU 执行时间,以便其他具有相同或更高优先级的线程有机会执行。sleep()
方法类似,yield()
方法也是与线程调度相关的操作,不依赖于具体的线程实例。因此,它也被定义为静态方法。将这些方法定义为静态方法的好处是,它们可以直接通过 Thread
类来调用,无需创建线程实例。这样可以更方便地使用这些方法来控制线程的行为,而无需关注具体的线程对象。
需要注意的是,虽然 sleep()
和 yield()
方法是静态的,但它们仍然会影响当前线程的执行。因此,在调用这些方法时,要谨慎考虑对当前线程以及其他线程的影响,并确保合理地使用这些方法来实现预期的线程行为。
停止线程是在多线程开发中很重要的技术点,比如在多线程持续处理业务代码时,由于处理逻辑中有第三方接口异常,我们就假设发送短信接口挂了吧,那么此时多线程调用短信接口是没有任何意义的,我们希望接口恢复后再对接口进行处理,那么此时怎么办呢,如何中止已经启动的线程呢?
其实在Java中有3种方式可以终止正在运行的线程
- public class UserModel {
-
- /**
- * 给定userName+password默认值
- * 用于模拟上一个线程给赋的旧值
- */
- private String userName = "张三";
-
- private String password = "hahahha";
-
- /**
- * 用于复制的方法
- * 为防止多线程数据错乱,加上synchronized关键字
- * @param userName
- * @param password
- */
- synchronized public void setValue(String userName, String password){
- try {
- this.userName = userName;
- Thread.sleep(3000);
- this.password = password;
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- 省略get\set方法...
- }
然后我们再在ThreadDemo中使用这个实体:
- public class ThreadDemo extends Thread{
-
- private UserModel userModel;
-
- public ThreadDemo(UserModel userModel){
- this.userModel = userModel;
- }
-
- @Override
- public void run() {
- /**
- * 重新设置用户名+密码
- * 用户名:niceyoo
- * 密码:123456
- */
- userModel.setValue("niceyoo","123456");
- }
- }
然后在MyTest中创建并启动线程,然后调用stop()方法:
- public class MyTest {
-
- public static void main(String[] args) {
-
- try {
- /**创建用户实体**/
- UserModel userModel = new UserModel();
- /**创建线程**/
- ThreadDemo demo = new ThreadDemo(userModel);
- /**开启线程**/
- demo.start();
- /**线程休眠**/
- Thread.sleep(1000);
- /**停止线程**/
- demo.stop();
- /**输出用户实体**/
- System.out.println(userModel.getUserName() + " :" + userModel.getPassword());
- } catch (ThreadDeath | InterruptedException e) {
- e.printStackTrace();
- }
-
- }
-
- }
输出结果如下:
niceyoo :hahahha
显然跟我们预期的输出结果niceyoo\123456不一致,使用stop()释放锁,对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致的情况,所以这样就会导致数据安全问题,这也是现在为何 stop() 方法被标注为 “作废、过期”。
使用interrupt()方法就不像是stop()方法那样简单粗暴了,调用该方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,就好比,我打电话告诉你不要玩游戏了,但是你什么时候停止玩游戏就是你的事了。
- public class MyTest {
-
- public static void main(String[] args) {
- try {
- /**创建线程**/
- ThreadDemo2 demo = new ThreadDemo2();
- /**开启线程**/
- demo.start();
- /**线程休眠**/
- Thread.sleep(2000);
- /**停止线程**/
- demo.interrupt();
- } catch (InterruptedException e) {
- System.out.println("线程已经暂停");
- e.printStackTrace();
- }
- }
-
- }
-
- public class ThreadDemo2 extends Thread{
-
- @Override
- public void run() {
- try {
- for (int i = 0; i < 1800000; i++) {
-
- if(!this.isInterrupted()){
- System.out.println("输出i:"+i++ + " - 线程未停止 ");
- }else{
- System.out.println("输出i:"+i++ + " - 线程已停止 - 抛出异常");
- throw new InterruptedException();
- }
- }
- }catch (InterruptedException e){
- System.out.println("线程已结束...");
- }
- }
- }
输出结果:
- 输出i:1499992 - 线程未停止
- ...
- 输出i:1700624 - 线程未停止
- 输出i:1700626 - 线程未停止
- 输出i:1700628 - 线程已停止 - 抛出异常
- 线程已结束...
简单说一下上方代码,首先我们创建了一个for循环输出i++的线程,启动线程后调用 interrupt() 方法停止线程,但是啥时候停止是不可控的,虽然不可控但是还是有方法知道线程是否是停止的,我们在ThreadDemo2线程类中看到 if 判断 — this.isInterrupted() 「等价于Thread.currentThread().isInterrupt() 」,这是用来判断当前线程是否被终止,通过这个判断我们可以做一些业务逻辑处理,通常如果this.isInterrupted被判定为true后,我们会抛一个中断异常,然后通过try-catch捕获。
再额外说一下,有的小伙伴设置的 for 循环变量的最大值比较小,测试执行过程中并没有重现线程被终止,然后就怀疑这个 interrupt() 到底能不能停止线程呀, 不用纠结,这正是线程的自主权,我们无法像 stop() 方法一样直接停止线程的。
设置标志位使用了volatile关键字共享变量方式,通过改变共享变量+抛异常的方式来暂停线程,注意是暂停线程。当共享标志位位false的的时候就停止线程,我们了解线程对于变量的操作都是操作的变量副本,而一旦使用volatile关键字修饰后,因为其可见性,变量变更始将终从主存中获取最新值。
- public class MyTest {
-
- public static void main(String[] args) {
- /**创建2个线程**/
- ThreadDemo3 demo1 = new ThreadDemo3();
- ThreadDemo3 demo2 = new ThreadDemo3();
- demo1.setName("线程1");
- demo2.setName("线程2");
- /**开启线程**/
- demo1.start();
- demo2.start();
- /**让线程先运行5s**/
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- /**修改线程的变量**/
- demo1.heartbeat = false;
- demo2.heartbeat = false;
-
- System.out.println("----暂停线程----");
-
- /**让线程再运行5s**/
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- /**再将标志为置为true**/
- demo1.heartbeat = true;
- demo2.heartbeat = true;
-
- System.out.println("----从新开启线程----");
- }
- }
-
- public class ThreadDemo3 extends Thread{
- /**共享变量**/
- volatile Boolean heartbeat = true;
-
- @Override
- public void run() {
- while (true){
- /**判断标志是否为true**/
- if (heartbeat){
- System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 运行");
- }else{
- System.out.println("当前运行线程为:" +Thread.currentThread().getName() + " - 非运行");
- }
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
输出结果:
- 省略ing...
- 当前运行线程为:线程1 - 运行
- 当前运行线程为:线程2 - 运行
- ----暂停线程----
- 省略ing...
- 当前运行线程为:线程1 - 非运行
- 当前运行线程为:线程2 - 非运行
- ----从新开启线程----
- 当前运行线程为:线程1 - 运行
- 当前运行线程为:线程2 - 运行
- 省略ing...
来看一下上方代码,我们在线程类里创建了共享变量heartbeat,因为要监听这个共享变量的状态,肯定是要用while循环体了,为了演示状态的变更,所以在while循环体代码中没有throw抛出 InterruptedException 异常,正常情况下在判断共享变量为fasle时,也是要手动抛出异常的,ok,这就是设置标志位了。
stop()方法:确实,stop()方法已经被废弃,因为它可能会导致线程的状态不一致和资源泄漏等问题,不推荐使用。
interrupt()方法+抛异常处理:使用interrupt()方法可以中断一个线程,它会设置线程的中断状态为true。然后在线程的代码中,可以通过检查中断状态来判断是否需要终止线程。通常,可以通过抛出InterruptedException异常来中断线程,并在异常处理代码中进行相应的处理。
设置标志位(volatile共享变量)+抛异常处理:通过设置一个共享的标志位(通常使用volatile关键字修饰),线程在运行过程中可以检查这个标志位来判断是否需要终止线程。当需要终止线程时,将标志位设置为true,并在线程的代码中进行相应的处理,例如抛出异常或者执行清理操作。
阻塞状态下的中断问题:在某些阻塞状态下,线程无法及时响应中断请求,例如在Thread.join()、Thread.sleep()、ServerSocket.accept()等方法中。这是因为线程在阻塞状态时,无法主动检查中断状态。因此,需要在代码中合理处理中断,例如在捕获InterruptedException异常后,手动设置中断状态,以便让线程能够及时响应中断。
总的来说,使用interrupt()方法结合适当的异常处理是目前最为正确和可靠的方式来中断一个正在运行的线程。但需要注意,在线程的代码中需要合理处理中断请求,以确保线程能够及时响应和退出。
当一个线程因为某些原因没有正常执行,而是处于等待阻塞状态,这就是线程阻塞,例如等待I/O操作完成,等待某个锁可用或等待一个外部计算结束。而可以导致线程阻塞的方法,我们称为 阻塞方法。
在 Java 中,能抛出 InterruptedException 的方法一定是阻塞方法,这个异常会在处于阻塞状态的线程被中断时抛出。Java 中,每个线程有一个中断状态,Thread 提供了 interrupt 方法用以中断线程(设置中断状态)。
需要注意的是,中断是一种协作机制。一个线程不能强制另一个线程停止正在执行的操作而去执行其他操作。当线程 A 去中断线程 B 时,A 仅仅设置了 B 的中断状态,B 可以检查到这个中断状态然后在可以停止的地方自愿停下当前操作。
在处理线程中断时,可以采取以下几种方法:
检查中断状态:在线程的执行代码中,可以使用Thread类的静态方法Thread.interrupted()或实例方法isInterrupted()来检查线程的中断状态。通过检查中断状态,可以根据需要决定是否终止线程的执行。
抛出InterruptedException异常:在使用一些阻塞方法(如Thread.sleep()、Object.wait()、Condition.await()等)时,需要捕获InterruptedException异常。当线程被中断时,这些阻塞方法会抛出InterruptedException异常,可以在异常处理代码中进行相应的处理,例如终止线程的执行。
手动设置中断状态:通过调用Thread类的实例方法interrupt(),可以手动设置线程的中断状态为true。这样,在线程的执行代码中可以根据中断状态进行相应的处理,例如退出循环或终止线程。
使用Thread.interrupt()方法中断线程:可以通过调用Thread类的实例方法interrupt()来中断线程。这会将线程的中断状态设置为true。在线程的执行代码中,可以通过检查中断状态来决定是否终止线程的执行。
需要注意的是,中断只是一种请求,线程是否真正中断还取决于线程的具体实现。在处理中断时,需要合理处理中断请求,例如在捕获InterruptedException异常后,可以选择继续执行或终止线程的执行。
另外,需要注意的是,在处理中断时应该避免使用System.exit()方法来终止整个程序,而应该通过合适的方式来终止线程的执行,以保证程序的正常退出。
在Java中,Thread类提供了三个方法来处理线程的中断状态,分别是interrupt()、interrupted()和isInterrupted()方法。它们之间的区别如下:
interrupt()方法:这是一个实例方法,用于中断线程。当调用interrupt()方法时,它会将线程的中断状态设置为true。如果线程正在阻塞状态(如调用了sleep()、wait()、join()等方法),那么会抛出InterruptedException异常,同时将中断状态重置为false。如果线程没有处于阻塞状态,那么只是将中断状态设置为true。interrupt()方法通常用于请求中断线程,但具体是否中断还需要线程自身进行处理。
interrupted()方法:这是一个静态方法,用于检查当前线程的中断状态,并清除中断状态。当调用interrupted()方法时,它会返回当前线程的中断状态,并将中断状态重置为false。如果当前线程的中断状态为true,则返回true;否则返回false。interrupted()方法通常用于检查其他线程是否请求了中断,并且可以在适当的时候决定是否终止当前线程的执行。
isInterrupted()方法:这是一个实例方法,用于检查线程对象的中断状态,但不会清除中断状态。当调用isInterrupted()方法时,它会返回线程对象的中断状态,如果中断状态为true,则返回true;否则返回false。与interrupted()方法不同的是,isInterrupted()方法不会改变线程的中断状态,它只是返回当前中断状态的值。isInterrupted()方法通常用于在线程的执行代码中检查中断状态,并根据需要决定是否终止线程的执行。
需要注意的是,interrupted()方法是一个静态方法,调用它会影响到当前线程的中断状态。而isInterrupted()方法是一个实例方法,调用它只会检查线程对象的中断状态,不会改变中断状态。
另外,interrupted()方法和isInterrupted()方法都不会抛出InterruptedException异常。它们只是用来查询线程的中断状态。
在Java中,可以使用以下几种方式来唤醒一个阻塞的线程:
notify()方法:当一个线程调用了某个对象的notify()方法时,会唤醒等待在该对象上的一个线程(选择性地唤醒)。被唤醒的线程将从等待状态进入到就绪【可运行】状态,但不一定会立即执行,需要等待获取到对象的锁才能继续执行。
notifyAll()方法:当一个线程调用了某个对象的notifyAll()方法时,会唤醒等待在该对象上的所有线程。所有被唤醒的线程将从等待状态进入到就绪【可运行】状态,但同样需要等待获取到对象的锁才能继续执行。
需要注意的是,notify()和notifyAll()方法只能在同步代码块或同步方法中调用,并且调用者必须是对象的锁的持有者。这是因为这两个方法会释放对象的锁,让其他线程有机会获取到锁并执行。
另外,被唤醒的线程需要与唤醒它的线程竞争锁。如果被唤醒的线程获取到了锁,它将继续执行;如果没有获取到锁,它将继续等待。
需要注意的是,唤醒一个阻塞的线程并不意味着它会立即执行,执行的先后顺序还取决于线程调度器的调度策略。
在Java中,可以通过以下几种方式在两个线程之间共享数据:
1. 共享对象:创建一个对象,在两个线程中引用该对象,并通过对象的成员变量来实现数据共享。线程可以通过对该对象的成员变量进行读写操作来实现数据的共享。例如:
- class SharedData {
- public int sharedValue;
- }
-
- class MyThread implements Runnable {
- private SharedData sharedData;
-
- public MyThread(SharedData sharedData) {
- this.sharedData = sharedData;
- }
-
- public void run() {
- sharedData.sharedValue = 10; // 写操作
- System.out.println(sharedData.sharedValue); // 读操作
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- SharedData sharedData = new SharedData();
- Thread thread1 = new Thread(new MyThread(sharedData));
- Thread thread2 = new Thread(new MyThread(sharedData));
- thread1.start();
- thread2.start();
- }
- }
在上述例子中,通过共享一个`SharedData`对象,两个线程可以对`sharedValue`进行读写操作来实现数据的共享。
2. 使用共享容器:使用线程安全的容器(如`ConcurrentHashMap`、`CopyOnWriteArrayList`等)来存储共享的数据。这些容器提供了线程安全的操作方法,可以在多个线程之间共享数据。例如:
- import java.util.concurrent.ConcurrentHashMap;
-
- class MyThread implements Runnable {
- private ConcurrentHashMap<String, Integer> sharedMap;
-
- public MyThread(ConcurrentHashMap<String, Integer> sharedMap) {
- this.sharedMap = sharedMap;
- }
-
- public void run() {
- sharedMap.put("key", 10); // 写操作
- System.out.println(sharedMap.get("key")); // 读操作
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- ConcurrentHashMap<String, Integer> sharedMap = new ConcurrentHashMap<>();
- Thread thread1 = new Thread(new MyThread(sharedMap));
- Thread thread2 = new Thread(new MyThread(sharedMap));
- thread1.start();
- thread2.start();
- }
- }
在上述例子中,通过共享一个`ConcurrentHashMap`对象,两个线程可以对其中的数据进行读写操作来实现数据的共享。
需要注意的是,在多线程环境下进行数据共享时,需要考虑线程安全性,避免出现竞态条件和数据不一致的问题。可以通过使用同步机制(如`synchronized`关键字、`Lock`接口等)来保证数据的一致性和线程安全性。
在Java中,可以使用以下几种方式实现多线程之间的通信和协作:
1. 使用共享对象的wait()和notify()方法:通过共享对象的wait()和notify()方法来实现线程之间的通信和协作。一个线程可以调用共享对象的wait()方法使自己进入等待状态,同时释放对象的锁;另一个线程可以调用共享对象的notify()方法来唤醒等待的线程。例如:
- class SharedObject {
- private boolean flag = false;
-
- public synchronized void waitForFlag() {
- while (!flag) {
- try {
- wait(); // 等待flag为true
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println("Flag is now true");
- }
-
- public synchronized void setFlag() {
- flag = true;
- notify(); // 唤醒等待的线程
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- SharedObject sharedObject = new SharedObject();
-
- Thread thread1 = new Thread(() -> sharedObject.waitForFlag());
- Thread thread2 = new Thread(() -> sharedObject.setFlag());
-
- thread1.start();
- thread2.start();
- }
- }
在上述例子中,线程1等待flag为true,线程2设置flag为true并唤醒等待的线程1。
2. 使用线程间的阻塞队列:通过使用线程间的阻塞队列(如`BlockingQueue`)来实现线程之间的通信和协作。一个线程可以将数据放入阻塞队列,另一个线程可以从队列中取出数据进行处理。例如:
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
-
- class Producer implements Runnable {
- private BlockingQueue<Integer> queue;
-
- public Producer(BlockingQueue<Integer> queue) {
- this.queue = queue;
- }
-
- public void run() {
- try {
- for (int i = 0; i < 5; i++) {
- Thread.sleep(1000);
- queue.put(i); // 将数据放入队列
- System.out.println("Produced: " + i);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
-
- class Consumer implements Runnable {
- private BlockingQueue<Integer> queue;
-
- public Consumer(BlockingQueue<Integer> queue) {
- this.queue = queue;
- }
-
- public void run() {
- try {
- for (int i = 0; i < 5; i++) {
- int num = queue.take(); // 从队列中取出数据
- System.out.println("Consumed: " + num);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
-
- public class Main {
- public static void main(String[] args) {
- BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
-
- Thread producerThread = new Thread(new Producer(queue));
- Thread consumerThread = new Thread(new Consumer(queue));
-
- producerThread.start();
- consumerThread.start();
- }
- }
在上述例子中,生产者线程将数据放入阻塞队列,消费者线程从队列中取出数据进行处理。
需要注意的是,在多线程之间进行通信和协作时,需要考虑线程安全性和同步机制,以避免出现竞态条件和数据不一致的问题。
在Java中,notify()
、notifyAll()
、sleep()
和wait()
是用于多线程编程的关键字和方法,它们有以下区别:
notify()和notifyAll():
notify()
方法用于唤醒在共享对象上等待的单个线程。如果有多个线程在等待,那么只有其中一个线程会被唤醒,但是具体唤醒哪个线程是不确定的。notifyAll()
方法用于唤醒在共享对象上等待的所有线程,让它们竞争对象的锁。sleep()和wait():
sleep()
方法是Thread
类的静态方法,用于使当前线程休眠指定的时间。线程在休眠期间不会释放对象的锁,其他线程无法访问该对象。wait()
方法是Object
类的方法,用于使当前线程进入等待状态,直到其他线程调用相同对象的notify()
或notifyAll()
方法来唤醒等待的线程。线程在等待期间会释放对象的锁,让其他线程能够访问该对象。需要注意的是,wait()
和notify()
方法必须在同步代码块或同步方法中调用,并且针对同一个对象进行操作。否则会抛出IllegalMonitorStateException
异常。
总结:
notify()
和notifyAll()
用于线程之间的通信,通过唤醒等待的线程来实现协作。sleep()
用于使当前线程休眠,不会释放对象的锁。wait()
用于使当前线程进入等待状态,并释放对象的锁,直到被其他线程唤醒。同步块是更好的选择,同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在 Java 虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer ,它是一个线程安全的类,自然最常用的 append() 方法是一个同步方法,我们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁 -> 解锁,这对性能不利,因为这意味着 Java 虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此 Java 虚拟机会将多次 调用append 方法的代码进行一个锁粗化的操作,将多次的 append 的操作扩展到 append 方法的头尾,变成一个大的同步块,这样就减少了加锁 --> 解锁的次数,有效地提升了代码执行的效率。
线程同步和线程互斥是多线程编程中的两个重要概念。
线程同步是指多个线程按照一定的顺序执行共享资源,以避免并发访问导致的数据不一致或错误的问题。线程同步可以保证共享资源在同一时刻只被一个线程访问,从而避免竞态条件和数据冲突。
线程互斥是指多个线程之间相互排斥,即同一时刻只允许一个线程执行临界区代码。线程互斥可以通过锁机制来实现,以确保同一时刻只有一个线程能够访问临界资源。
骚戴理解:这里不要和同步异步搞混淆了
区别:
线程同步的实现方式有以下几种:
synchronized
关键字来实现线程同步。这种方式是最常见和简单的实现方式。Lock
接口及其实现类(如ReentrantLock
),通过获取锁和释放锁来实现线程同步。AtomicInteger
、AtomicBoolean
等),这些类提供了原子操作,可以实现线程安全的同步。Semaphore
)来控制同时访问某个资源的线程数量。Condition
接口提供了条件变量的支持,可以通过条件变量实现线程的等待和唤醒机制。选择合适的线程同步方式取决于具体的需求和场景,需要综合考虑性能、复杂度、可读性等因素。
在监视器(Monitor)内部,线程同步是通过使用锁(Lock)来实现的。锁是一种同步机制,用于保护共享资源,确保在同一时刻只有一个线程可以访问临界区(Critical Section)代码。
在Java中,监视器是由synchronized关键字实现的。当一个线程进入一个被synchronized关键字修饰的代码块或方法时,它会尝试获取锁。如果锁没有被其他线程占用,该线程将获得锁并进入临界区执行代码。其他线程在尝试进入同一个临界区时,会被阻塞,直到锁被释放。
当线程执行完临界区的代码后,会释放锁,让其他线程有机会获取锁并执行临界区。这样就保证了在同一时刻只有一个线程能够进入临界区,从而实现了线程的同步。
在监视器内部,锁的获取和释放是由系统自动管理的,开发者无需手动控制。当线程进入synchronized代码块或方法时,会自动获取锁;当线程执行完synchronized代码块或方法后,会自动释放锁。
需要注意的是,监视器的锁是可重入的,即同一个线程可以多次获取同一个锁,而不会发生死锁。这个特性使得线程可以在同一个监视器内部嵌套调用同步方法,而不会被阻塞。
在Spring MVC中,Controller默认是单例的,即每个Controller类的实例在整个应用程序中只有一个。因此,如果Controller类的实例变量没有状态(即不包含任何可变的实例变量),那么它可以被认为是线程安全的。
由于Controller是单例的,多个线程可能会同时访问同一个Controller实例的方法。如果Controller中包含可变的实例变量,那么多个线程同时修改这些变量可能会导致数据不一致或其他并发问题。
为了确保Controller的线程安全性,可以采取以下措施:
1. 避免在Controller中使用可变的实例变量,尽量将状态保存在方法的局部变量中。
2. 如果必须使用实例变量,确保对它们的访问是线程安全的,可以使用同步机制(如synchronized关键字)或使用线程安全的数据结构(如ConcurrentHashMap)来保护共享状态。
3. 尽量避免在Controller中使用全局共享的资源,以减少并发访问的潜在问题。
需要根据具体的业务需求和场景来评估Controller的线程安全性,并采取适当的措施来保证线程安全性。
线程安全问题是多线程编程中常见的问题,主要体现在以下几个方面:
竞态条件(Race Condition):当多个线程同时访问和修改共享资源时,由于执行顺序的不确定性,可能导致数据的不一致或错误的结果。
数据竞争(Data Race):多个线程同时读写共享的可变数据,由于缺乏同步机制,可能导致数据的不一致性和不可预测的结果。
死锁(Deadlock):当多个线程相互等待对方释放资源时,导致所有线程都无法继续执行,从而陷入无限等待的状态。
活锁(Livelock):类似于死锁,但不同之处在于线程不会被阻塞,而是一直在重复相同的操作,导致无法继续进行有意义的工作。
这些线程安全问题的主要原因包括:
解决线程安全问题的常见方案包括:
解决线程安全问题需要根据具体情况选择合适的方法和技术,同时需要对多线程编程的原理和机制有一定的了解。
在Java程序中,可以采取以下几种方式来保证多线程的运行安全:
使用同步机制:使用synchronized
关键字或Lock
接口来保护共享资源的访问。通过在关键代码块或方法上加锁,确保同一时间只有一个线程可以执行该代码块或方法,避免竞态条件和数据竞争。
使用原子操作:使用原子变量类(如AtomicInteger
、AtomicBoolean
等)来进行原子操作,避免数据竞争。原子操作是不可分割的,保证了操作的完整性和一致性。
使用线程安全的数据结构:使用线程安全的数据结构(如ConcurrentHashMap
、CopyOnWriteArrayList
等),它们在内部实现了同步机制,可以避免手动处理同步问题。
使用volatile关键字:使用volatile
关键字来保证变量的可见性。volatile
关键字确保了变量的修改对所有线程可见,避免了线程之间的数据不一致性。
使用线程安全的库和框架:选择使用已经经过测试和验证的线程安全的库和框架,这些库和框架已经实现了线程安全的机制,可以减少手动处理线程安全问题的工作量。
合理设计对象的可见性:通过使用volatile
关键字或使用线程安全的发布和初始化机制,确保对象的可见性和正确的初始化。
避免死锁和活锁:合理设计锁的获取和释放顺序,避免线程之间的相互等待和无限循环。
需要根据具体的业务需求和场景来评估选择合适的方法和技术,同时需要对多线程编程的原理和机制有一定的了解。同时,多线程的运行安全也需要进行充分的测试和验证,确保程序在并发环境下的正确性。
线程优先级是操作系统调度线程执行的一个指标,用于确定在竞争CPU资源时,哪个线程会被优先执行。每个线程都有一个优先级,范围从1到10,其中1是最低优先级,10是最高优先级。
线程优先级的作用是给操作系统一个提示,希望高优先级的线程能够更频繁地被调度执行。然而,线程优先级并不能保证绝对的执行顺序,操作系统可能会根据自身的调度算法和策略进行调整。
需要注意的是,线程优先级的具体行为和效果可能因操作系统和硬件平台而异。不同的操作系统可能对线程优先级的处理方式有所不同,而且线程优先级的设置并不能完全控制线程的执行顺序。
在实际应用中,应该谨慎使用线程优先级,避免过度依赖线程优先级来控制程序的行为。过度依赖线程优先级可能导致程序在不同的操作系统和平台上表现不一致,而且过分依赖线程优先级可能会导致其他线程饥饿的问题。
总之,线程优先级是一个操作系统提供的机制,用于给线程提供一个相对的执行优先级提示。但在编写多线程应用时,应该通过合理的设计和同步机制来确保程序的正确性和可靠性,而不是过度依赖线程优先级。
请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
Dump文件(也称为堆转储文件)是一种用于存储程序内存状态的文件。它包含了应用程序在某个特定时间点的内存快照,包括堆、栈和线程等信息。Dump文件通常用于分析应用程序的崩溃、内存泄漏、死锁等问题,以帮助开发人员进行故障排查和性能优化。
在Java中,可以使用以下方式获取线程堆栈信息:
Thread.currentThread()
获取当前线程对象,然后使用getStackTrace()
方法获取该线程的堆栈跟踪信息。例如:- Thread thread = Thread.currentThread();
- StackTraceElement[] stackTrace = thread.getStackTrace();
ManagementFactory
类获取ThreadMXBean
对象,然后使用getThreadInfo(threadId)
方法获取指定线程的详细信息,包括堆栈跟踪。例如:- ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
- ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
- StackTraceElement[] stackTrace = threadInfo.getStackTrace();
需要注意的是,以上方法只能获取当前线程的堆栈信息。如果需要获取其他线程的堆栈信息,可以通过线程ID(Thread ID)来获取。
获取到的堆栈信息通常以StackTraceElement
对象数组的形式返回,每个StackTraceElement
对象表示一帧的堆栈信息,包括类名、方法名、文件名和行号等。
通过获取线程堆栈信息,可以帮助我们定位问题发生的位置,分析线程执行的路径和调用关系,以便进行问题排查和调优。
当一个线程运行时发生异常,如果该线程没有进行异常处理,那么以下两种情况可能会发生:
线程终止:线程会立即终止,并且不会继续执行后续代码。异常会被传播到线程的顶层,如果没有进行捕获和处理,那么线程会被终止,并且异常信息会被输出到控制台或日志中。
线程异常处理:如果在线程中进行了异常处理,通过try-catch
语句捕获了异常,那么线程可以继续执行后续代码。在异常处理块中,可以根据具体的业务需求进行相应的处理,例如记录日志、回滚事务、重新尝试等。
需要注意的是,线程中的异常默认情况下不会影响其他线程的正常执行。每个线程都有自己的异常处理机制,一个线程的异常不会直接影响到其他线程的运行。然而,如果异常导致共享资源的状态不一致或产生其他问题,那么其他线程可能会受到影响。
为了保证程序的稳定性和可靠性,建议在多线程应用程序中对线程进行适当的异常处理,以避免未处理的异常导致程序崩溃或产生不可预料的结果。同时,及时记录和分析线程的异常信息,有助于定位和解决问题。
当Java线程数过多时,可能会导致以下异常或问题:
OutOfMemoryError: 当线程数过多时,每个线程都需要一定的内存资源,包括线程栈空间和线程相关的数据结构。如果系统的可用内存资源不足以支持创建更多的线程,就会抛出OutOfMemoryError异常。
CPU过载: 每个线程都需要CPU资源来执行任务。当线程数过多时,CPU可能无法及时处理所有线程的任务,导致CPU过载。这会导致系统的响应性能下降,甚至可能导致系统崩溃。
上下文切换开销增加: 当线程数过多时,操作系统需要频繁地进行线程切换(上下文切换),以便为每个线程分配CPU时间片。上下文切换是一项开销较大的操作,当线程数过多时,上下文切换的开销会增加,导致系统的整体性能下降。
竞争和死锁: 过多的线程可能导致共享资源的竞争和死锁问题。当多个线程同时访问共享资源时,可能会发生竞争条件,导致数据不一致或错误的结果。此外,如果线程之间存在死锁情况,即互相等待对方释放资源,那么系统将无法继续执行。
为了避免以上问题,需要合理地管理和控制线程数。在设计多线程应用程序时,应根据系统的硬件资源、业务需求和性能要求等因素,合理地分配和调整线程数。可以通过线程池来管理线程,限制线程数,并且可以复用线程,减少线程的创建和销毁开销。同时,需要合理设计线程同步机制,避免竞争和死锁问题的发生。
代码重排序是指编译器或处理器在不改变程序执行结果的前提下,对指令的执行顺序进行重新排序的优化技术。代码重排序的目的是提高程序的性能和执行效率。
代码重排序可能发生的原因包括:
编译器优化: 编译器在生成目标代码时,可能会对代码进行优化,包括指令的重排。编译器优化的目标是生成更高效的代码,减少指令之间的依赖关系,提高指令的并行度。
处理器优化: 处理器在执行指令时,也可能会对指令进行重排,以提高执行效率。处理器可以使用多级流水线、乱序执行等技术来重排指令,以充分利用处理器的并行能力和资源。
内存模型优化: 在多线程环境下,为了保证程序的正确性和一致性,Java内存模型(Java Memory Model)定义了一系列规则和约束。然而,为了提高性能,编译器和处理器可能会对内存操作进行重排序,但需要遵循内存模型的规则,保证多线程程序的正确执行。
需要注意的是,代码重排序是在保证程序语义不变的前提下进行的。也就是说,重排序不会改变单线程程序的执行结果。然而,在多线程环境下,代码重排序可能会影响多线程程序的正确性,因为重排序可能会改变线程间的依赖关系和内存操作的顺序。为了避免多线程程序中的重排序问题,可以使用同步机制(如锁、volatile关键字、原子操作等)来保证线程间的可见性和有序性。
as-if-serial
规则和happens-before
规则是Java内存模型(Java Memory Model)中定义的两个重要规则,用于保证多线程程序的正确性和一致性。
as-if-serial规则: as-if-serial
规则是指在单线程环境下,不管如何重排序,程序的执行结果必须与按照程序顺序执行的结果一致。也就是说,编译器和处理器可以对指令进行重排序,以提高执行效率,但不能改变程序的执行结果。这个规则保证了单线程程序的语义不变。
happens-before规则: happens-before
规则是用于定义多线程程序中的操作之间的顺序关系。如果操作A happens-before操作B,那么操作A的结果对操作B可见,且操作A的执行顺序在操作B之前。happens-before
规则提供了一种保证多线程程序正确性的机制,确保线程间的操作按照预期的顺序执行。
两者的区别在于作用范围和目的:
as-if-serial
规则针对的是单线程环境下的指令重排,保证单线程程序的执行结果与按照程序顺序执行的结果一致。
happens-before
规则针对的是多线程环境下的操作之间的顺序关系,保证多线程程序的操作按照预期的顺序执行,避免数据竞争和不确定性。
需要注意的是,happens-before
规则提供了一种偏序关系,即可以确定某些操作的顺序关系,但并不是所有操作之间都有happens-before
关系。对于没有happens-before
关系的操作,它们的执行顺序是不确定的,可能会发生重排序或并发执行。因此,在编写多线程程序时,必须遵守happens-before
规则,使用同步机制来保证操作之间的有序性和可见性。
LockSupport
是Java并发包中的一个工具类,用于实现线程的阻塞和唤醒操作。它提供了一种基于线程的阻塞原语,可以用于实现自定义的线程同步机制。
LockSupport
的主要方法有:
park()
:阻塞当前线程,使其进入等待状态。如果调用park()
方法的线程已经被中断,则会立即返回。
park(Object blocker)
:与park()
方法类似,但可以指定一个blocker对象,用于记录阻塞的原因。这个blocker对象在调试和监控工具中可以看到。
unpark(Thread thread)
:唤醒指定线程,使其从等待状态返回。
LockSupport
的特点和使用方式如下:
无需先获得锁: 与Object
类中的wait()
和notify()
方法不同,LockSupport
可以在任何时候调用,无需先获得锁对象。
线程阻塞和唤醒: park()
方法可以阻塞当前线程,使其等待某个条件满足;unpark()
方法可以唤醒指定线程,使其从等待状态返回。
不会导致死锁: LockSupport
不会导致线程死锁,即使多次调用unpark()
方法也不会有副作用。
支持线程中断: 调用park()
方法的线程可以被中断,如果线程已经被中断,则调用park()
方法会立即返回。
支持blocker对象: 可以使用park(Object blocker)
方法指定一个blocker对象,用于记录阻塞的原因。
LockSupport
通常与其他同步工具(如ReentrantLock
、Condition
等)配合使用,用于实现更复杂的线程同步和通信机制。它提供了一种灵活而底层的线程阻塞和唤醒的方式,可以满足各种线程同步的需求。
当使用`LockSupport`时,我们可以通过调用`park()`方法阻塞当前线程,并通过调用`unpark(Thread thread)`方法唤醒指定线程。下面是一个简单的示例:
- import java.util.concurrent.locks.LockSupport;
-
- public class LockSupportExample {
- public static void main(String[] args) {
- Thread mainThread = Thread.currentThread(); // 获取当前线程
-
- Thread thread = new Thread(() -> {
- try {
- Thread.sleep(2000); // 线程休眠2秒
- LockSupport.unpark(mainThread); // 唤醒主线程
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- thread.start();
-
- System.out.println("Main thread is blocked");
- LockSupport.park(); // 阻塞当前线程
-
- System.out.println("Main thread is resumed");
- }
- }
在上述示例中,我们创建了一个子线程,该线程会休眠2秒后调用`LockSupport.unpark()`方法唤醒主线程。主线程在调用`LockSupport.park()`方法后会被阻塞,直到被子线程唤醒。
输出结果:
Main thread is blocked
Main thread is resumed
通过调用`LockSupport.park()`方法,主线程被阻塞,直到子线程调用`LockSupport.unpark()`方法唤醒主线程,主线程才会继续执行。
需要注意的是,`LockSupport`是一种底层的线程阻塞和唤醒机制,通常与其他同步工具(如`ReentrantLock`、`Condition`等)结合使用,用于实现更复杂的线程同步和通信机制。
线程局部变量(Thread Local Variable)是指每个线程都拥有自己独立的变量副本,互不干扰。每个线程可以通过访问和修改自己的变量副本,而不会影响其他线程的副本。
线程局部变量的特点如下:
线程隔离: 每个线程都有自己独立的变量副本,不同线程之间的变量互不干扰。这样可以避免多线程环境下的数据共享和竞争条件。
线程安全: 由于每个线程都拥有自己的变量副本,因此在多线程环境下,每个线程都可以独立地访问和修改自己的变量副本,不需要进行额外的同步措施,从而提供了一种线程安全的方式。
简化编程模型: 线程局部变量可以简化编程模型,避免了显式的线程同步和共享变量的复杂性。每个线程可以独立地操作自己的变量副本,提供了一种更加简单和可靠的编程方式。
线程局部变量通常通过ThreadLocal
来实现,ThreadLocal
是Java中的一个工具类,用于在每个线程内部维护自己的变量副本。每个线程可以通过ThreadLocal
对象来访问和修改自己的变量副本,而不会影响其他线程的副本。
线程局部变量在多线程编程中有很多应用场景,如线程封闭、线程上下文传递、线程池和异步任务等。它可以提供一种简单、安全和高效的方式来处理线程间的数据隔离和共享问题。
ThreadLocal
是Java中的一个线程局部变量工具类。它提供了一种机制,可以让每个线程都拥有自己独立的变量副本,互不干扰。每个线程可以通过ThreadLocal
对象来访问和修改自己的变量副本,而不会影响其他线程的副本。
ThreadLocal
的原理是通过在每个线程内部维护一个ThreadLocalMap
对象来存储变量副本。ThreadLocalMap
是ThreadLocal
的内部类,它是一个以ThreadLocal
对象为键、变量副本为值的哈希表。每个线程在访问ThreadLocal
时,实际上是通过当前线程的ThreadLocalMap
来获取和修改变量副本。
当一个线程访问ThreadLocal
时,它首先会获取当前线程的ThreadLocalMap
对象。如果该ThreadLocal
对象尚未在ThreadLocalMap
中存在,则会通过ThreadLocal
对象的initialValue()
方法创建一个初始值,并将其存储在ThreadLocalMap
中。如果该ThreadLocal
对象已经在ThreadLocalMap
中存在,则直接获取和修改对应的变量副本。
ThreadLocal
的主要使用场景包括:
线程封闭(Thread Confinement): ThreadLocal
可以将某个对象与当前线程绑定,使得每个线程都拥有自己独立的对象副本。这在多线程环境下可以实现线程封闭,避免线程间的数据共享和竞争条件。
线程上下文传递(Thread Context Passing): 在一些框架和库中,需要将一些上下文信息(如用户身份、请求信息等)在多个方法或组件之间传递。通过ThreadLocal
可以将这些上下文信息与当前线程绑定,方便在任何地方访问和修改。
线程池和异步任务: 在使用线程池执行任务时,每个任务都可能在不同的线程上执行。通过ThreadLocal
可以在任务执行过程中维护一些线程私有的状态或上下文信息,而不会受到线程切换的影响。
需要注意的是,由于ThreadLocal
中存储的变量副本与线程绑定,因此在使用完毕后需要及时清理,避免内存泄漏。可以通过调用ThreadLocal
的remove()
方法来清理线程的变量副本。
在使用ThreadLocal
时,如果不注意及时清理线程的变量副本,就可能导致内存泄漏。这是因为ThreadLocal
中使用的变量副本是与线程绑定的,如果线程一直存在而没有被销毁,那么对应的变量副本也会一直存在,无法被垃圾回收。
下面是分析和解决ThreadLocal
内存泄漏的一般步骤:
分析内存泄漏原因: 首先需要确定是否存在ThreadLocal
内存泄漏问题。可以通过检查ThreadLocal
对象是否被长时间引用,以及对应的线程是否一直存在而没有被销毁,来判断是否有内存泄漏。
查找引起内存泄漏的根源: 如果确定存在ThreadLocal
内存泄漏问题,需要找到引起内存泄漏的根源。通常是因为在使用ThreadLocal
的代码中,没有在合适的时机调用ThreadLocal
的remove()
方法来清理变量副本。
修复内存泄漏问题: 一旦找到了引起内存泄漏的根源,可以采取以下解决方案来修复内存泄漏问题:
在使用完ThreadLocal
后,及时调用ThreadLocal
的remove()
方法来清理线程的变量副本。可以通过在finally
块中调用remove()
方法来确保清理操作一定会执行。
使用ThreadLocal
的时候,可以将其定义为static
,并在不再使用时手动调用remove()
方法进行清理,避免长时间引用和泄漏。
通过使用InheritableThreadLocal
代替ThreadLocal
,可以确保子线程继承父线程的变量副本,避免创建新的副本,从而减少内存泄漏的可能性。
使用弱引用(WeakReference
)来包装ThreadLocal
对象,这样当ThreadLocal
对象没有被其他强引用持有时,可以被垃圾回收,进而清理对应的变量副本。
使用第三方库或框架提供的ThreadLocal
实现,这些实现通常会自动处理内存泄漏问题,或提供更好的内存泄漏检测和清理机制。
需要注意的是,修复ThreadLocal
内存泄漏问题需要根据具体的使用场景和代码逻辑进行分析和处理。合理地管理ThreadLocal
的生命周期,及时清理变量副本,可以避免内存泄漏问题的发生。
在Java中,有几种常见的锁机制可供选择,包括:
1. synchronized关键字:synchronized关键字是Java内置的锁机制,可以用于修饰方法或代码块。它使用的是对象级别的锁,也称为内置锁或监视器锁。
2. ReentrantLock类:ReentrantLock是Java提供的可重入锁,它提供了比synchronized更灵活的锁机制。ReentrantLock可以实现公平锁和非公平锁,并提供了更多的高级功能,如可中断锁、超时锁等。
3. ReadWriteLock接口:ReadWriteLock是Java提供的读写锁机制,它允许多个线程同时读取共享数据,但在写入时需要独占锁。ReadWriteLock接口的实现类是ReentrantReadWriteLock。
4. Lock接口:Lock接口是Java提供的通用锁机制,它定义了锁的基本操作,如获取锁、释放锁等。除了ReentrantLock,Java还提供了其他实现了Lock接口的锁类,如StampedLock、Condition等。
5. AtomicInteger类:AtomicInteger是Java提供的原子类,它可以实现线程安全的原子操作,避免了使用锁的开销。AtomicInteger类提供了一些原子操作方法,如getAndIncrement、compareAndSet等。
这些锁机制都可以用于实现线程安全的代码,根据具体的需求和场景选择合适的锁机制。在使用锁时,需要注意锁的范围和粒度,以及避免死锁和活锁等并发问题。
i、乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
ii、悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
i、共享锁
共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
Java中用到的共享锁: ReentrantReadWriteLock的读锁。
ii、独享锁(独占锁)
独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。对于Synchronized而言,当然是独享锁。
Java中用到的独占锁: synchronized,ReentrantLock
上面讲的独享锁/共享锁就是一种概念,互斥锁/读写锁是具体的实现
i、互斥锁(独占锁的实现)
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源
互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock是JDK1.5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,更加灵活。
ii、读写锁(共享锁的实现)
读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。
读锁: 允许多个线程获取读锁,同时访问同一个资源。
写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
读写锁在Java中的具体实现就是ReadWriteLock
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
广义上的可重入锁指的是可重复可递归调用的锁,任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,例如在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。可重入锁的作用是避免死锁,ReentrantLock和synchronized都是可重入锁
实现原理
每一个可重入锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功或获取锁后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增1;当线程退出同步代码块时,计数器会递减1,如果计数器为 0,则释放该锁。
分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。
线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。
JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
i、重量级锁
重量级锁是一种称谓: 这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized
iii、轻量级锁
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
iii、偏向锁
研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
volatile
关键字在Java中用于修饰变量,其作用是保证变量的可见性和禁止指令重排序。
可见性: 当一个变量被声明为volatile
时,线程在修改该变量的值后,会立即将修改后的值刷新到主内存中,并且在读取该变量时,会直接从主内存中获取最新的值,而不是使用线程的工作内存中的副本。这样可以保证多个线程对该变量的修改和读取都能看到最新的值,从而实现了可见性。
禁止指令重排序: 在多线程环境下,为了提高性能,编译器和处理器可能会对指令进行重排序。但是对于被声明为volatile
的变量,编译器和处理器会遵守特定的规则,禁止对其进行重排序。这样可以确保volatile
变量的读写操作按照程序的顺序执行,避免了可能的数据竞争和不一致性。
volatile
关键字的原理是通过内存屏障(Memory Barrier)来实现的。内存屏障是一种硬件或软件层面的机制,用于确保指令的执行顺序和内存的可见性。在读取或写入volatile
变量时,会插入相应的内存屏障,保证变量的修改和读取操作不会受到指令重排序的影响,并且能够及时同步到主内存和其他线程中。
需要注意的是,虽然volatile
关键字可以保证可见性和禁止指令重排序,但它并不能保证原子性。对于复合操作,如i++
这样的操作,仍然需要额外的同步手段,如synchronized
或java.util.concurrent.atomic
包下的原子类。
保证变量的内存可见性的实现原理
当对volatile修饰的共享变量执行写操作后,JMM会把工作内存(本地内存)中的最新变量值强制刷新到主内存,并且通过 CPU 总线嗅探机制( CPU 总线嗅探机制其实就是一种用来实现缓存一致性的常见机制)告知其他线程该变量副本已经失效,需要重新从主内存中读取。这样volatile就保证了不同线程对共享变量操作的可见性
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
可见性
多个线程共同访问共享变量时,某个线程修改了此变量,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值(要看懂这句话就要知道JMM模型)
Java 内存模型
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(也叫工作内存),本地内存中存储了该线程以读/写共享变量的副本。
JMM 的规定
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
JMM 的抽象示意图
然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。正因为 JMM 这样的机制,就出现了可见性问题。
禁止指令重排序的实现原理
使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。说白了就是靠这个内存屏障来实现volatile 禁止指令重排序
什么是指令重排序?
为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令重新排序,也就是不一定会根据编译时的指令顺序(可以理解为不会按照你写代码的顺序从上到下的依次执行),而是它自己会重新排序以达到提高性能的目的
什么是volatile 重排序规则表?
JMM 针对编译器制定了 volatile 重排序规则表,JMM 会限制特定类型的编译器和处理器重排序。如下所示:
其实就是JMM为了实现volatile 禁止指令重排序这个功能针对编译器和处理器制定的规则来限制特定类型的编译器和处理器重排序
什么是内存屏障指令?
内存屏障指令是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
JMM 把内存屏障指令分为下列四类:
StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。
下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:
从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:
也就是说volatile 禁止指令重排序就是靠上面两条内存屏障规则来实现的,这样编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
可重入锁(Reentrant Lock)是一种支持重复进入的锁机制,也称为递归锁。它允许同一个线程在持有锁的情况下,多次进入同步代码块或方法,而不会被自己所持有的锁所阻塞。
可重入锁的主要目的是解决线程在递归调用或多层嵌套调用中对同步代码的重复加锁的问题。在使用可重入锁时,线程可以重复获取已经持有的锁,而不会造成死锁或其他异常情况。
可重入锁的实现原理是通过给每个锁关联一个线程持有者和一个计数器来实现的。当线程第一次获取锁时,计数器加一,并将锁的持有者设置为当前线程。当同一个线程再次获取锁时,计数器再次加一,而不会阻塞。只有当线程释放锁时,计数器才会递减。只有计数器为0时,其他线程才能获取该锁。
在可重入锁的实现中,还需要考虑线程的等待队列和唤醒机制。当一个线程无法获取锁时,会被放入等待队列中,并进入等待状态。当锁被释放时,会从等待队列中选择一个线程唤醒,使其能够继续执行。
可重入锁的实现可以使用多种方式,如使用内置的`synchronized`关键字、`ReentrantLock`类等。无论使用哪种方式,可重入锁的核心原理都是通过线程持有者和计数器来实现重复进入的控制。这样可以确保线程在递归调用或多层嵌套调用中,能够正确地获取和释放锁,避免死锁和其他并发问题的发生。
synchronized
关键字是Java中用于实现线程同步的一种机制,它可以修饰方法或代码块。
synchronized
关键字的底层实现原理涉及到Java对象头和监视器锁(Monitor Lock)。
Java对象头: 每个Java对象在内存中都有一个对象头,用于存储对象的元数据信息,包括锁状态、哈希码、GC标记等。对象头的一部分用于存储锁信息。
监视器锁: 在Java中,每个对象都可以关联一个监视器锁,也称为内置锁或对象锁。当一个线程需要进入synchronized
修饰的方法或代码块时,它必须先获取对象的监视器锁。
synchronized
关键字的底层实现原理如下:
进入同步块: 当一个线程需要进入synchronized
修饰的方法或代码块时,它首先会尝试获取对象的监视器锁。
获取锁: 如果对象的监视器锁没有被其他线程占用,那么当前线程将获取到锁,可以进入同步块执行。此时,对象的锁状态会被置为"锁定"。
锁占用和释放: 如果对象的监视器锁已经被其他线程占用,那么当前线程将进入阻塞状态,等待锁的释放。当占用锁的线程执行完同步块后,会释放锁,唤醒等待的线程。
线程唤醒: 当一个线程释放锁时,它会通过唤醒机制(如notify或notifyAll)通知等待队列中的线程,使其中的一个或多个线程被唤醒,竞争获取锁。
需要注意的是,synchronized
关键字在底层的实现中,会自动进行锁的获取和释放,开发者无需手动控制。这种内置的锁机制提供了简单而有效的线程同步方式,但相对于ReentrantLock
等其他同步机制,它的灵活性和扩展性较差。
synchronized修饰的地方不同,实现的原理不同
monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。
synchronized用来修饰方法和静态方法时
synchronized用来修饰方法和静态方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。
- public class SyncTest {
-
- public synchronized void sync(){
-
- }
- }
通过javap -verbose xxx.class查看反编译结果:
从反编译的结果来看,我们可以看到sync()方法中多了一个标识符。JVM就是根据该ACC_SYNCHRONIZED标识符来实现方法的同步,即:
当方法被执行时,JVM 调用指令会去检查方法上是否设置了ACC_SYNCHRONIZED标识符,如果设置了ACC_SYNCHRONIZED标识符,则会获取锁对象的 monitor 对象,线程执行完方法体后,又会释放锁对象的 monitor对象。在此期间,其他线程无法获得锁对象的 monitor 对象。
修饰代码块和类时,是通过monitorenter和monitorexit指令来完成
- public class SyncTest {
-
- private static int count;
-
- public SyncTest() {
- count = 0;
- }
-
- public void sync() {
- synchronized (this) {
- for (int i = 0; i < 5; i++) {
- try {
- System.out.println(Thread.currentThread().getName() + ":" + (count++));
- Thread.sleep(100);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- }
-
-
- public static void main(String[] args) {
- SyncTest s = new SyncTest();
- Thread t0 = new Thread(new Runnable() {
- @Override
- public void run() {
- s.sync();
- }
- });
- Thread t1 = new Thread(new Runnable() {
- @Override
- public void run() {
- s.sync();
- }
- });
- t0.start();
- t1.start();
- }
- }
查看字节码信息:
我们可以看到sync()字节码指令中会有两个monitorenter和monitorexit指令:
monitorenter: 该指令表示获取锁对象的 monitor 对象,这时 monitor 对象中的 count 会加+1,如果 monitor 已经被其他线程所获取,该线程会被阻塞住,直到 count = 0,再重新尝试获取monitor对象。
monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。
实现原理的核心
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。注:monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
ACC_SYNCHRONIZE:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。而两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致用户态和内核态两个态之间来回切换,对性能有较大影响。
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
synchronized
关键字在Java中的作用是实现线程的同步和并发控制,确保多个线程在访问共享资源时能够按照一定的顺序执行,避免数据不一致和并发问题的发生。
具体来说,synchronized
的作用包括以下几个方面:
互斥访问: synchronized
关键字可以保证同一时间只有一个线程可以进入被锁定的代码块或方法,避免多个线程同时修改共享资源导致的数据不一致问题。
可见性保证: 当一个线程进入synchronized
代码块后,会将修改的共享变量刷新到主内存中,并使其他线程能够看到最新的值。这样可以保证共享变量的可见性,避免出现脏读、写入和重排序等问题。
顺序性保证: synchronized
关键字可以保证线程按照一定的顺序执行,避免出现乱序执行的情况。即使多个线程同时竞争锁,也会根据先后顺序依次进入临界区。
线程间的通信: synchronized
关键字结合wait()
和notify()
或notifyAll()
方法,可以实现线程间的等待和唤醒机制,用于线程之间的协调和通信。
总的来说,synchronized
关键字的作用是确保多个线程能够有序地访问共享资源,保证数据的一致性和线程安全性。它是Java中最基本、最常用的线程同步机制之一,适用于大多数简单的并发场景。但在复杂的并发控制情况下,可能需要使用更灵活的同步机制,如ReentrantLock
等。
synchronized关键字最主要的三种使用方式:
1、修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
2、修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
3、修饰代码块: 指定加锁对象,对指定对象加锁,进入同步代码块前要获得指定对
象的锁。
总结: synchronized 关键字加到 static 静态方法是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
`volatile`修饰符在Java中用于声明变量,确保多个线程能够正确地处理该变量的可见性和有序性。
实践上,`volatile`修饰符通常用于以下几个场景:
1. 保证变量的可见性: 当一个变量被`volatile`修饰时,对该变量的写操作会立即刷新到主内存中,而对该变量的读操作会从主内存中读取最新的值,从而保证了变量的可见性。
2. 禁止指令重排序: `volatile`修饰符可以防止编译器和处理器对指令进行重排序,从而保证了指令的有序性。这对于一些需要保证顺序执行的场景非常重要。
3. 轻量级的同步机制:`volatile`修饰符提供了一种比`synchronized`更轻量级的同步机制,用于对变量的读写操作进行同步,避免了线程安全问题。
关于单例模式,它是一种设计模式,用于保证一个类只有一个实例,并提供一个全局访问点。在Java中,常见的单例模式实现方式是使用私有构造函数和静态方法来控制对象的创建和访问。
以下是一个简单的单例模式的手写实现:
- public class Singleton {
- private static volatile Singleton instance;
-
- private Singleton() {
- // 私有构造函数
- }
-
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
上述代码使用了双重检验锁(Double-Checked Locking)方式实现单例模式。其原理如下:
1. 首先,通过`instance`变量来保存单例对象,初始值为`null`。
2. 在`getInstance()`方法中,第一次检查`instance`是否为`null`。如果为`null`,表示还没有创建实例,进入同步块。
3. 在同步块内部,再次检查`instance`是否为`null`。这是为了避免多个线程同时通过了第一次检查,然后进入同步块创建实例,从而导致多次创建实例的问题。
4. 如果`instance`仍然为`null`,则创建一个新的实例,并将其赋值给`instance`变量。
5. 最后,返回`instance`变量作为单例对象。
通过双重检验锁方式,可以在多线程环境下实现延迟加载的单例模式,并保证了线程安全性和性能。同时,使用`volatile`修饰`instance`变量,可以确保变量的可见性和有序性,避免了指令重排序导致的线程安全问题。
自旋是一种线程等待的方式,它指的是线程在获取某个资源时,如果发现资源被其他线程占用,就不会立即阻塞等待,而是通过循环不断地尝试获取资源,直到成功或达到一定的尝试次数。
自旋锁是一种基于自旋的同步机制,它是在多线程环境下用于保护共享资源的一种锁。与传统的互斥锁不同,自旋锁在资源被占用时,线程不会立即阻塞等待,而是通过自旋的方式不断尝试获取锁,直到成功获取锁或达到一定的尝试次数。
自旋锁的基本原理如下:
1. 当一个线程需要获取自旋锁时,它会尝试获取锁,如果锁没有被其他线程占用,那么当前线程将获取到锁,并可以进入临界区执行。
2. 如果锁已经被其他线程占用,那么当前线程将进入自旋状态,不断地尝试获取锁。
3. 在自旋的过程中,如果锁被释放,当前线程会立即获取到锁,并进入临界区执行。
4. 如果自旋的次数达到一定的限制(如达到设定的阈值),仍然无法获取到锁,当前线程会放弃自旋,转而进入阻塞状态,等待锁的释放。
自旋锁的优点是避免了线程的上下文切换和阻塞等待带来的开销,适用于资源占用时间短、线程竞争不激烈的场景。然而,自旋锁也存在一些问题,如长时间自旋会导致CPU占用过高,浪费资源等。因此,自旋锁的使用需要根据具体的场景和需求进行评估和选择。
自旋锁的优点
自旋锁的缺点
自旋锁的使用场景
需要注意的是,自旋锁并不是适用于所有情况的同步机制。在线程竞争激烈、资源占用时间长或者是单核CPU环境下,使用自旋锁可能会导致性能下降。因此,在选择同步机制时,需要根据具体的场景和需求进行评估和选择。
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
骚戴理解:刚开始如果没有synchronized锁没有线程占用那就是无状态锁,然后有个A线程占用了这个锁就会升级为偏向锁,然后如果这个时候A线程又来尝试获取这个锁,那就可以直接获取这个锁,如果是B线程来获取这个锁,那就会从偏向锁升级为轻量级锁, 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,B线程就会自旋等待A线程释放锁,这个时候如果又来了个C线程,那就会从轻量级锁升级为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。简单来说就是一个线程占有锁那就是偏向锁,两个就是轻量级锁,三个及以上就是重量级锁!
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁(就是先让它是一个偏向锁),并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
i、重量级锁
重量级锁是一种称谓: 这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致用户态和内核态两个态之间来回切换,对性能有较大影响。为了优化synchonized,引入了轻量级锁,偏向锁。
Java中的重量级锁: synchronized
iii、轻量级锁
轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁。轻量级锁是使用CAS操作去消除同步使用的互斥量,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。如果出现两个以上的线程争用同一个锁的情况(这里注意是两个以上!不是两个!),那轻量级锁将不会有效,必须膨胀为重量级锁。
优点: 如果竞争的程度很低(小于等于两个线程竞争锁),通过CAS操作成功避免了使用互斥量的开销。
缺点: 如果两个以上的线程竞争锁就会失效,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
iii、偏向锁
研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。
优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。
缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
当一个线程进入一个对象的 synchronized 方法A之后,其他线程无法进入该对象的任何其他synchronized方法(包括方法B),即同一个对象的其他synchronized方法会被阻塞。
这是因为synchronized关键字是基于对象级别的锁,当一个线程获得了对象的锁后,其他线程就无法获取该对象的锁,直到持有锁的线程释放锁。这种机制确保了同一时间只有一个线程可以执行该对象的synchronized方法,保证了对共享资源的互斥访问和线程安全性。
因此,当一个线程进入了对象的synchronized方法A后,其他线程对该对象的其他synchronized方法(如方法B)的访问会被阻塞,直到持有锁的线程执行完synchronized方法A并释放锁。这样可以避免多个线程同时对同一个对象的不同synchronized方法进行操作,确保数据的一致性和可靠性。
下面是对synchronized、volatile、CAS(Compare and Swap)和Lock进行比较的一些要点:
synchronized:
volatile:
CAS(Compare and Swap):
Lock:
在选择使用以上同步机制时,需要根据具体的需求和场景进行评估:
需要根据具体的并发需求和性能要求选择合适的同步机制。
synchronized和Lock都是Java中用于实现线程同步的机制,它们的区别如下:
使用方式:
灵活性:
异常处理:
性能:
需要根据具体的需求和场景选择适合的同步机制。一般来说,对于简单的同步需求,可以使用synchronized,而对于复杂的同步需求或需要更高级别控制的场景,可以选择使用Lock。
synchronized和volatile是Java中用于实现线程间共享变量的机制,它们的区别如下:
可见性:
原子性:
使用方式:
适用场景:
需要根据具体的需求和场景选择适合的同步机制。一般来说,如果需要保证对共享变量的原子性操作,可以使用synchronized;如果只需要保证对共享变量的可见性,可以使用volatile。
在Java中,不能直接创建一个volatile数组。
Volatile关键字只能应用于类的实例变量和静态变量,不能应用于局部变量、方法参数、方法返回值以及数组本身。因此,无法直接声明一个volatile数组。
如果需要对数组元素进行原子操作或者保证对数组的可见性,可以考虑使用其他同步机制,如使用synchronized关键字或使用java.util.concurrent.atomic包中的原子类来实现。另外,可以使用volatile修饰数组引用变量,以确保对数组引用的可见性。但是对于数组元素的操作仍然需要其他同步机制来保证原子性。
可见性和原子性:
使用方式:
适用场景:
需要根据具体的需求和场景选择适合的机制。一般来说,如果只需要保证对共享变量的可见性,可以使用volatile;如果需要保证对共享变量的原子性操作,可以使用atomic变量。
不,volatile不能使一个非原子操作变成原子操作。
Volatile关键字只能保证对变量的可见性,即当一个线程修改了volatile变量的值,其他线程能够立即看到最新的值。它并不能保证对变量的操作是原子性的。
原子操作是指不可被中断的操作,要么全部执行成功,要么全部不执行,不会出现中间状态。而非原子操作是指由多个步骤组成的操作,可能会出现中间状态。
Volatile关键字只能保证对变量的可见性,但不能保证对变量的操作是原子性的。如果一个操作是由多个步骤组成的,使用volatile关键字修饰变量并不能保证这个操作的原子性。在多线程环境下,如果多个线程同时进行非原子操作,仍然可能会发生竞态条件和数据不一致的问题。
要保证对变量的原子性操作,可以使用synchronized关键字或者java.util.concurrent.atomic包中的原子类来实现。这些机制提供了更强大的原子性保证,能够确保多个线程同时对变量进行操作时的一致性。
不可变对象是指一旦创建就不能被修改的对象。它的状态(属性)在创建后不可改变,所有的操作都不会改变对象本身,而是返回一个新的对象。
不可变对象对写并发应用有以下帮助:
线程安全性: 不可变对象是线程安全的,因为它的状态不可变,不会被多个线程同时修改,从而避免了并发访问的竞态条件和数据不一致的问题。
无需同步: 不可变对象不需要进行同步操作,因为它的状态不会发生变化,不需要考虑多个线程之间的竞争和同步问题。这样可以减少并发应用中的锁竞争和线程同步开销,提高性能。
易于缓存: 不可变对象的状态不会改变,可以被安全地缓存起来,避免了重复创建对象的开销。多个线程可以共享同一个不可变对象,提高了内存利用率。
可靠性: 不可变对象一旦创建就不能被修改,可以避免意外的修改操作导致对象状态错误。它的行为是可预测的,更易于理解和调试。
因此,使用不可变对象可以简化并发编程的复杂性,提高代码的可靠性和性能。在设计并发应用时,尽量使用不可变对象来代替可变对象,特别是在多线程环境下,可以减少并发问题的出现。
Lock接口是Java Concurrency API中的一种同步机制,它提供了比synchronized更灵活和可扩展的锁定机制。Lock接口的实现类可以作为替代synchronized关键字来实现线程间的同步。
与synchronized相比,Lock接口具有以下优势:
可中断性: Lock接口提供了可中断的锁获取方式。在某些情况下,如果一个线程长时间持有锁,其他线程可能需要等待很长时间。使用Lock接口可以通过调用lockInterruptibly()方法来实现可中断的锁获取,当其他线程中断当前等待的线程时,可以响应中断并释放锁。
公平性: Lock接口可以实现公平锁,即按照线程的请求顺序来获取锁。而synchronized关键字是不可控的,无法指定线程获取锁的顺序。
条件变量: Lock接口提供了Condition接口,可以实现更灵活的线程等待和通知机制。通过Condition,可以实现线程的等待和唤醒,灵活控制线程的执行顺序和条件。
灵活性: Lock接口提供了更多灵活的锁定方式。例如,可以使用tryLock()方法尝试获取锁,如果锁不可用则立即返回,而不是一直等待。还可以使用lock()方法的重载版本,设置超时时间,避免长时间等待锁。
总的来说,Lock接口提供了更多的功能和灵活性,可以根据具体的需求选择合适的锁实现。它在一些特定场景下可以替代synchronized关键字,提供更细粒度的控制和更高的性能。但是,在简单的同步需求下,synchronized关键字更加简洁和易用。
乐观锁和悲观锁是并发控制中的两种不同的策略。
悲观锁: 悲观锁假设会有并发冲突发生,因此在访问共享资源之前,会先对资源进行加锁,确保其他线程无法修改资源。悲观锁的特点是在整个访问过程中,资源都会被锁定,其他线程需要等待锁的释放。
常见的悲观锁实现方式包括:
乐观锁: 乐观锁假设并发冲突较少发生,因此不对资源进行加锁,而是在更新资源时先进行检查,确保在更新期间没有其他线程修改了资源。如果检查通过,就进行更新操作;如果检查失败,可以选择重试或进行其他处理。
常见的乐观锁实现方式包括:
乐观锁相对于悲观锁的优点在于不需要加锁,避免了线程的等待和上下文切换开销,适用于读多写少的场景。但是,乐观锁需要进行额外的检查和处理,并且在并发冲突较多时可能导致重试操作增多,影响性能。
需要根据具体的应用场景和需求选择合适的锁策略和实现方式。
CAS(Compare and Swap)是一种并发控制的原语,用于实现乐观锁机制。CAS操作包含三个参数:内存地址(或称为变量的引用)、期望值和新值。CAS操作会比较内存地址处的值与期望值,如果相等,则将内存地址处的值更新为新值;如果不相等,则表示有其他线程修改了内存地址处的值,CAS操作失败。
CAS操作是原子的,即在执行期间不会被其他线程干扰。它利用底层的硬件原子指令(如CMPXCHG指令)来实现原子性的比较和交换操作。CAS操作是乐观锁的核心思想,通过避免加锁的开销,提高了并发性能。
CAS操作的基本流程如下:
1. 读取内存地址处的值,即当前值。
2. 比较当前值与期望值是否相等。
3. 如果相等,则将内存地址处的值更新为新值。
4. 如果不相等,则表示有其他线程修改了值,操作失败。
CAS操作是一种无锁算法,适用于读多写少的场景。但是,CAS操作可能会出现ABA问题,即在操作期间,值经过多次修改后又恢复原值,导致CAS操作无法感知到中间的修改。为了解决ABA问题,可以使用版本号等方式进行增强。
在Java中,CAS操作可以通过java.util.concurrent.atomic包中的原子类(如AtomicInteger、AtomicLong等)来实现。这些原子类提供了CAS操作的封装,可以方便地实现线程安全的操作。
CAS(Compare and Swap)操作的实现原理如下:
1. 读取内存地址处的当前值。
2. 比较当前值与期望值是否相等。
3. 如果相等,则将内存地址处的值更新为新值。
4. 如果不相等,则表示有其他线程修改了值,CAS操作失败。
在底层实现上,CAS操作依赖于硬件提供的原子指令(如CMPXCHG指令)。这些原子指令可以保证在执行期间不会被其他线程干扰,从而实现原子性的比较和交换操作。
CAS操作的基本流程如下:
1. 使用底层原子指令读取内存地址处的当前值,将其保存在寄存器中。
2. 将寄存器中的值与期望值进行比较。
3. 如果相等,则将新值存储到内存地址处,完成更新操作。
4. 如果不相等,则表示有其他线程修改了值,CAS操作失败,需要重新执行整个CAS操作。
CAS操作在执行期间不会加锁,因此避免了加锁的开销和线程的等待。它利用了硬件提供的原子指令,保证了操作的原子性和线程安全性。
然而,CAS操作也存在一些问题,最主要的是ABA问题。在CAS操作期间,值可能经过多次修改后又恢复原值,导致CAS操作无法感知到中间的修改。为了解决ABA问题,可以使用版本号等方式进行增强,确保CAS操作的正确性。
在Java中,CAS操作可以通过java.util.concurrent.atomic包中的原子类(如AtomicInteger、AtomicLong等)来实现。这些原子类封装了CAS操作,提供了高效的线程安全操作。
CAS(Compare and Swap)操作具有以下优点和缺点:
优点
1. 无锁并发:CAS操作是一种无锁算法,不需要使用传统的互斥锁来保护共享资源,避免了线程的等待和上下文切换开销,提高了并发性能。
2. 原子性:CAS操作是原子的,底层硬件提供的原子指令保证了CAS操作在执行期间不会被其他线程干扰,确保操作的原子性和线程安全性。
3. 高效性:相对于悲观锁机制,CAS操作不需要加锁和解锁的开销,适用于读多写少的场景,可以提高并发性能。
缺点
1. ABA问题:CAS操作可能存在ABA问题,即在操作期间,值经过多次修改后又恢复原值,导致CAS操作无法感知到中间的修改。为了解决ABA问题,可以使用版本号等方式进行增强。
2. 自旋开销:如果CAS操作失败,需要重试整个CAS操作,可能会导致自旋的开销增加,降低性能。
3. 限制:CAS操作只能保证单个变量的原子性,无法保证多个变量之间的一致性,对于复杂的操作需要额外的处理。
综上所述,CAS操作在适合的场景下具有高效、无锁并发和原子性的优点,但需要注意解决ABA问题和自旋开销,并且对于复杂的操作可能需要额外的处理。在Java中,可以使用java.util.concurrent.atomic包中的原子类来实现CAS操作,提供了方便和高效的线程安全操作。
CAS(Compare and Swap)操作可能产生以下问题:
1. ABA问题:在CAS操作期间,值可能经过多次修改后又恢复原值,导致CAS操作无法感知到中间的修改。例如,线程A读取值为A,线程B将值修改为B,然后又修改回A,最后线程A执行CAS操作时,发现值仍然是A,认为没有其他线程修改过,但实际上已经发生了变化。解决ABA问题的方法之一是使用版本号或标记位等方式进行增强,确保CAS操作能够正确感知到中间的修改。
2. 自旋开销:如果CAS操作失败,需要重试整个CAS操作,可能会导致自旋的开销增加。自旋是指线程不断尝试执行CAS操作,直到操作成功或达到一定的重试次数。如果自旋次数过多,会占用CPU资源,降低性能。因此,在使用CAS操作时,需要根据具体情况合理设置自旋次数或使用其他并发控制机制。
3. 限制:CAS操作只能保证单个变量的原子性,无法保证多个变量之间的一致性。如果需要对多个变量进行原子操作或保持一致性,需要额外的处理,例如使用锁机制或使用其他并发控制算法。
需要注意的是,以上问题并非CAS操作本身的问题,而是在使用CAS操作时需要考虑和解决的问题。CAS操作在适合的场景下具有高效、无锁并发和原子性的优点,但需要注意处理ABA问题和自旋开销,并根据具体需求选择合适的并发控制机制。
死锁和活锁是并发编程中两种不同的问题,而死锁和饥饿是两种不同的线程调度问题。
死锁(Deadlock)是指两个或多个线程彼此持有对方所需的资源而无法继续执行的情况。在死锁中,每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行。死锁是一种严重的问题,会导致系统无响应或陷入无限等待的状态。
活锁(Livelock)是指两个或多个线程在不断地改变自己的状态,但是却没有任何进展,导致无法继续执行。在活锁中,线程不断地相互响应对方的动作,但是最终没有取得进展。活锁与死锁不同,线程并没有被阻塞,但是却无法完成任务。
饥饿(Starvation)是指一个或多个线程由于某种原因无法获取所需的资源,导致无法继续执行。在饥饿中,线程被长时间地阻塞或无法满足其资源需求,导致无法进行工作。饥饿可能是由于资源分配不公平或优先级设置不当等原因引起的。
总结
- 死锁和活锁是两种并发问题,死锁是线程相互等待对方释放资源,导致无法继续执行,而活锁是线程不断地改变状态但无法取得进展。
- 饥饿是线程调度问题,指线程无法获取所需的资源而无法继续执行。饥饿可能是由于资源分配不公平或优先级设置不当等原因引起的。
AQS(AbstractQueuedSynchronizer)是Java并发包中的一个抽象类,用于构建各种类型的同步器。它提供了一种基于FIFO等待队列的同步器框架,用于实现线程的同步和互斥。
AQS的核心思想是通过一个整型的同步状态(state)和一个等待队列来管理线程的访问和同步。同步状态可以被线程独占(exclusive mode)或共享(shared mode)获取。线程在获取同步状态时,如果同步状态不可用,则会被加入到等待队列中,并进入阻塞状态。
AQS的等待队列采用CLH(Craig, Landin, and Hagersten)队列的变种实现。每个等待线程都会创建一个节点(Node)并加入到等待队列中。当同步状态可用时,AQS会从等待队列中唤醒一个或多个线程,使其继续执行。
AQS提供了一些核心方法供子类实现,包括获取同步状态、释放同步状态、判断同步状态是否可用等。子类可以通过继承AQS并实现这些方法来构建自定义的同步器,如锁、信号量、倒计时门闩等。
AQS的使用可以简化并发编程的复杂性,提供了一个可靠的同步机制。它在Java并发包中被广泛使用,如ReentrantLock、CountDownLatch、Semaphore等都是基于AQS实现的同步器。
总结: AQS是Java并发包中的一个抽象类,用于构建各种类型的同步器。它通过同步状态和等待队列来管理线程的访问和同步。AQS提供了核心方法供子类实现,子类可以通过继承AQS来构建自定义的同步器。AQS的使用可以简化并发编程的复杂性,并提供可靠的同步机制。
AQS(AbstractQueuedSynchronizer)是Java并发包中的一个抽象类,用于构建各种类型的同步器。它的核心原理是基于一个FIFO等待队列和一个同步状态(state)来实现线程的同步和互斥。
AQS的等待队列采用CLH(Craig, Landin, and Hagersten)队列的变种实现。CLH队列是一种自旋锁的变种,它通过节点之间的关联来组织线程的等待顺序。每个等待线程都会创建一个节点(Node)并加入到等待队列中。
AQS内部维护了一个同步状态(state),该状态可以被线程独占(exclusive mode)或共享(shared mode)获取。线程通过调用AQS提供的方法来获取或释放同步状态。
当一个线程尝试获取同步状态时,如果同步状态可用,则线程可以继续执行;如果同步状态不可用,则线程会被加入到等待队列中,并进入阻塞状态。在阻塞状态下,线程会自旋等待,不断尝试获取同步状态。
当一个线程释放同步状态时,AQS会唤醒等待队列中的下一个线程,使其继续执行。唤醒的线程会重新尝试获取同步状态,如果成功获取,则可以继续执行,否则仍然会被加入到等待队列中。
AQS的具体实现依赖于子类的实现。子类需要实现AQS提供的几个核心方法,包括获取同步状态、释放同步状态、判断同步状态是否可用等。通过实现这些方法,子类可以根据自己的需求来构建不同类型的同步器,如锁、信号量、倒计时门闩等。
总结:
AQS的核心原理是基于一个FIFO等待队列和同步状态来实现线程的同步和互斥。线程通过获取和释放同步状态来进入临界区或执行特定操作。AQS的实现依赖于子类的具体实现,通过实现AQS提供的核心方法来构建不同类型的同步器。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
AQS(AbstractQueuedSynchronizer)提供了两种资源共享的方式:独占模式(exclusive mode)和共享模式(shared mode)。
1. 独占模式(exclusive mode):
在独占模式下,同一时刻只允许一个线程获取同步状态,其他线程必须等待。独占模式适用于需要互斥访问临界资源的情况,如ReentrantLock。
2. 共享模式(shared mode):
在共享模式下,多个线程可以同时获取同步状态,允许多个线程并发访问共享资源。共享模式适用于需要多个线程同时访问共享资源的情况,如ReadWriteLock的读锁。
AQS通过维护一个整型的同步状态(state)来控制资源的共享方式。在独占模式下,同步状态为0表示资源可用,为1表示资源被占用;在共享模式下,同步状态的高16位表示共享资源的数量,低16位表示排队等待的线程数量。
当一个线程尝试获取同步状态时,AQS会根据当前的同步状态以及获取方式(独占模式或共享模式)来决定是否允许线程获取同步状态。如果同步状态可用,则线程可以获取同步状态并继续执行;如果同步状态不可用,则线程会被加入到等待队列中,并进入阻塞状态。
在独占模式下,当持有同步状态的线程释放同步状态时,AQS会唤醒等待队列中的下一个线程,并将同步状态交给该线程。而在共享模式下,当持有同步状态的线程释放同步状态时,AQS会唤醒等待队列中的所有线程,并将同步状态平均分配给它们。
总结:
AQS提供了独占模式和共享模式两种资源共享的方式。独占模式适用于互斥访问临界资源的情况,共享模式适用于多个线程并发访问共享资源的情况。AQS通过同步状态来控制资源的获取和释放,并根据不同的模式来决定线程的唤醒方式。
AQS底层使用了模板方法(Template Method)设计模式。
在AQS中,定义了一个抽象的模板方法`acquire(int arg)`,该方法用于线程获取同步状态。具体的获取逻辑由子类实现,通过子类的具体实现来决定获取同步状态的方式和行为。
AQS的模板方法模式包括以下几个关键步骤:
1. 在模板方法中,首先会调用子类实现的`tryAcquire(int arg)`方法,该方法用于尝试获取同步状态。子类可以根据自己的需求和规则来实现具体的获取逻辑。
2. 如果`tryAcquire(int arg)`方法返回成功,即获取同步状态成功,则模板方法会继续执行,并将同步状态分配给当前线程。
3. 如果`tryAcquire(int arg)`方法返回失败,即获取同步状态失败,则模板方法会调用`addWaiter(Node mode)`方法将当前线程加入到等待队列中,并进入阻塞状态。
4. 当其他线程释放同步状态时,AQS会唤醒等待队列中的下一个线程,并再次调用模板方法进行获取尝试,重复上述步骤。
通过模板方法设计模式,AQS将获取同步状态的逻辑封装在模板方法中,而具体的获取方式则由子类实现。这种设计模式使得AQS可以灵活适应不同类型的同步器,并提供了一种可扩展的框架。
总结:
AQS底层使用了模板方法设计模式。通过定义抽象的模板方法和子类的具体实现,AQS实现了获取同步状态的逻辑的封装和灵活性,提供了一种可扩展的同步器框架。
自定义同步器时需要重写下面几个AQS提供的模板方法:
默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用(因为继承不了),只有这几个方法可以被其他类使用
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加), 这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定
- class Mutex implements Lock, java.io.Serializable {
- // 自定义同步器
- private static class Sync extends AbstractQueuedSynchronizer {
- // 判断是否锁定状态
- protected boolean isHeldExclusively() {
- return getState() == 1;
- }
-
- // 尝试获取资源,立即返回。成功则返回true,否则false。
- public boolean tryAcquire(int acquires) {
- assert acquires == 1; // 这里限定只能为1个量
- if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
- setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
- return true;
- }
- return false;
- }
-
- // 尝试释放资源,立即返回。成功则为true,否则false。
- protected boolean tryRelease(int releases) {
- assert releases == 1; // 限定为1个量
- if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
- throw new IllegalMonitorStateException();
- setExclusiveOwnerThread(null);
- setState(0);//释放资源,放弃占有状态
- return true;
- }
- }
-
- // 真正同步类的实现都依赖继承于AQS的自定义同步器!
- private final Sync sync = new Sync();
-
- //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
- public void lock() {
- sync.acquire(1);
- }
-
- //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
- public boolean tryLock() {
- return sync.tryAcquire(1);
- }
-
- //unlock<-->release。两者语文一样:释放资源。
- public void unlock() {
- sync.release(1);
- }
-
- //锁是否占有状态
- public boolean isLocked() {
- return sync.isHeldExclusively();
- }
- }
公平锁和非公平锁是指在多线程环境下,线程获取锁的顺序和公平性的不同。
公平锁(Fair Lock):
非公平锁(Nonfair Lock):
总结: 公平锁和非公平锁的区别在于线程获取锁的顺序和公平性。公平锁按照申请锁的顺序来获取锁,保证了线程获取锁的公平性,但可能会导致系统性能下降;非公平锁则不保证线程获取锁的顺序,通过减少线程切换开销提高了系统的吞吐量,但可能会导致某些线程长时间等待。选择公平锁还是非公平锁要根据具体的应用场景和性能需求来决定。
ReadWriteLock是Java中的一个接口,用于实现读写锁(Read-Write Lock)。读写锁是一种特殊的锁机制,可以同时支持多个线程对共享资源的读访问,但只允许一个线程进行写操作。
ReadWriteLock接口定义了两个关键方法:
1. readLock():返回一个读锁,用于支持多个线程的读操作。多个线程可以同时获取读锁,只要没有线程持有写锁。
2. writeLock():返回一个写锁,用于支持独占的写操作。只有当没有线程持有读锁或写锁时,线程才能获取写锁。
读锁和写锁之间是互斥的,即当一个线程持有写锁时,其他线程无法获取读锁或写锁。这样可以确保在写操作期间,没有其他线程能够同时进行读操作,保证了数据的一致性和完整性。
ReadWriteLock的设计目的是在读多写少的场景中提高并发性能。当多个线程只进行读操作时,可以同时获取读锁,提高并发度。而当有线程进行写操作时,会阻塞其他线程的读锁和写锁获取,保证数据的正确性。
Java中的ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,它提供了可重入的读写锁功能,允许同一个线程多次获取读锁或写锁,避免了死锁的问题。
总结:
ReadWriteLock是Java中用于实现读写锁的接口,支持多个线程的读操作和独占的写操作。它提供了读锁和写锁两种锁机制,通过互斥关系保证了数据的一致性和完整性,并在读多写少的场景中提高了并发性能。
为什么会有读写锁?
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。
同步容器(Synchronized Container)是指在多线程环境下,通过使用同步机制(如synchronized关键字)来保证容器的线程安全性。在同步容器中,所有对容器的操作都会被同步,一次只能有一个线程进行操作,从而避免了多线程并发访问容器时可能出现的数据不一致或异常情况。
并发容器(Concurrent Container)是指专门设计用于在多线程环境下进行高效并发访问的容器。与同步容器不同,并发容器通过使用更加高效的并发控制机制来提供更好的性能和可伸缩性,允许多个线程同时进行读写操作,以提高并发度。
Java中提供了一些常见的并发容器,包括:
1. ConcurrentHashMap:是线程安全的哈希表实现,支持高并发读写操作,并通过分段锁(Segment)来提高并发度。
2. ConcurrentLinkedQueue:是线程安全的无界队列实现,支持高并发的入队和出队操作,使用CAS(Compare and Swap)操作来保证线程安全。
3. CopyOnWriteArrayList:是线程安全的动态数组实现,通过复制整个数组来实现写操作的线程安全,适用于读多写少的场景。
4. BlockingQueue:是一个阻塞队列接口,提供了线程安全的入队和出队操作,常用的实现有ArrayBlockingQueue、LinkedBlockingQueue等。
这些并发容器的实现方式各不相同,但都通过使用特定的并发控制机制来保证线程安全性和高效性能,可以在多线程环境下安全地进行并发操作。
总结:
同步容器使用同步机制来保证线程安全,一次只能有一个线程进行操作;并发容器通过使用更高效的并发控制机制,允许多个线程同时进行读写操作,提高并发度和性能。Java提供了一些常见的并发容器,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们通过不同的实现方式来实现线程安全和高效的并发操作。
Java中的同步容器和并发容器都是用于在多线程环境下进行操作的容器,但它们在实现方式和性能特点上有一些区别。
实现方式:
并发度:
性能特点:
总结: 同步容器通过同步机制来保证线程安全,一次只能有一个线程进行操作,性能较低;并发容器通过使用更高效的并发控制机制,允许多个线程同时进行读写操作,提高了并发度和性能。在选择使用同步容器还是并发容器时,需要根据具体场景和性能需求来进行选择。
Condition是Java中用于实现等待通知机制的一部分,它是Lock接口的一部分,用于在多线程环境下实现线程间的等待和通知。
Condition接口提供了以下主要方法:
Condition的实现通常与Lock配合使用,使用Lock的newCondition()方法来创建一个Condition实例。在Condition的实现中,会维护一个等待队列,用于存放等待在该Condition上的线程。
下面是Condition的源码分析与等待通知机制的简要描述:
Condition的实现类通常会包含一个等待队列,用于存放等待在该Condition上的线程。在Java中,常用的Condition实现类是AbstractQueuedSynchronizer中的ConditionObject类。
当一个线程调用Condition的await()方法时,它会释放持有的锁,并进入等待状态,将自己加入到等待队列中。
当另一个线程调用Condition的signal()方法时,它会从等待队列中选择一个线程,将其唤醒,并使其从await()方法返回。被唤醒的线程会重新尝试获取锁,并继续执行。
如果有多个线程在等待队列中等待,调用signalAll()方法会唤醒所有等待线程,使它们从await()方法返回。
在等待过程中,如果线程被中断,它会抛出InterruptedException异常,可以通过捕获异常来处理中断情况。而awaitUninterruptibly()方法则会忽略中断,继续等待。
通过使用Condition,我们可以实现更细粒度的线程等待和通知机制,而不仅仅依赖于synchronized关键字的wait()和notify()方法。它提供了更灵活的控制和更高级别的线程间通信方式。
需要注意的是,Condition的使用需要与Lock配合使用,它们通常作为一对使用,用于替代传统的synchronized关键字和Object的wait()和notify()方法。
BlockingQueue是Java中的一个接口,用于实现线程安全的阻塞队列。它继承自Queue接口,并在其基础上添加了一些阻塞的特性。
阻塞队列是一种特殊的队列,当队列为空时,从队列中获取元素的操作会被阻塞,直到队列中有元素可用;当队列已满时,往队列中添加元素的操作会被阻塞,直到队列有空闲位置。
下面是一个使用BlockingQueue的简单示例:
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.ArrayBlockingQueue;
-
- public class BlockingQueueExample {
- public static void main(String[] args) {
- // 创建一个有界阻塞队列,最多可容纳3个元素
- BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
-
- // 创建生产者线程
- Thread producer = new Thread(() -> {
- try {
- // 往队列中放入元素
- queue.put(1);
- System.out.println("Producer put 1");
- queue.put(2);
- System.out.println("Producer put 2");
- queue.put(3);
- System.out.println("Producer put 3");
- // 队列已满,下面的put操作将会被阻塞
- queue.put(4);
- System.out.println("Producer put 4");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 创建消费者线程
- Thread consumer = new Thread(() -> {
- try {
- // 从队列中取出元素
- int element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- // 队列为空,下面的take操作将会被阻塞
- element = queue.take();
- System.out.println("Consumer take " + element);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 启动生产者和消费者线程
- producer.start();
- consumer.start();
- }
- }
在上面的示例中,我们创建了一个有界阻塞队列`ArrayBlockingQueue`,最多可容纳3个元素。生产者线程往队列中放入元素,当队列已满时,`put()`操作会被阻塞。消费者线程从队列中取出元素,当队列为空时,`take()`操作会被阻塞。
通过使用BlockingQueue,我们可以方便地实现线程间的协作和数据交换,而不需要手动编写复杂的同步代码。
在Queue接口中,poll()和remove()方法都用于从队列中获取并移除元素,但它们在处理空队列时的行为有所不同。
所以,主要区别在于当队列为空时,poll()方法会返回null,而remove()方法会抛出异常。
当涉及到并发容器时,有两个常用的实现:ArrayBlockingQueue和LinkedBlockingQueue。下面将详细介绍它们的特点和使用场景,并提供一些示例。
1. ArrayBlockingQueue:
- 内部实现:ArrayBlockingQueue使用数组作为底层数据结构,具有固定的容量。它按照先进先出(FIFO)的原则来维护元素的顺序。
- 特点:
- 有界队列:ArrayBlockingQueue的容量是固定的,一旦创建后无法改变。
- 阻塞操作:当队列已满时,往队列中添加元素的操作会被阻塞,直到队列有空闲位置;当队列为空时,从队列中获取元素的操作会被阻塞,直到队列有可用元素。
- 适用场景:适用于生产者-消费者模式,其中生产者和消费者在不同的线程中工作,且生产者和消费者的速度不一致。
下面是一个使用ArrayBlockingQueue的示例:
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.BlockingQueue;
-
- public class ArrayBlockingQueueExample {
- public static void main(String[] args) {
- BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
-
- // 创建生产者线程
- Thread producer = new Thread(() -> {
- try {
- queue.put(1);
- System.out.println("Producer put 1");
- queue.put(2);
- System.out.println("Producer put 2");
- queue.put(3);
- System.out.println("Producer put 3");
- queue.put(4); // 队列已满,该操作会被阻塞
- System.out.println("Producer put 4");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 创建消费者线程
- Thread consumer = new Thread(() -> {
- try {
- int element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take(); // 队列为空,该操作会被阻塞
- System.out.println("Consumer take " + element);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 启动生产者和消费者线程
- producer.start();
- consumer.start();
- }
- }
在上面的示例中,我们创建了一个容量为3的ArrayBlockingQueue。生产者线程往队列中放入元素,当队列已满时,`put()`操作会被阻塞。消费者线程从队列中取出元素,当队列为空时,`take()`操作会被阻塞。
2. LinkedBlockingQueue:
- 内部实现:LinkedBlockingQueue使用链表作为底层数据结构,可以选择有界或无界。
- 特点:
- 可选有界或无界:可以根据需求选择队列的容量。
- 阻塞操作:当队列已满时,往队列中添加元素的操作会被阻塞,直到队列有空闲位置;当队列为空时,从队列中获取元素的操作会被阻塞,直到队列有可用元素。
- 适用场景:适用于生产者-消费者模式,其中生产者和消费者在不同的线程中工作,且生产者和消费者的速度不一致。
下面是一个使用LinkedBlockingQueue的示例:
- import java.util.concurrent.BlockingQueue;
- import java.util.concurrent.LinkedBlockingQueue;
-
- public class LinkedBlockingQueueExample {
- public static void main(String[] args) {
- BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
-
- // 创建生产者线程
- Thread producer = new Thread(() -> {
- try {
- queue.put(1);
- System.out.println("Producer put 1");
- queue.put(2);
- System.out.println("Producer put 2");
- queue.put(3);
- System.out.println("Producer put 3");
- queue.put(4); // 队列已满,该操作会被阻塞
- System.out.println("Producer put 4");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 创建消费者线程
- Thread consumer = new Thread(() -> {
- try {
- int element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take();
- System.out.println("Consumer take " + element);
- element = queue.take(); // 队列为空,该操作会被阻塞
- System.out.println("Consumer take " + element);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- });
-
- // 启动生产者和消费者线程
- producer.start();
- consumer.start();
- }
- }
在上面的示例中,我们创建了一个LinkedBlockingQueue,可以容纳3个元素。生产者线程往队列中放入元素,当队列已满时,`put()`操作会被阻塞。消费者线程从队列中取出元素,当队列为空时,`take()`操作会被阻塞。
无论是ArrayBlockingQueue还是LinkedBlockingQueue,它们都提供了一种方便且线程安全的方式来进行并发编程,可以根据具体的需求选择适合的并发容器。
ArrayBlockingQueue和LinkedBlockingQueue是Java中常用的并发容器,它们都实现了BlockingQueue接口,用于实现线程安全的阻塞队列。虽然它们都可以用于实现生产者-消费者模式,但在内部实现和使用场景上有一些不同之处。
下面是ArrayBlockingQueue和LinkedBlockingQueue的比较:
内部实现:
容量:
阻塞操作:
性能:
使用场景:
综上所述,ArrayBlockingQueue和LinkedBlockingQueue在内部实现和使用场景上有所不同。根据具体的需求,选择适合的并发容器可以提高程序的性能和可维护性。
原子操作是指在执行过程中不会被其他线程中断的操作。它们是不可分割的,要么完全执行成功,要么完全不执行。
在Java Concurrency API中,有一些原子类(atomic classes),它们提供了线程安全的原子操作。以下是一些常用的原子类:
1. AtomicBoolean:提供了对boolean类型的原子操作,如原子设置、原子更新等。
2. AtomicInteger:提供了对int类型的原子操作,如原子增加、原子更新等。
3. AtomicLong:提供了对long类型的原子操作,如原子增加、原子更新等。
4. AtomicReference:提供了对引用类型的原子操作,如原子更新引用等。
5. AtomicStampedReference:提供了对引用类型的原子操作,并支持解决ABA问题。
6. AtomicIntegerFieldUpdater:提供了对指定类的int类型字段的原子操作。
7. AtomicLongFieldUpdater:提供了对指定类的long类型字段的原子操作。
8. AtomicReferenceFieldUpdater:提供了对指定类的引用类型字段的原子操作。
这些原子类可以保证在多线程环境下的线程安全性,避免了使用锁机制的开销。它们通常用于实现计数器、状态标志、自旋锁等并发控制的场景。使用原子类可以简化并发编程的复杂性,并提高程序的性能和可维护性。
"atomic"(原子)一词在并发编程中表示原子性操作,即不可分割的操作。原子操作要么完全执行成功,要么完全不执行,不会被其他线程中断。
在Java中,原子操作可以通过使用Java Concurrency API中提供的原子类来实现。这些原子类使用底层的硬件支持或锁机制来确保操作的原子性。
原子类的实现原理主要有以下几个方面:
1. 原子性保证:原子类使用底层的硬件支持或锁机制来保证操作的原子性。例如,使用CAS(Compare and Swap)操作,即比较并交换操作,可以在不使用锁的情况下实现原子操作。
2. 内存可见性:原子类通过使用volatile关键字来保证多个线程之间对共享变量的修改能够及时可见。volatile关键字会禁止指令重排序,确保变量的修改对其他线程是可见的。
3. 无锁算法:原子类的实现通常使用无锁算法,即不使用传统的锁机制来实现线程安全性。无锁算法通过使用CAS操作等技术来实现线程安全性,避免了使用锁带来的性能开销和死锁等问题。
4. 原子类的底层实现:原子类的底层实现通常使用了底层的硬件支持,如CPU提供的原子指令,或者使用了一些特殊的数据结构来实现原子性操作。
总之,原子操作是并发编程中重要的概念,可以通过使用Java Concurrency API中提供的原子类来实现。原子类通过底层的硬件支持或锁机制来保证操作的原子性,并且使用volatile关键字来保证内存可见性。原子类的底层实现通常使用无锁算法,以提高性能和避免死锁等问题。
CyclicBarrier和CountDownLatch是Java中的两个并发工具类,它们都可以用于线程间的同步,但在使用方式和特性上有一些区别。
使用方式:
计数器:
重用性:
应用场景:
综上所述,CyclicBarrier和CountDownLatch在使用方式、计数器、重用性和应用场景上有一些区别。根据具体的需求,选择适合的同步工具可以提高并发程序的效率和可维护性。
Semaphore和Exchanger是Java中的两个并发工具类,它们都可以用于线程间的同步,但在使用方式和特性上有一些区别。
使用方式:
功能特性:
计数器:
应用场景:
综上所述,Semaphore和Exchanger在使用方式、功能特性、计数器和应用场景上有一些区别。根据具体的需求,选择适合的并发工具可以提高并发程序的效率和可维护性。
Java中提供了许多常用的并发工具类,用于处理多线程编程中的同步和协作问题。以下是一些常用的并发工具类:
1. Lock/ReentrantLock:可重入锁,提供了比synchronized更灵活的锁机制,支持公平锁和非公平锁。
2. Condition:条件变量,与Lock配合使用,可以实现线程间的等待和通知机制。
3. Semaphore:信号量,用于控制同时访问某个资源的线程数量。
4. CountDownLatch:倒计时门闩,用于等待其他线程完成一组操作后再继续执行。
5. CyclicBarrier:循环屏障,用于等待一组线程达到某个屏障点后再同时执行。
6. Phaser:阶段器,可以用于多阶段并发任务的同步。
7. Exchanger:交换器,用于两个线程之间的数据交换和同步。
8. CompletableFuture:异步编程的工具类,可以实现异步任务的执行和结果处理。
9. BlockingQueue:阻塞队列,提供了线程安全的队列操作,支持生产者-消费者模式。
10. ConcurrentHashMap:线程安全的哈希表,支持高并发读写操作。
这些并发工具类提供了不同的功能和特性,可以根据具体的需求选择合适的工具类来解决多线程编程中的并发问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。