文章目录
线程概述
单线程的程序往往功能非常有限,例如开发一个简单的服务器程序,这个服务器程序需要向不同的客户端提供服务时,不同的客户端之间应该互不干扰,否则会让客户端感到非常沮丧。多线程的程序则可以包括多个顺序执行流,多个顺序流之间互不干扰。可以这样理解:单线程的程序如同只雇佣了一个服务员的餐厅,他必须做完一件事后才可以做下一件事情:多线程的程序则如同雇佣多个服务员的餐厅,她们可以同时做多件事情。
理解线程和进程的区别与联系
线程的基本知识
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
线程和进程
当一个程序进入内存运行时,即变成了一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程包括如下三个特征:
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
并发性和并行性是两个概念。
并行指在同一时刻,有多条指令在多个处理器上同时执行
并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
多线程则拓展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程,线程是进程的执行单位。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些程序执行流就是线程,每个线程也是互相独立的。
- 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但也要小心,因为需要确保线程不会妨碍同一进程里的其他线程。
- 线程看完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境
- 线程是独立运行的,它并不知道进程中是否还有其他线程存在。所以线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
多线程的优势
线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程之间的隔离程度要小。
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性—多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。
使用多线程编程的几个优点:
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
- Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
在实际应用中,多线程非常有用的,一个浏览器必须能同时下载多个图片;一个web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收…总之,多线程在实际编程中的应用是非常广泛的。
创建线程的方式
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。
继承Thread类创建线程类
通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
package 多线程.线程的基本使用;
//通过继承Thread类来创建线程类
public class FirstThread extends Thread{
private int i;
@Override
public void run() {
for (; i < 100; i++) {
//当线程类继承Thread类时,直接使用this即可获取当前线程
//Thread对象的getName()返回当前线程的名字
// 因此可以直接调用getName()方法返回当前线程的名字
System.out.println(getName()+" " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20){
//创建第一条线程
new FirstThread().start();
//创建第二条线程
new FirstThread().start();
}
}
}
}
结果为:(截取一部分)
main 16
main 17
main 18
main 19
main 20
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
main 21
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-1 6
Thread-1 7
Thread-1 8
当循环变量i等于20时创建并启动两个新的线程。虽然上面显式地创建并启动了两个线程,但实际上程序有三个线程,即程序显式创建的2个子线程和主线程。前面已经提到了,当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的—main()方法的方法体代表主线程的线程执行体
实现Runnable接口创建线程类
实现Runnable接口来创建并启动多线程的步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
//创建Runnable实现类对象
SceondThread st = new SecondThread();
//以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
new Thread(st);
//也可以创建Thread对象时该Thread对象指定一个名字
//创建Thread对象时指定target和新线程的名字
new Thread(st,"新线程1");
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
package 多线程.线程的基本使用;
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for (; i < 100; i++) {
//当线程类实现Runnable接口时
//如果想获取当前线程,只能用Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()+" " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20){
SecondThread st = new SecondThread();
new Thread(st,"新线程1").start();
new Thread(st,"新线程2").start();
}
}
}
}
对比FirstThread中的run()方法体和SecondThread中的run()方法体并不难发现,通过继承Thread类来获得当前线程对象比较简单,直接使用this就可以了;但通过实现Runnable接口来获得当前线程对象,则必须使用Thread.currentThread()方法。
除此之外还有创建线程对象的方式也有所区别:前者直接创建的Thread子类即可代表线程对象;后者创建的Runnable对象只能作为线程对象的target
继承Thread,这种方式的类代表该类就是线程类
实现Runnable接口,这种方式并不是代表该类就是线程类,只是代表该类具备线程的能力
线程的run()方法和start()方法的区别与联系
run()方法中的代码就是线程要做的事情
start()开启线程
线程是由“JVM线程调度器”来调起执行的!哪条线程先执行由“JVM线程调度器”来决定!!
并不是调用了线程的start()方法就立马执行,而是进入了“就绪状态”,随时等待JVM来调度
线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、和死亡(Dead)5种状态。尤其是当线程启动以后,他不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
线程的状态:
- 创建对象
- 就绪
- 运行
- 阻塞
- 终止
新建状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
就绪状态
当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度
启动线程使用start()方法,而不是run()方法!!永远不要调用线程对象的run()方法!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行----也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
运行状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。
对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一线程时,系统会考虑线程的优先级。
阻塞状态
所有现代的桌面和服务器操作系统都采用抢占式调度策略。在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源–也就是必须由该线程主动放弃所占用的资源。
当发生如下情况时,线程将会进入阻塞状态:
1. 线程调用sleep()方法主动放弃所占用的处理器资源。
2. 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4. 线程在等待某个通知(notify)。
5. 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞接触后,必须重新等待线程调度器再次调度它。
针对如上几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。
1. 调用sleep()方法的线程经过了指定时间。
2. 线程调用的阻塞式IO方法已经返回。
3. 线程成功地获得了试图取得的同步监视器。
4. 线程正在等待某个通知时,其他线程发出了一个通知。
5. 处于挂起状态的线程被调用了resume()恢复方法。
总结
- 线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。
- 就绪状态和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。
- 还有一个特例!调用yield()方法可以让运行状态的线程转入就绪状态。
线程死亡的几种情况
- 正常执行完run()方法。
- run()方法执行过程中出现异常尚未捕获
- 手动终止:1.调用了stop()方法(不推荐使用,该方法已过时);2.也可以通过定义一个boolean类型的变量来控制线程的终止
检测线程的活动状态可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡两种状态时,该方法返回false。
不要对处于死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的。这都会引发IllegalThreadStateException异常。
控制线程的常用方法
join线程
Thread提供了一个让线程等待另一个线程完成的方法—join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
package 多线程.线程控制;
import jdk.nashorn.internal.scripts.JO;
public class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+ " " + i);
}
}
public static void main(String[] args) throws Exception{
new JoinThread("新线程").start();
for (int i = 0; i < 100; i++) {
if (i == 20){
JoinThread jt = new JoinThread("被Join的线程插队");
jt.start();
// main线程调用了jt线程的join()方法,nain线程必须等待jt执行结束后才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
被Join的线程插队 92
被Join的线程插队 93
被Join的线程插队 94
被Join的线程插队 95
被Join的线程插队 96
被Join的线程插队 97
被Join的线程插队 98
被Join的线程插队 99
main 20
main 21
main 22
main 23
main 24
main 25
上面一共有三个线程,主方法开始时就启动了名为“新线程”的子线程,该子线程将会和main线程并发执行。当主线程的循环变量i等于20时,启动了名为“被Join的线程插队”的线程,该线程不会和main线程并发执行,main线程必须等该线程执行结束后才可以向下执行。在名为“被Join的线程插队”的线程执行时,实际上只有2个子线程并发执行,而主线程处于等待状态。
join()方法属于Thread类的方法,让某线程对象加入到另外一个线程对象结尾
thread1.join() “当前线程”要加入到“thread1对象”的末尾去执行(谁点谁在前,当前在后面)
守护线程
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又被称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
package 多线程.线程守护;
public class TestDaemonThread {
public static void main(String[] args) {
//开辟一条子线程
Thread t1 = new Thread(new Task());
t1.setDaemon(true);
t1.start();
//主线程
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("["+Thread.currentThread().getName()+"]"+">>>>"+i);
}
}
private static class Task implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("["+Thread.currentThread().getName()+"]"+">>>>"+i);
}
}
}
}
运行结果:
[main]>>>>0
[Thread-0]>>>>0
[main]>>>>1
[main]>>>>2
[Thread-0]>>>>1
[main]>>>>3
[main]>>>>4
[Thread-0]>>>>2
[main]>>>>5
[main]>>>>6
[Thread-0]>>>>3
[main]>>>>7
[main]>>>>8
[Thread-0]>>>>4
[main]>>>>9
调用Thread对象的setDaemon(true)方法可将指定线程设置为后台线程。可以看到当前台线程执行完之后,后台线程不会执行完的。
改变线程优先级(建议JVM去做)
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority(int newPriority)的参数范围1~10,也可以使用Thread类的三个静态常量。
- MAX_PRIORITY: 其值是10
- MIN_PRIORITY: 其值是1
- NORM_PRIORITY: 其值是5
线程同步的概念和必要性
当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。
因为多条线程同时操作同一个数据时,有可能造成数据混乱(不安全)
如何保证线程安全问题??
使用同步来解决
使用synchronized控制线程同步
Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。
synchronized(锁对象){
...
//此处的代码就是同步代码块(受保护资源)
}
上面的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。当同步代码块结束后该线程就会释放同步锁。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
通过这种方法可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。
⚠️注意事项
- 允许任何对象充当锁
- 锁对象必须是开辟了空间(不允许为null,否则将会抛出NullPointException异常)
- 要保证使用的是同一个锁对象
同步方法
与同步代码块对于,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显示指定同步监视器,同步方法的监视器是this,也就是调用该方法的对象。
synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。
释放同步监视器的锁定
程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
- 当前线程在同步方法、同步代码块中遇到了break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器
- 当前线程在同步方法、同步代码块中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
- 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步代码块。
如下情况,线程不会释放同步监视器
- 线程执行同步方法或同步代码块时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。当然,程序应该尽量避免使用suspend()和resume()方法来控制线程。
使用Lock对象控制线程同步
从Java5开始,Java提供了一种功能更强大的线程同步机制----通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,格式为:
class x
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 定义需要保证线程安全的方法
public void m(){
// 加锁
lock.lock();
try
{
//...method body
}
// 使用finally块来保证释放锁
finally
{
lock.unlock();
}
}
}
使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁-修改-释放锁”的操作模式
ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序即不会发生异常,也不会给出任何提示,只有所有线程处于阻塞状态,无法继续。
package 多线程;
public class 死锁问题 {
//创建两个对象来充当synchronized的锁对象
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread1().start();
new Thread2().start();
}
private static class Thread1 extends Thread{
@Override
public void run() {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1拿到了lock1锁");
synchronized (lock2){
System.out.println("Thread1拿到了lock2锁");
}
}//释放lock1
System.out.println("Thread1执行完毕");
}
}
private static class Thread2 extends Thread{
@Override
public void run() {
synchronized (lock2){
System.out.println("Thread2拿到了lock2锁");
synchronized (lock1){
System.out.println("Thread2拿到了lock1锁");
}
}
System.out.println("Thread2执行完毕");
}
}
}
Thread2拿到了lock2锁
Thread1拿到了lock1锁
.......(程序未结束,一直卡住)
Thread2等着lock1的锁,Thread1等着lock2的锁。两个线程互相等待对方先释放,所以出现了死锁。
解决方案
package 多线程;
public class 死锁问题 {
//创建两个对象来充当synchronized的锁对象
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
new Thread1().start();
new Thread2().start();
}
private static class Thread1 extends Thread{
@Override
public void run() {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1拿到了lock1锁");
synchronized (lock2){
System.out.println("Thread1拿到了lock2锁");
}
}//释放lock1
System.out.println("Thread1执行完毕");
}
}
private static class Thread2 extends Thread{
@Override
public void run() {
synchronized (lock1){
System.out.println("Thread2拿到了lock2锁");
synchronized (lock2){
System.out.println("Thread2拿到了lock1锁");
}
}
System.out.println("Thread2执行完毕");
}
}
}
Thread1拿到了lock1锁
Thread1拿到了lock2锁
Thread1执行完毕
Thread2拿到了lock2锁
Thread2拿到了lock1锁
Thread2执行完毕
线程通信
当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,但Java提供了一些机制来保证线程协调运行
假设现在系统中有两个线程,这两个线程分别代表存款者和取钱者---假设系统有一种特殊的要求,
系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,
取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
我们可以借助Object类提供的wait()、notify()和notifyAll()这三个方法,这三个方法并不属于Thread类,而是属于Object类。但是这三个方法必须由同步监视器对象来调用
- 对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
- 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。调用wait()方法的当前线程会释放对该同步监视器的锁定
notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
⚠️注意!!!
抛出异常java.lang.IllegalMonitorStateException(违法的监听器状态异常)的原因:
- 没有在同步代码中使用
- 没有使用锁对象调用wait()
- 这把锁对象,锁住的不是当前线程
sleep()和wait()方法有什么区别?
1. sleep()不会释放锁,wait()会释放锁
2. sleep()属于Thread类的类方法,wait()属于Object类的实例方法
3. sleep()可以在任何地方直接使用,wait()只能出现在同步方法或者同步代码块中
线程池的功能和用法
一台服务器完成一个客户端请求任务一定要经过下面几个阶段:
- 创建线程(内存消耗)
- 执行线程中的任务
- 销毁线程(内存消耗)
假设一台服务器一天处理50000个请求,并且每一个请求都需要一个单独的子线程来完成,如果服务器不重复利用这些线程,那么将需要创建50000条线程!!!(浪费内存)
如果服务器本身的支持并发数只有10000,那么剩下的40000条线程也开辟了内存并且都没有执行(浪费时间)
解决方法: 使用线程池,提高效率,提升内存的使用率!!
线程池的作用:能够很好的管理多条线程,能够使得线程复用!!!
Java中通过Executors来创建线程池,有四种线程池:
-
ExecutorService pool = Executors.newCachedThreadPool();
该线程池中的线程是异步(并发)的
创建了一个可缓存的线程池
如有限制线程,则拿来服用
如无闲置线程,则会开辟新的线程
-
ExecutorService pool = Executors.newSingleThreadExecutor();
该线程池中的线程是同步的
创建了一个单线程的线程池(里面只维护一条线程),这个线程池中只有一条线程在工作
线程池中所有的任务的执行顺序将按照任务的提交顺序来执行(FIFO)
-
ExecutorService pool = Executors.newFixedThreadPool();
该线程池中的线程是异步(并发)的
创建一个固定大小的线程池,每次提交一个任务都会创建一个线程(除非达到线程池的最大数量)
超出的线程会在队列后等待
-
ExecutorService pool = Executors.newScheduledThreadPool();
该线程池中的线程是异步(并发)的