程序(program):为完成特定任务,用某种语言编写的一组指令的集合
。即指一段静态的代码
,静态对象。
进程(process):**程序的一次执行过程,或是正在内存中运行的应用程序。**如:运行中的QQ,运行中的网易音乐播放器。
操作系统调度和分配资源的最小单位
(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径
。一个进程中至少有一个线程。
并行
执行多个线程,就是支持多线程的。线程作为CPU调度和执行的最小单位
。
一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患
。
下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
进程与线程的区别:
进程是一个独立的执行环境,拥有自己的地址空间和系统资源,进程间通信的开销较大。
线程是进程内的执行单元,共享进程的资源,线程间通信相对容易且开销较小。
进程之间是相互独立的,一个进程的崩溃不会影响其他进程。
线程之间共享同一进程的上下文,一个线程的错误可能会影响整个进程的稳定性。
多线程的优势和应用:
多线程可以提高程序的并发性,充分利用多核处理器的能力,提高程序的执行效率。
多线程适用于需要同时处理多个任务或需要实时响应的应用,如并发服务器、多媒体处理和游戏等。
注意:
不同的进程之间是不共享内存的。
进程之间的数据交换和通信的成本很高。
并行和并发是两个与多任务处理相关的概念
并行是真正同时执行多个任务的情况,需要具备多个物理执行单元;而并发是多个任务在时间上重叠执行的情况,可以在单个处理器上通过快速切换来实现。并行和并发在多任务处理中具有不同的应用场景和特点。
总结起来,单核处理器只有一个物理处理核心,一次只能执行一个任务,通过任务切换来实现多任务的并发执行;而多核处理器拥有多个物理处理核心,可以同时执行多个任务,通过并行执行来提高处理能力。多核处理器具有更好的性能和并行能力,适用于需要处理大量并发任务的应用。随着技术的进步,多核处理器成为了主流,核心数量和性能不断增加。
下面是关于单核和多核的详细解释:
单核处理器(Single-core Processor):
多核处理器(Multi-core Processor):
单核与多核的区别:
单核与多核的应用和优势:
单核与多核的发展趋势:
多线程是指在一个程序中同时执行多个线程,每个线程都有自己的执行路径。Java提供了内置的多线程支持,通过使用java.lang.Thread
类来创建和管理线程。
在实际应用中,还有许多高级的多线程概念和技术,例如线程池的配置、线程间的通信方式、线程的优先级、线程的中断和终止等。
以下是Java多线程的详细细节:
Thread
类的子类,并重写run()
方法,该方法包含线程的实际执行逻辑。Runnable
接口的类,并实现run()
方法。start()
方法来启动线程。这会在新的线程中调用run()
方法。start()
方法。ConcurrentHashMap
、AtomicInteger
等)。synchronized
关键字:可以修饰方法或代码块,确保同一时间只有一个线程可以访问被修饰的代码。volatile
关键字:用于确保变量的可见性,保证多个线程对变量的修改能够正确地被其他线程读取。java.util.concurrent.locks
包中的锁机制,如ReentrantLock
、ReadWriteLock
等。wait()
、notify()
和notifyAll()
:这些方法是Object
类中的方法,用于线程间的等待和唤醒。join()
:一个线程可以调用另一个线程的join()
方法,等待另一个线程执行完毕后再继续执行。java.util.concurrent.Executors
类提供了创建线程池的方法,如newFixedThreadPool()
、newCachedThreadPool()
等。JVM启动时会创建一个主线程,该主线程负责执行Main方法,它是程序的入口点。主线程负责执行程序的主要逻辑,并且是程序中第一个被执行的线程。
**子线程(Child Thread)**是由主线程或其他线程创建的额外线程,用于并发执行任务。
子线程和主线程是相对的概念,主线程创建子线程后,可以继续执行其他代码,而子线程则在独立的执行路径上执行自己的任务。子线程可以同时与主线程并发执行,实现多任务处理。
@Test
测试类需要使用.join()
方法来确保子线程执行完毕.join()
方法会阻塞当前线程(通常是主线程),直到调用.join()
方法的线程(子线程)执行完毕。
Thread
类的.start()
方法来启动线程的执行,并使用.join()
方法来等待线程执行完毕。这样可以确保主线程在所有线程执行完毕后再继续执行。try-catch
块来捕获异常,并在测试中适当地处理或报告异常情况。CountDownLatch
、CyclicBarrier
等同步工具来控制线程的执行顺序和同步点,并在合适的时机进行断言验证。总结起来,守护线程是一种特殊类型的线程,其存在不会阻止程序的结束。它们常用于执行后台任务和支持功能,但在设计和使用守护线程时需要注意线程的可靠性和行为的不确定性。
守护线程(Daemon Thread)是在Java中的一种特殊类型的线程。与普通线程(用户线程)不同,守护线程的存在不会阻止程序的结束。在理解守护线程的全部详细细节时,以下是需要了解的重要概念和行为:
守护线程定义:通过将线程对象的setDaemon(true)
方法设置为true
,可以将线程设置为守护线程。默认情况下,线程是非守护线程(用户线程)。
程序结束条件:当所有非守护线程(用户线程)执行完毕且没有活动非守护线程时,Java程序将自动退出。守护线程的结束并不会阻止程序的退出。
生命周期管理:守护线程的生命周期与程序的生命周期相互关联。当所有非守护线程结束时,Java虚拟机(JVM)会检查是否还有活动的守护线程。如果没有,JVM会自动退出。
资源回收:守护线程通常用于执行一些后台任务,如垃圾回收(Garbage Collection)等。当所有非守护线程结束时,JVM会自动停止守护线程并释放相关资源。
不可靠性:守护线程可能会在任何时候被中断,甚至在执行过程中被强制终止。这是因为守护线程的执行时间不受程序的控制,而是由JVM自主决定。
父线程的特性继承:守护线程的创建是在其父线程中进行的。因此,父线程结束后,守护线程会继承父线程的特性,包括线程优先级、线程组等。
需要注意的是,守护线程适用于执行一些后台任务或提供支持功能的线程,而不应该用于执行关键任务或涉及重要数据的线程。由于守护线程的不可靠性和无法控制的特性,它们可能无法完成预期工作。
总结起来,普通线程是在Java中常见的线程类型,具有自己的生命周期、并发执行、同步机制和线程间通信等特性。了解普通线程的详细细节可以帮助你编写正确、可靠的多线程程序。
普通线程(用户线程)是在Java中最常见的线程类型。与守护线程不同,普通线程的存在会阻止程序的结束。以下是关于普通线程的全部详细细节:
线程创建和启动:使用Thread类或其子类创建线程对象,并调用start()
方法来启动线程的执行。每个线程都有一个对应的线程控制块(Thread Control Block,TCB),包含线程的状态、优先级、栈等信息。
生命周期管理:线程的生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)等状态。线程通过状态的转换来管理其生命周期。
并发执行:多个普通线程可以并发执行,共享CPU时间片并交替执行。线程调度器负责根据调度算法决定线程的执行顺序和时间片分配。
线程同步:线程同步是确保多个线程之间按照预期的顺序和方式访问共享资源的机制。常用的线程同步机制包括互斥锁(Mutex)、信号量(Semaphore)、条件变量(Condition Variable)等。
线程间通信:线程间通信是指不同线程之间交换信息或协调行动的机制。常用的线程间通信机制包括共享内存、管道、消息队列、信号量等。
异常处理:线程可能抛出异常,因此需要适当地处理线程抛出的异常。可以使用try-catch
块来捕获并处理线程中的异常。
线程优先级:线程可以设置优先级来指示其相对重要性和调度顺序。优先级较高的线程在竞争CPU时间时具有较高的概率被调度执行。
线程安全性:线程安全性涉及保护共享资源免受并发访问的影响。通过使用同步机制、原子操作、线程安全的数据结构等,可以确保多线程环境下的数据一致性和正确性。
需要注意的是,普通线程的执行顺序和时间片分配是由线程调度器决定的,因此无法精确控制线程的执行顺序。并且,在多线程编程中需要小心处理共享资源的并发访问,以避免竞态条件和数据不一致性的问题。
run()
方法来定义线程的执行逻辑。start()
方法来启动线程的执行。start()
方法会在新的线程中调用run()
方法。start()
方法后,线程从新建状态转换为就绪状态,然后由线程调度器决定何时转换为运行状态。run()
,用于定义线程的执行逻辑。run()
方法没有参数和返回值,需要在实现类中重写该方法,并在其中编写线程的具体执行逻辑。start()
方法来启动使用Runnable实现的线程执行。start()
方法后,会创建一个新的线程,并自动调用Runnable对象的run()
方法来执行线程的逻辑。run()
方法,否则只会在当前线程中以普通方法的方式执行,而不会启动新的线程。run()
方法正常返回来结束,或者通过调用Thread类的interrupt()
方法来中断线程的执行。run()
方法来定义线程的执行逻辑。start()
方法启动线程的执行。调用start()
方法后,会自动调用线程的run()
方法。sleep()
、join()
、interrupt()
等。run()
,用于定义线程的执行逻辑。start()
方法启动线程的执行,该线程会自动调用Runnable对象的run()
方法。线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
1. 新建(New)
当声明并创建一个Thread类或其子类的对象时,新生的线程对象处于新建状态。在这个阶段,JVM为该线程对象分配内存,并初始化实例变量的值。与其他Java对象一样,线程对象在新建状态下没有任何线程的动态特征,程序也不会执行它的线程体run()。
2. 就绪(Runnable)
但是,当线程对象调用了start()方法之后,线程的状态发生变化,从新建状态转为就绪状态。JVM会为该线程创建方法调用栈和程序计数器。然而,**处于就绪状态的线程并没有开始运行,它只是表示已具备了运行的条件,并随时可以被调度。**线程的具体调度时机取决于JVM中的线程调度器。
注意:
程序只能在新建状态的线程上调用start()方法,并且只能调用一次。如果对非新建状态的线程(如已启动的线程或已死亡的线程)调用start()方法,将会引发IllegalThreadStateException异常。
3. 运行(Running)
一旦就绪状态的线程获得CPU资源,开始执行其线程体run()中的代码,该线程就进入运行状态。在单核处理器的计算机上,任何时刻只能有一个线程处于运行状态。然而,在多核处理器上,多个线程可以并行执行。
然而,运行状态是短暂的,因为CPU需要公平地分配资源。对于采用抢占式调度策略的系统来说,系统会为每个可执行的线程分配一个小时间段来处理任务。当该时间段用完时,系统会剥夺该线程所占用的资源,使其返回到就绪状态,等待下一次被调度。此时,其他线程将获得执行的机会。在选择下一个线程时,系统会适当考虑线程的优先级。
4. 阻塞(Blocked)
当正在运行的线程遇到以下情况时,它会让出CPU并暂时中止执行,进入阻塞状态:
当正在执行的线程被阻塞后,其他线程将有机会执行。对于上述情况,当发生以下情况时,线程将解除阻塞,重新进入就绪状态,等待线程调度器再次调度:
5. 死亡(Dead)
线程以以下三种方式之一结束,结束后进入死亡状态:
在Java的java.lang.Thread.State
枚举类中,定义了以下状态:
NEW(新建)
:表示线程刚被创建但尚未启动,即还未调用start
方法。
RUNNABLE(可运行)
:此状态没有区分就绪和运行状态。对于Java对象而言,只能标记为可运行。具体的运行时间由操作系统进行调度,因此对于Java对象的状态来说,无法区分它们在什么时候运行。
TERMINATED(被终止)
:表示线程已经结束其生命周期,不再运行。
需要重点说明的是,根据Thread.State
的定义,阻塞状态分为三种:BLOCKED
、WAITING
和TIMED_WAITING
。
BLOCKED(锁阻塞)
:该状态在API中的介绍为:线程正在等待获取一个监视器锁(锁对象)。只有获得锁对象的线程才能继续执行。
RUNNABLE
状态,而线程B则进入BLOCKED
状态。TIMED_WAITING(计时等待)
:该状态在API中的介绍为:线程正在等待另一个线程执行某个(唤醒)动作,但设置了一个限定时间。
Thread
类的sleep
或join
方法、Object
类的wait
方法,或LockSupport
类的park
方法,并且在调用这些方法时设置了时间限制,线程将进入TIMED_WAITING
状态,直到指定的时间到达或被中断。WAITING(无限等待)
:该状态在API中的介绍为:线程正在无限期地等待另一个线程执行某个特殊的(唤醒)动作。
Object
类的wait
方法、Thread
类的join
方法,或LockSupport
类的park
方法,并且在调用这些方法时未指定等待时间,线程将进入WAITING
状态,直到被唤醒。
Object
类的wait
方法进入WAITING
状态时,需要使用Object
的notify
或notifyAll
方法进行唤醒。Condition
的await
方法进入WAITING
状态时,需要使用Condition
的signal
方法进行唤醒。LockSupport
类的park
方法进入WAITING
状态时,需要使用LockSupport
类的unpark
方法进行唤醒。Thread
类的join
方法进入WAITING
状态时,只有当调用join
方法的线程对象结束时,当前线程才能恢复。需要注意的是,当从WAITING
或TIMED_WAITING
状态恢复到RUNNABLE
状态时,如果发现当前线程未获取到监视器锁,则立即转入BLOCKED
状态。
在仔细研究API文档时,我们可以明显观察到Timed Waiting(计时等待)和Waiting(无限等待)状态之间存在密切联系。
在Waiting状态中,wait方法是不带参数的。这种情况下,线程陷入了无限期等待状态,类似于我们日常生活中设定的一个没有具体时间限制的闹钟。只有在某个特定条件满足时,通过调用相应的唤醒方法,才能使线程从这种状态中恢复。这样的设计方案旨在确保线程在等待期间不会被不必要地唤醒,以提高系统的效率。
而在Timed Waiting状态中,wait方法是带有参数的。这种情况下,我们可以将其类比为设置了倒计时的闹钟。我们设定了一个特定的时间,在时间到达时,线程会自动被唤醒。然而,如果在线程等待期间提前收到唤醒通知,那么预先设定的倒计时就变得多余了,因为此时不再需要等待预定的时间。因此,这种设计方案可以同时满足两种需求:如果未收到唤醒通知,线程将保持在Timed Waiting状态,直到倒计时结束自动唤醒;如果在倒计时期间收到唤醒通知,线程将立即从Timed Waiting状态被唤醒。
此设计方案的优势在于根据具体需求灵活地控制线程的等待时间,并在必要时及时唤醒线程,以提高程序的效率。
synchronized
关键字或ReentrantLock
类)来保证同一时间只有一个线程可以修改共享数据。volatile
关键字来确保数据的可见性,即一个线程对数据的修改对其他线程是可见的。Atomic
类提供的原子操作,如AtomicInteger
、AtomicLong
等,来保证对数据的原子性操作。ConcurrentHashMap
、ConcurrentLinkedQueue
等,来避免竞态条件。ReentrantLock
类提供的tryLock()
方法来尝试获取锁,并设置超时时间,避免线程长时间等待。wait()
、notify()
和notifyAll()
等方法进行线程的等待和唤醒操作。Lock
接口提供的条件变量(Condition
)进行线程间的通信。BlockingQueue
、CountDownLatch
、CyclicBarrier
等)来实现线程间的同步和通信。线程同步是一种机制,用于解决多线程并发访问共享资源时可能引发的线程安全问题。当多个线程同时访问共享资源时,如果没有适当的同步机制,可能会导致数据不一致、竞态条件和其他并发问题。
在线程同步中,常用的同步机制包括锁、互斥量、信号量和条件变量等。这些机制都提供了一种方式,让线程能够互斥地访问共享资源,即一次只允许一个线程访问资源,其他线程需要等待。
同步机制的核心思想是通过引入临界区(Critical Section)来保护共享资源。临界区是指一段代码,只有一个线程可以进入执行,其他线程必须等待。通过对临界区的控制,可以确保共享资源在任意时刻只被一个线程访问,从而避免并发访问引发的问题。
在实现线程同步时,需要注意以下几点:
同步机制的原理实际上是给某段代码加上了"锁",任何线程在执行这段代码之前都必须先获取这个"锁",我们称之为同步锁。Java对象在堆中的数据分为对象头、实例变量和空白填充。其中,对象头包含以下内容:
当某个线程获取了同步锁对象后,同步锁对象会记录该线程的ID,这样其他线程就只能等待。除非该线程释放了锁对象,其他线程才能重新获取/占用同步锁对象。
同步代码块:synchronized关键字可以用于某个代码块之前,表示对该代码块的资源进行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步方法:synchronized关键字直接修饰方法,表示在同一时刻只能有一个线程进入该方法,其他线程在外部等待。
public synchronized void method(){
可能会产生线程安全问题的代码
}
在《Thinking in Java》中,对并发工作的解释如下:在处理并发任务时,你需要一种方法来防止两个任务访问相同的资源(即共享资源竞争)。防止此类冲突的方法是在任务使用资源时对其进行加锁。第一个访问某个资源的任务必须锁定该资源,使其他任务无法在解锁之前访问它,而在解锁时,另一个任务可以锁定并使用该资源。
同步锁对象可以是任意类型,但必须确保多个线程竞争同一个共享资源时使用相同的同步锁对象。
对于同步代码块而言,同步锁对象由程序员手动指定(通常为this或类名.class),但对于同步方法而言,同步锁对象只能是默认的:
同步代码块:
同步代码块使用synchronized
关键字来标记一段代码,用于控制多个线程对共享资源的访问。
语法格式为:
synchronized (锁对象) {
// 需要同步的代码
}
```
锁对象可以是任意Java对象。当多个线程进入同步代码块时,它们会按顺序尝试获取锁对象并执行同步代码块中的代码。
同步代码块只会对锁对象进行加锁,其他非同步的代码块和方法不受影响。
当线程执行完同步代码块或遇到异常时,会释放锁对象,使其他线程可以获取锁并执行同步代码块。
同步方法:
同步方法是一种特殊的方法,使用synchronized
关键字修饰,用于实现对共享资源的线程安全访问。
在同步方法中,锁对象是方法所属对象(对于静态方法是类的Class对象)。
语法格式为:
public synchronized void methodName() {
// 需要同步的代码
}
```
当一个线程进入同步方法时,它会尝试获取该方法所属对象的锁。如果锁可用,线程将执行同步方法中的代码;否则,线程将被阻塞,直到锁可用。
同步方法会自动获取和释放锁,无需在代码中显式编写获取和释放锁的逻辑。
同一个对象的其他同步方法在同一时间只能被一个线程执行,但其他非同步方法不受影响。
注意事项:
如何发现问题,即代码是否存在线程安全问题(非常重要):
(1)明确哪些代码是多线程运行的代码。
(2)明确多个线程是否共享数据。
(3)明确多线程运行代码中是否包含多条语句操作共享数据。
如何解决问题(非常重要):
对于多条操作共享数据的语句,只能让一个线程执行完它们,在执行过程中,其他线程不能参与执行。
换句话说,所有操作共享数据的语句都必须放在同步范围内。
切记:
范围太小:不能解决安全问题。
范围太大:因为一旦某个线程获取到锁,其他线程就只能等待,所以范围太大会降低效率,无法合理利用CPU资源。
线程同步是一种机制,用于在多线程环境中协调线程的执行顺序,以避免数据竞争和不一致的问题。
synchronized关键字:
对象锁:
类锁:
实例锁与对象锁:
内置锁和重入性:
锁的释放:
volatile关键字:
Lock接口:
synchronized和Lock是Java中用于实现线程同步的两种机制,它们在实现方式、功能和使用方面有一些区别。
需要根据具体的需求和场景选择使用synchronized还是Lock。synchronized是Java内置的同步机制,使用简单,适用于大多数情况下的线程同步;而Lock提供了更高级的功能和灵活性,适用于复杂的同步场景。
说明:开发建议中处理线程安全问题优先使用顺序为:
• Lock ----> 同步代码块 ----> 同步方法
下面是它们的详细对比:
1. 实现方式:
2. 锁的获取与释放:
3. 锁的灵活性:
4. 锁的公平性:
5. 可重入性:
6. 性能:
7. 锁的可用性:
8. 适用场景:
特点 | synchronized | Lock |
---|---|---|
实现方式 | 关键字 | 接口 |
锁的获取与释放 | 自动获取和释放锁 | 手动获取和释放锁 |
锁的灵活性 | 低 | 高 |
中断获取锁的线程 | 不支持 | 支持 |
设置获取锁的超时时间 | 不支持 | 支持 |
锁的公平性 | 非公平锁(默认) | 可以选择公平锁或非公平锁 |
可重入性 | 支持 | 支持 |
性能 | 相对较低,因为可能存在竞争和上下文切换开销 | 相对较高,因为允许更细粒度的控制和更灵活的线程调度 |
锁的可用性 | 无法监控和管理锁的状态 | 可以监控和管理锁的状态,例如判断锁是否被占用、获取等待线程数量 |
适用场景 | 简单的同步需求,易于使用和理解 | 复杂的同步需求,需要更灵活的控制和功能 |
死锁是多线程编程中的一种常见并发问题,指的是两个或多个线程互相持有对方所需的资源,并且由于无法获取到对方所持有的资源而陷入无限等待的状态。
死锁条件:
死锁示例:
死锁的影响:
预防和避免死锁:
检测和恢复死锁:
解决死锁:
一旦死锁发生,往往难以通过人为干预来解决,因此我们只能尽量规避死锁的产生。为了打破诱发死锁的条件,我们可以考虑以下方法:
针对条件1 互斥条件(Mutual Exclusion):互斥条件基本上无法被破坏,因为线程需要通过互斥来解决资源的安全性问题。
针对条件2 请求与保持条件(Hold and Wait)(占用且等待):可以考虑一次性申请所有所需的资源,这样就消除了等待的问题,避免了死锁的发生。
针对条件3 不可剥夺条件(No Preemption):当线程在占用部分资源后进一步申请其他资源时,若无法获取所需资源,应主动释放已经占用的资源。
针对条件4 循环等待条件(Circular Wait):可以将资源按线性顺序进行分配。在申请资源时,优先申请序号较小的资源,以避免循环等待的情况。
线程间通信是多线程编程中的重要概念,用于不同线程之间的数据交换和协调。
共享内存:
线程间通信的方法:
等待/通知机制:
阻塞队列:
信号量:
栅栏:
计数器:
条件变量:
为何需要处理线程间通信:
在需要多个线程共同完成任务且希望它们按照一定规律执行的情况下,多线程之间需要一些通信机制来协调它们的工作,以实现多线程对共享数据的协同操作。
例如,假设线程A负责生产包子,线程B负责消费包子,这里的包子可以视为共享资源。线程A和线程B执行的动作分别是生产和消费,但线程B必须等待线程A完成生产后才能执行。因此,线程A和线程B之间需要线程通信机制,即等待唤醒机制。
等待唤醒机制是多个线程之间的一种协作机制。我们通常提到线程时常将其与竞争(race)联系在一起,例如争夺锁,但这并不是线程故事的全部,线程之间也存在协作机制。
在某个线程满足特定条件时,它会进入等待状态(wait() / wait(time)),等待其他线程执行完特定代码后将其唤醒(notify())。还可以指定等待的时间,时间到达后自动唤醒。当有多个线程进行等待时,可以使用notifyAll()来唤醒所有等待的线程。wait/notify就是线程之间的一种协作机制。
注意:
被通知的线程被唤醒后,不一定能立即恢复执行,因为它之前中断的地方是在同步块内,而此时它已经不持有锁。因此,它需要再次尝试获取锁(可能会面临其他线程的竞争)。只有成功获取锁后,才能在之前调用wait方法的地方继续执行。
总结如下:
- 如果能获取锁,线程从WAITING状态变为RUNNABLE(可运行)状态。
- 否则,线程从WAITING状态又变为BLOCKED(等待锁)状态。
wait()
方法与notify()
方法必须要由同一个锁对象调用。这是因为只有通过相同的锁对象调用notify()
方法,才能唤醒使用相同锁对象调用wait()
方法后的线程。这种同步保证了线程之间的正确通信和协作。
wait()
方法与notify()
方法是属于Object
类的方法。这是因为锁对象可以是任意对象,而任意对象的所属类都是继承了Object
类的。因此,所有的对象都可以调用wait()
和notify()
方法来实现线程间的等待和唤醒操作。
wait()
方法与notify()
方法必须要在同步代码块或者是同步函数中使用。这是因为在调用这两个方法时,必须通过锁对象来进行调用。否则,将会抛出java.lang.IllegalMonitorStateException
异常。通过在同步代码块或同步函数中使用wait()
和notify()
方法,我们可以保证线程在正确的同步环境下进行等待和唤醒操作,从而避免出现并发访问数据的问题。
当使用wait()
方法时,需要在while
循环中检查条件。这是为了防止虚假唤醒(spurious wakeup)的情况发生,在线程被唤醒时重新检查条件是否满足。
在Java中,等待唤醒机制只适用于在同一个对象上等待的线程之间的通信。如果需要跨多个对象进行线程通信,可以考虑使用Lock
和Condition
接口提供的等待唤醒机制。
相同点:
不同点:
sleep()
: 定义在Thread类中。wait()
: 定义在Object类中。sleep()
可以在任何需要使用的位置被调用;wait()
必须在同步代码块或同步方法中使用。sleep()
不会释放同步监视器;wait()
会释放同步监视器。sleep()
:指定时间一到就结束阻塞。wait()
:可以指定时间,也可以无限等待直到被notify()
或notifyAll()
唤醒。任何线程在进入同步代码块或同步方法之前,必须先获取对同步监视器的锁定。那么,什么时候会释放对同步监视器的锁定呢?
以下情况下,当前线程会释放对同步监视器的锁定:
以下情况下,线程执行同步代码块或同步方法时,不会释放锁(同步监视器):
请注意,应尽量避免使用过时的suspend()和resume()方法来控制线程。
线程池是一种用于管理和重用线程的机制,它可以提高线程的创建和销毁效率,并有效控制并发线程的数量。
这些是线程池的一些关键细节和常见配置,实际使用线程池时,可以根据具体需求来选择适当的线程池实现和配置参数。
线程池组成:线程池由以下组件组成:
线程池工作原理:
线程池的优势:
线程池的参数和配置:
常见的线程池实现:
在JDK5.0之前,我们必须手动自定义线程池。然而,从JDK5.0开始,Java提供了内置的线程池相关API,位于java.util.concurrent
包下。这些API包括ExecutorService
和Executors
。
ExecutorService
是真正的线程池接口,其常见子类为ThreadPoolExecutor。它提供以下方法:
void execute(Runnable command)
: 用于执行任务或命令,没有返回值。一般用于执行实现了Runnable接口的任务。 Future submit(Callable task)
: 用于执行任务,具有返回值。通常用于执行实现了Callable接口的任务。void shutdown()
: 用于关闭线程池,释放相关资源。Executors
是一个线程池的工厂类,通过其静态工厂方法可以创建多种类型的线程池对象。以下是其常用方法:
Executors.newCachedThreadPool()
: 创建一个可根据需要创建新线程的线程池,适用于执行大量短期任务的场景。Executors.newFixedThreadPool(int nThreads)
: 创建一个可重用固定线程数的线程池,适用于控制并发线程数量的场景。Executors.newSingleThreadExecutor()
: 创建一个只有一个线程的线程池,适用于需要保证任务按顺序执行的场景。Executors.newScheduledThreadPool(int corePoolSize)
: 创建一个线程池,它可以安排在给定延迟后运行命令或定期执行任务的场景。Callable接口是Java中用于表示可返回结果并可能抛出异常的任务的接口。它是Java并发编程中的一部分,位于java.util.concurrent
包下。下面是Callable接口的全部详细细节:
public interface Callable<V> {
V call() throws Exception;
}
其中,V
表示任务执行完毕后的返回值类型。
call()
方法:Callable接口中唯一的方法,用于执行具体的任务逻辑。该方法可以抛出Exception
,允许任务在执行过程中抛出异常。call()
方法返回一个泛型类型的结果,即V
类型的值。Future
对象来获取任务的执行结果。call()
方法可以抛出异常,而Runnable接口的run()
方法不能抛出受检查异常。submit()
方法提交给线程池执行,而Runnable任务可以使用execute()
方法或submit()
方法提交给线程池执行。通过实现Callable接口,可以定义具有返回值的任务,并利用线程池进行执行和管理。这使得并发编程更加灵活,可以获取任务的执行结果,并进行后续处理。
Future接口是Java中用于表示异步计算结果的接口,它是Java并发编程中的一部分,位于java.util.concurrent
包下。下面是Future接口的全部详细细节:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
其中,V
表示异步计算的结果类型。
cancel(boolean mayInterruptIfRunning)
方法:尝试取消任务的执行。mayInterruptIfRunning
参数表示是否允许中断正在执行的任务。isCancelled()
方法:判断任务是否被取消。isDone()
方法:判断任务是否已完成(无论是正常完成、被取消还是异常完成)。get()
方法:获取异步计算的结果,如果任务还未完成,则会阻塞当前线程直到任务完成并返回结果。该方法可能抛出InterruptedException
和ExecutionException
异常。get(long timeout, TimeUnit unit)
方法:在指定的时间内获取异步计算的结果,如果任务还未完成,则会阻塞当前线程直到任务完成并返回结果,或者超过指定的时间。该方法可能抛出InterruptedException
、ExecutionException
和TimeoutException
异常。cancel()
方法返回一个boolean值,表示是否成功取消任务。isCancelled()
方法返回一个boolean值,表示任务是否被取消。isDone()
方法返回一个boolean值,表示任务是否已完成。get()
方法返回异步计算的结果,类型为V
。通过使用Future接口,可以异步执行计算任务,并在需要时获取计算结果。这在并发编程中非常有用,可以提高程序的性能和响应性。