线程同步的目的是为了保证多个线程访问共享资源时,能够避免数据的不一致性和竞争条件。Java提供了多种机制来实现线程同步,包括synchronized关键字、显式锁(ReentrantLock)、信号量(Semaphore)、读写锁(ReadWriteLock)等。
Java语言里的线程安全讨论线程安全,将以多个线程之间存在共享数据访问为前提。否则线程是串行执行还是多线程执行根本没有任何区别。但线程安全并不是分为安全和不安全两类,而是以【安全程度】来分为5类:
① 不可变:在Java语言里,不可变的对象一定是线程安全的,无论是对象的方法实现或是方法的调用者,都不需要进行任何的线程安全保障措施。如果多线程共享的数据是一个基本数据类型,使用final关键字修饰它就可以保证是不可变的;对于引用数据类型,则需要对象自行保证其行为不会对其状态产生影响。例如java.lang.String类,它是一个典型的不可变对象,用户调用其substring()、replace()、concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
② 绝对线程安全:“不管运行时环境如何,调用者都不需要任何额外的同步措施”。Java语言中标注自己是线程安全的类,使用起来都不是绝对线程安全。例如Vector,它是一个线程安全的容器,类里面的方法都使用了synchronized修饰,但并不意味着调用它的方法就是绝对线程安全的。
协程是一种轻量级的线程模型,也称为用户态线程,Java通过虚拟线程支持了协程的概念。
协程的调度由开发者(或运行时库)显式控制,协程主动让出执行权(yield),而不是像线程那样被操作系统强制抢占。协程的切换开销极小(通常只有纳秒级)。协程的栈空间可以动态调整(如几KB到几十KB),远小于线程的固定栈(通常几MB)。因此,单机可以轻松创建数百万个协程。协程通过挂起(suspend)机制避免阻塞线程。例如,遇到I/O操作时,协程自动挂起,让出CPU资源,待I/O完成后恢复执行。
在Java中,线程的生命周期可以细化为六个状态。
① 新建(New):当程序使用new关键字创建了一个线程后,此时由JVM为其分配内存,并初始化其成员变量的值。Java虚拟机会为其创建方法调用栈和程序计数器,等到调度运行。
② 可运行(Runnable):调用线程的start()方法后,线程进入可运行状态,等待CPU调度。
③ 运行(Running):处于可运行状态的线程获得CPU分片后,执行run()方法。
④ 阻塞(Blocked):线程试图获取一个对象锁而被阻塞。
⑤ 等待资源(Waiting):线程进入等待状态,但没有指定等待时间,需要被其他线程显式唤醒。
⑥ 含等待时间的等待状态(TimedWaiting):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
⑦ 终止(Terminated):线程执行完成或因异常退出,生命周期结束。
⑧ 以上就是Java中线程生命周期的六个状态,理解这些状态有助于开发者更好地管理和控制多线程程序。
Java多线程通信方法主要包括以下几种:
① 共享变量:多个线程可以共享同一个变量,通过读写该变量来进行通信。这种方式简单,但需要注意同步问题,防止数据不一致。
② 等待/通知机制:使用wait()、notify()和notifyAll()方法来实现线程之间的等待和通知。其中,wait()方法使当前线程进入等待状态,直到被其他线程调用notify()或notifyAll()方法来唤醒;notify()方法唤醒在该对象上等待的一个线程;notifyAll()方法唤醒在该对象上等待的所有线程。
③ 使用阻塞队列:通过阻塞队列(例如ArrayBlockingQueue、LinkedBlockingQueue等)来实现线程之间的数据传递。阻塞队列提供了put()和take()方法,分别用于向队列中添加元素和从队列中获取元素,如果队列为空或已满,则阻塞线程,直到条件满足。
④ Condition条件变量:Condition是在Java 5中引入的用于替代传统的Object的wait()和notify()方法的方式,它提供了更加灵活和精细的线程通信机制。通过Lock对象的newCondition()方法创建一个Condition对象,并使用await()和signal()方法来实现线程等待和唤醒操作。
⑤ CountDownLatch:CountDownLatch是一个同步工具类,它可以让一个或多个线程等待其他线程完成操作后再继续执行。在CountDownLatch对象创建时需要指定一个计数值,当计数值为0时,等待的线程就可以继续执行。
** 在Java中创建多线程主要有三种方式:继承Thread类、实现Runnable接口以及使用Executor框架。
继承Thread类**
① 定义一个类继承Thread类,并重写Thread类的run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;
② 创建该类的实例对象,即创建了线程对象;
③ 调用线程对象的start()方法来启动线程。
实现Runnable接口
④ 定义一个类实现Runnable接口;
⑤ 创建该类的实例对象obj;
⑥ 将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
⑦ 调用线程对象的start()方法启动该线程。
使用Executor框架
⑧ 创建一个实现了Runnable接口的类;
⑨ 实现类去实现Runnable中的抽象方法:run();
⑩ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;
⑪ 通过Thread类的对象调用start()。
Java线程池的原理是通过线程集合(workerSet)和阻塞队列(workQueue)来管理和调度线程的执行。
Java线程池的实现原理是通过线程集合(workerSet)和阻塞队列(workQueue)来管理和调度线程的执行。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断地从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。corePoolSize规定线程池有几个线程(worker)在运行。maximumPoolSize规定线程池最多只能有多少个线程(worker)在执行。keepAliveTime超出corePoolSize大小的那些线程的生存时间,这些线程如果长时间没有执行任务并且超过了keepAliveTime设定的时间,就会消亡。threadFactory创建线程的工厂。
设置Java线程池的线程数需要考虑多个因素,包括系统的CPU核心数、任务的类型(CPU密集型、I/O密集型或混合型)、系统资源以及任务的执行时间。
以下是根据不同任务类型设置线程池大小的一些建议:
① CPU密集型任务:线程池的大小通常设置为N+1,其中N是可用CPU核心数。额外的一个线程用于应对偶尔的任务阻塞或意外的上下文切换。
② I/O密集型任务:由于线程在等待I/O操作时会空闲,因此可以适当增加线程池的大小。一个经验公式是:线程池大小=CPU核心数*(1+平均等待时间/平均工作时间)。
③ 混合型任务:可以将任务分为CPU密集型和I/O密集型,然后分别使用不同的线程池去处理。
Java线程池拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy。
以下是每种拒绝策略的详细说明:
主要线程池实现的详细对比:
可以
java线程池核心线程数在运行过程中可以修改。
Java的ThreadPoolExecutor提供了动态调整核心线程数和最大线程数的方法。使用ThreadPoolExecutor.setCorePoolSize(int corePoolSize)方法可以动态修改核心线程数。corePoolSize参数代表线程池中的核心线程数,当池中线程数量少于核心线程数时,会创建新的线程来处理任务。这个修改可以在线程池运行的过程中进行,立即生效。核心线程数的修改不会中断现有任务,新的核心线程数会在新任务到来时生效。
shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
**在Java线程池中,当内部任务出异常后,可以通过自定义任务实现、使用ThreadFactory、设置异常处理器等方法知道是哪个线程出了异常。
自定义任务实现**
为提交给线程池的任务实现一个自定义的Runnable或Callable,并在其run或call方法中捕获异常。在捕获异常时,你可以记录当前线程的名称(通过Thread.currentThread().getName()获取)以及其他相关的上下文信息,如任务ID、任务参数等。
使用ThreadFactory
创建线程池时,可以指定一个自定义的ThreadFactory。在这个工厂中,你可以为创建的每个线程设置一个唯一的名称或属性,这样在异常发生时,你就可以通过线程的名称或属性来识别它。
设置异常处理器
线程池允许你设置一个UncaughtExceptionHandler来处理未捕获的异常。在这个处理器中,你可以访问抛出异常的线程,并记录或处理这个异常。
重写ThreadPoolExecutor的afterExecute()方法
通过重写ThreadPoolExecutor的afterExecute()方法,可以在任务执行完成后被调用,无论任务是正常完成还是抛出了异常。可以在该方法中捕获异常,并记录与当前线程相关的信息,例如线程名称、ID等,从而帮助定位问题。
为线程池中的每个线程设置自定义的UncaughtExceptionHandler
为线程池中的每个线程设置自定义的UncaughtExceptionHandler,以便在未捕获的异常发生时进行处理。这种方式允许开发者在异常发生时获取详细的线程信息。
Java中的DelayQueue和ScheduledThreadPool的主要区别在于灵活性和适用场景。
DelayQueue更灵活,适合需要深度控制任务存储和执行逻辑的场景。它是一个基于优先队列的无界阻塞队列,适用于需要对任务的存储和执行进行精细控制的情况。而ScheduledThreadPool更强大,适合大多数单机定时任务需求(延迟/周期性任务)。它是Java 5.0引入的,是一个更通用的Timer替代品,允许多个服务线程,可以配置为等同于Timer的单线程模式。
Java中的Timer是java.util包中提供的一个工具类,用于在指定的时间或周期性地执行任务。
Java的Timer是Java早期(JDK 1.3引入)提供的一种简单定时任务调度机制。以下是Timer的详细说明:类名:java.util.Timer;作用:安排任务(TimerTask)在未来的某个时间执行,可以是单次执行或周期性执行;核心特性:使用单一后台线程处理所有任务。支持延迟执行和固定频率/固定间隔的重复执行。
时间轮(TimeWheel)是一种高效的定时器管理数据结构,特别适用于处理大量定时任务的场景。
时间轮的核心思想是将时间线按固定的间隔分割成若干个时间槽,每个时间槽存放在一个链表或队列中,链表中的元素是将在该时间点触发的定时任务。时间轮类似于一个钟表,每个时间槽相当于钟表上的一个刻度,指针每次移动一个刻度(即一个时间槽),依次检查在该槽中的任务是否需要执行。
在Java编程语言中,java.util.concurrent 及其子包提供了一系列并发工具类,用于简化多线程编程和提高程序的性能。以下是一些常用的Java并发工具类。
Java中的Semaphore是一种并发控制工具,用于控制同时访问特定资源的线程数量。
Semaphore通过维护一个许可集来控制同时访问特定资源的线程数量,从而避免资源竞争和潜在的性能问题。它内部维护了一个计数器,其值为可以访问的共享资源的个数。当线程需要访问共享资源时,它首先必须从Semaphore获取一个许可。如果许可可用,那么线程将获取它,并且计数器减一。如果计数器变为零,那么后续尝试获取许可的线程将被阻塞,直到有其他线程释放许可为止。当线程使用完共享资源后,它必须将许可归还给Semaphore,这样其他等待的线程就可以获取许可并继续执行。
CyclicBarrier是Java并发工具类,用于同步多个线程,使其等待所有线程到达指定点后继续执行。
CyclicBarrier,一个同步辅助类,在API中是这么介绍的:它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。因为该barrier在释放等待线程后可以重用,所以称它为循环的barrier。通俗点讲就是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CountDownLatch是Java并发包里的一个工具类,让一个或多个线程等待其他线程完成操作后再继续执行。
CountDownLatch(倒计时锁)是Java并发编程中重要的线程同步辅助工具类,其与join方法功能类似,其可以阻塞住一个或多个线程,等待在某些线程中执行想用的操作,将CountDownLatch倒计时计数到0时,这些被阻塞的线程才能继续向下执行。CountDownLatch可以将一个或多个线程阻塞,并在另外一个或多个线程中将CountDownLatch计数器减为0,被阻塞的线程解除休眠状态,继续执行。