核心类库 - 多线程
1 概述
1.1 线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。
一个进程最少有一个线程,线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
1.2 线程调度
分时调度:所有线程轮流使用 CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。
Java使用的是抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,同一时刻,只能执行一个线程。而CPU在多个线程间的切换速度相当之快,我们并不能感觉出来,因此,看上去就像在同时运行。
事实上,多线程程序并不能提高程序的运行速度,但能够提高程序的运行效率,提高CPU的使用率。
1.3 同步与异步
同步:排队执行,效率较低但是线程安全。
异步:同时执行,效率较高但是线程不安全。
关于线程安全,详见
1.4 并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
并发指的是一个时间段,而并行指的是同时。同时不是你觉得同时就是同时了,CPU有多少个线程决定了最多能同时发生多少个事件。
2 多线程的实现
2.1 继承Thread
Thread是Java所提供的用于实现线程的类,想要开启一个新的执行路径需要继承Thread,并重写run方法,当然这只是其中一种实现方式。
run方法就是线程要执行的任务方法,其中的代码就是一条新的执行路径。
这个执行路径的触发方式不是调用run方法,而是通过Thread对象的start()来启动任务,调用start方法就会启动一个线程,然后在线程里运行run方法。
public static void main(String[] args) {
MyThread m = new MyThread(); //创建线程
m.start(); //启动线程
for (int i=0;i<10;i++) {
System.out.println("汗滴禾下土"+i);
}
}
static class MyThread extends Thread { //线程继承Thread
public void run() { //线程要执行的任务
for (int i=0;i<10;i++) {
System.out.println("锄禾日当午"+i);
}
}
}
这时,两个循环就是并发执行的,由于Java使用的是抢占式调度,所以两个线程执行的速度是不确定的,每一次执行的结果可能都不一样。
这就开启了两个线程。每个线程都拥有自己的栈空间,共用一份堆内存。由一个线程调用的方法,这个方法也会执行在这个线程里面。
2.2 实现Runnable
第二种常见也是很常用的开启多线程的技术,即实现Runnable接口,并实现它的run方法。
public interface Runnable { //Runnable接口
public abstract void run();
}
run方法里面也是线程要执行的任务。
要想把任务执行起来还需要借助Thread,但是不用继承Thread了。
首先创建一个任务对象,然后创建一个线程,并为其分配一个任务,紧接着我们就可以执行这个线程了。
public static void main(String[] args) {
MyRunnable r = new MyRunnable(); //创建任务
Thread t = new Thread(r); //创建线程并为其分配任务
t.start(); //启动线程
for (int i=0;i<10;i++) {
System.out.println("疑是地上霜"+i);
}
}
static class MyRunnable implements Runnable { //实现Runnable接口
public void run() { //线程要执行的任务
for (int i=0;i<10;i++) {
System.out.println("床前明月光"+i);
}
}
}
2.3 两种方式对比
实现Runnable与继承Thread相比有如下的优势:
1.通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。
2.可以避免单继承所带来的局限性。
3.任务与线程本身是分离的,提高了程序的健壮性。
4.后续学习的线程池技术,接受Runnable类型的任务而不接受Thread类型的线程。
但是并不代表继承Thread就不用了,因为它有一个很简答的实现方式。
可以直接通过匿名内部类的方式开启,只需如下代码即可实现多线程。
public static void main(String[] args) {
new Thread() { //创建线程
public void run() { //线程要执行的任务
for (int i=0;i<10;i++) {
System.out.println("床前明月光"+i);
}
}
}.start(); //启动线程
for (int i=0;i<10;i++) {
System.out.println("疑是地上霜"+i);
}
}
2.4 Callable
线程并不是只有两种创建方式的,还有一种比较特殊的方式叫做Callable,这种线程的使用方法和之前的线程不太一样。
public interface Callable<V> { //Callable接口
V call() throws Exception;
}
Callable更像是主线程指派给的一个任务,执行完毕后会有一个结果,这个结果主线程可以拿到。
之前的线程和主线程是并发执行的,但是Callable这种线程既可以实现和主线程是并发执行也可以实现主线程等待此线程执行完毕获得结果。
可以让主线程同时等待多个线程完成多个事情并获得返回值。
使用步骤:
1.编写类实现Callable接口,并实现call方法。
class XXX implements Callable<T> {
public <T> call() throws Exception {
return T;
}
}
2.创建FutureTask对象,并传入第一步编写的Callable类对象。
FutureTask<Integer> future = new FutureTask<>(callable);
3.通过Thread,启动线程。
new Thread(future).start();
常用方法:
1.get()
获取线程执行的结果,如果主线程不调用get方法则不需要等待此线程执行完毕,若调用get方法则需要等待此线程执行完毕。
2.get(long timeout, TimeUnit unit)
获取线程执行的结果,可设置最大等待时间和单位,超时则不继续等待。
3.isDone()
判断线程的任务是否执行完毕,是则返回true,否则返回false。
4.cancel(boolean mayInterruptIfRunning)
传入true以尝试取消线程的执行,若取消成功返回true,否则(一般是线程已经执行完毕了)返回false。
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable(); //创建Callable对象
FutureTask<Integer> task = new FutureTask<>(c); //创建FutureTask对象,传入Callable对象
new Thread(task).start(); //启动线程
Integer j = task.get(); //等待返回值,这行开始main暂停直到收到返回值
System.out.println("返回值是"+j); //输出返回值
}
static class MyCallable implements Callable<Integer> { //实现Callable接口
public Integer call() throws Exception { //线程要执行的任务
System.out.println("正在思考返回值");
Thread.sleep(3000);
return 100; //线程的返回值
}
}
主线程调用了get方法,则会等待子线程执行完毕拿到返回值后才会继续执行。但是并不需要干等子线程执行完毕而不做任何事情。
可以通过隔一段时间使用一次isDone方法先判断子线程是否执行完毕,若执行完毕再调用get方法即可。
和Runnable对比:
相同点:
1.都是接口。
2.都可以编写多线程程序。
3.都采用Thread.start()启动线程。
不同点:
1.Runnable没有返回值;Callable可以返回执行结果。
2.Callable接口的call()允许抛出异常;Runnable的run()不能抛出。
3 Thread类及其使用
3.1 Thread类
两种线程的创建方式不管是通过继承Thread还是实现Runnable,都需要使用到Thread类,这个类就是程序中的线程。
常用字段:
1.MAX_PRIORITY
线程可以拥有的最大优先级。
2.MIN_PRIORITY
线程可以拥有的最低优先级。
3.NORM_PRIORITY
分配给线程的默认优先级。
构造方法:
1.Thread()
创建一个没有名字没有任务的线程。
2.Thread(Runnable target)
传入Runnable,创建一个有任务没有名字的线程。
3.Thread(String name)
传入字符串,创建一个有名字没有任务的线程。
4.Thread(Runnable target, String name)
传入Runnable和字符串,创建一个有名字有任务的线程。
常用方法:
1.currentThread()
返回当前线程的对象。
2.getId()
返回线程的标识符,long类型,不可指定。
3.getName()
返回线程的名字,String类型,可以指定。
4.getPriority()
返回线程的优先级,int类型。
5.interrupt()
给线程添加中断标记。详见 3.4 线程的中断。
6.setDaemon(boolean on)
true则把线程设为守护线程,false则设为用户线程。详见 3.5 守护线程和用户线程。
7.setName(String name)
修改线程的名字。
8.setPriority(int newPriority)
调整线程的优先级,通过传入常用字段的3个常量。
9.sleep(long millis)
让线程暂时停止执行,传入毫秒数,表示暂停的时间。
10.sleep(long millis, int nanos)
让线程暂时停止执行,传入毫秒数加纳秒数,表示暂停的时间。
11.start()
启动线程。
3.2 获取和设置线程名称
以下是一个关于获取和设置线程名称的案例。
public static void main(String[] args) {
new Thread("锄禾日当午") { //传入线程名称
public void run() { //线程要执行的任务
System.out.println(Thread.currentThread().getName()); //获取线程名称:锄禾日当午
}
}.start(); //启动线程
System.out.println(Thread.currentThread().getName()); //获取线程名称:main
}
3.3 线程休眠和线程阻塞
以下是一个关于线程休眠的案例。
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++) {
System.out.println(i); //每隔1秒输出一次,从0-9
Thread.sleep(1000); //让线程休眠1000毫秒即1秒
}
}
那么,什么是线程阻塞呢?线程阻塞并不等于线程休眠,它不止包含线程休眠。在线程执行的过程中,可能有的代码耗费了几秒钟的时间,那么这几秒也叫线程阻塞。
可以把线程阻塞简单的理解为所有比较消耗时间的操作,也称为耗时操作。比如:大文件的读取、接受用户输入等。
3.4 线程的中断
一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定。
首先不要用stop方法来停止线程,它是不安全的,因为有可能线程的事情还没有做完就被强制关闭,极有可能导致它正在使用某些资源而没有来得及释放,就会出现资源依然被占用的问题。
我们应该通知线程你该停止了,线程接到相应的通知后再自行停止。
因此,选择通过interrupt方法给线程添加中断标记,当线程在等待或休眠时会检查自身是否携带了中断标记,若有则会产生异常,可以通过捕获此异常来进行后续操作。
但是,中断标记并不代表线程一定要中断,当线程捕获到中断标记时,可以执行任意操作,若需要中断,使用return即可。
public class Demo1 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); //创建线程并为其分配任务
t1.start(); //启动线程
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000); //当前线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t1.interrupt(); //让线程t1中断
}
static class MyRunnable implements Runnable {
public void run() { //线程要执行的任务
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000); //当前线程休眠1秒
} catch (InterruptedException e) { //捕获中断异常
System.out.println("发现中断标记,但是我就不中断");
//如果需要中断在这里加 return; 即可
}
}
}
}
}
3.5 守护线程和用户线程
线程分为守护线程和用户线程。
用户线程:当一个进程不包含任何存活的用户线程时,进程结束。
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动中止。
直接创建的线程都为用户线程,可通过setDaemon方法将其改为守护线程,必须在启动前设置。
public class Demo2 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); //创建线程并为其分配任务
t1.setDaemon(true); //设置t1为守护线程
t1.start(); //启动线程
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000); //当前线程休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyRunnable implements Runnable {
public void run() { //线程要执行的任务
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000); //当前线程休眠1秒
} catch (InterruptedException e) {
}
}
}
}
}
此时当main线程执行完毕后,线程t1就会自动中止,而不会继续执行,这就是守护线程。
4 线程安全
4.1 线程安全问题
多个线程在同时运行时,很容易发生线程不安全的问题,下面是一个卖票的案例。
public class Demo3 {
public static void main(String[] args) {
Runnable run = new Ticket(); //创建任务
new Thread(run).start(); //创建线程1并为其分配任务
new Thread(run).start(); //创建线程2并为其分配相同的任务
new Thread(run).start(); //创建线程3并为其分配相同的任务
}
static class Ticket implements Runnable {
private int count = 10; //记录剩余票数
public void run() { //线程要执行的任务
while (count > 0) { //当票数大于0,继续卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000); //休眠1秒作为卖票所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //剩余票数减1
System.out.println("出票成功,余票"+count); //出票成功
}
}
}
}
上述卖票的案例,可能出现余票是负数的情况。
当当前线程判断票数大于0时,则开始卖票,但是此时另一个线程可能已经把这张票卖出去了,但是当前线程并不知情,于是就多卖了一张,余票就变成了负数。
多线程在操作同一个变量时,由于互相没有管对方,到最后出现了不合理的情况,这就是线程不安全问题。
那么,如何解决线程不安全的问题呢?
4.2 同步代码块
线程不安全的原因:多个线程同时执行去操作同一个数据,可能会导致某个数据在判断时和使用时是不一样的,因为中间间隔的时间被其它线程改变了,最终导致运行不符合预期。
只需要让某一个线程在执行中间代码时,禁止其它线程执行就可以了,也就是排队执行。
同步代码块是其中的一个解决方案,被同步代码块所括住的内容,可以简单认为会排队。
格式:synchronized(锁对象) { }
Java中任何的对象都可以作为锁存在,可以认为任何对象都可以打上锁的标记。当线程执行时,该对象会被打上锁标记,当执行结束时解锁。
若其它线程发现此对象已经被打上锁标记了,则会等待,直到解锁后再去争抢这个锁,谁抢到了锁就再次打上标记,其它线程继续等待。
当然锁对象如果不相同,则不会排队执行。
public class Demo4 {
public static void main(String[] args) {
Runnable run = new Ticket(); //创建任务
new Thread(run).start(); //创建线程1并为其分配任务
new Thread(run).start(); //创建线程2并为其分配相同的任务
new Thread(run).start(); //创建线程3并为其分配相同的任务
}
static class Ticket implements Runnable {
private int count = 1000; //记录剩余票数
private Object o = new Object(); //作为锁对象
public void run() { //线程要执行的任务
while (true) {
synchronized(o) { //同步代码块
if (count > 0) { //当票数大于0,继续卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(10); //休眠10毫秒作为卖票所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //剩余票数减1
System.out.println(Thread.currentThread().getName()+"出票成功,余票" + count); //出票成功
} else { //票数不大于0,结束卖票
break;
}
}
}
}
}
}
注意这里面的锁对象必须是所有线程共用的,不能把它放在run方法里,否则就会各自创建各自的锁对象,锁对象不相同了,就不会排队执行了。
4.3 同步方法
同步方法是第2种解决线程不安全问题的方案,即以方法为单位进行加锁。
只需把需要同步执行的部分单独写成一个方法,然后在该方法返回值的前面加上synchronized修饰符即可。
public class Demo5 {
public static void main(String[] args) {
Runnable run = new Ticket(); //创建任务
new Thread(run).start(); //创建线程1并为其分配任务
new Thread(run).start(); //创建线程2并为其分配相同的任务
new Thread(run).start(); //创建线程3并为其分配相同的任务
}
static class Ticket implements Runnable {
private int count = 1000; //记录剩余票数
public void run() { //线程要执行的任务
while (true) {
if(!sale()) { //票卖空了,结束循环
break;
}
}
}
public synchronized boolean sale() { //同步方法,在返回值前面加synchronized修饰符即可
if (count > 0) { //当票数大于0,继续卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(10); //休眠10毫秒作为卖票所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //剩余票数减1
System.out.println(Thread.currentThread().getName()+"出票成功,余票" + count); //出票成功
return true; //卖票成功返回true
}
return false; //卖票失败返回false
}
}
}
线程同步方法的锁是this,也就是调用这个方法的对象,如果是同一个对象的多个线程调用这个方法,则会排队执行;如果是多个对象调用这个方法,则不会排队执行。
如果是静态修饰的同步方法,那它的锁对象是类名.class。
在这里有一点要注意就是如果使用this锁住同步代码块,又使用这个this锁住同步方法,同一把锁,即使是两组代码,也会排队执行。也就是一个线程在执行同步代码块,另一个线程想执行同步方法也是要等待的。
同步代码块和同步方法都属于隐式锁,下面来说显式锁。
4.4 显式锁Lock
显式锁就是自己创建锁对象,自己上锁,自己解锁。使用Lock的子类ReentrantLock。
public class Demo6 {
public static void main(String[] args) {
Runnable run = new Ticket(); //创建任务
new Thread(run).start(); //创建线程1并为其分配任务
new Thread(run).start(); //创建线程2并为其分配相同的任务
new Thread(run).start(); //创建线程3并为其分配相同的任务
}
static class Ticket implements Runnable {
private int count = 1000; //记录剩余票数
private Lock l = new ReentrantLock(); //显式锁l
public void run() { //线程要执行的任务
while (true) {
l.lock(); //上锁
if (count > 0) { //当票数大于0,继续卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(10); //休眠10毫秒作为卖票所需时间
} catch (InterruptedException e) {
e.printStackTrace();
}
count--; //剩余票数减1
System.out.println(Thread.currentThread().getName()+"出票成功,余票" + count); //出票成功
} else { //票数不大于0,结束卖票
break;
}
l.unlock(); //解锁
}
}
}
}
可见,显式锁比隐式锁更能体现程序员在控制锁的概念,自己上锁,自己解锁,自己在创建锁对象,锁对象更体现了面向对象的机制。
4.5 公平锁与非公平锁
公平锁就是先到先得,排队,不公平锁就是解开后大家一起抢,谁抢到谁就执行。上述的三种处理方式都是不公平锁。
通过显式锁的构造方法传入true就可以实现公平锁,即private Lock l = new ReentrantLock(true);
陆续完善中……
4.6 线程死锁
两个线程的方法都加了同步,然后又互相调用对方的方法,导致两个线程都在等待对方回应,程序卡住无法继续执行,这种情况就叫线程死锁。
public class Demo7 {
public static void main(String[] args) {
ThreadA a = new ThreadA(); //创建线程A
ThreadB b = new ThreadB(); //创建线程B
new MyThread(a,b).start(); //启动线程
a.say(b); //A线程要执行的任务是调用B线程的say方法
}
static class MyThread extends Thread {
private ThreadA a;
private ThreadB b;
public MyThread(ThreadA a,ThreadB b) {
this.a = a;
this.b = b;
}
public void run() { //B线程要执行的任务
b.say(a); //B线程要执行的任务是调用A线程的say方法
}
}
static class ThreadA { //线程A
public synchronized void say(ThreadB b) {
System.out.println("A准备调用B");
b.end(); //A线程的say方法是锁住并调用B线程的end方法
}
public synchronized void end() {
System.out.println("成功调用线程A"); //有一定概率调用成功,但也有一定概率卡住
}
}
static class ThreadB { //线程B
public synchronized void say(ThreadA a) {
System.out.println("B准备调用A"); //有一定概率调用成功,但也有一定概率卡住
a.end(); //B线程的say方法是锁住并调用A线程的end方法
}
public synchronized void end() {
System.out.println("成功调用线程B");
}
}
}
上述程序就有几率卡住,由于方法都加了同步,所以线程A在等待线程B执行完say以后调用线程B的end方法,而线程B又在等待线程A执行完say以后调用线程A的end方法,就会互相卡住,呈现线程死锁的情况。
在开发时一定要尽量避免这种问题,在任何有可能导致锁产生的方法里不要再调用另外一个方法让另外一个锁产生。
4.7 多线程通信
多个线程如何互相通信呢,先来看Object类下的一些方法。由于所有类都是Object类下的子类,所以这些方法是所有类都拥有的方法。
常用方法:
1.notify()
随机唤醒一个通过此对象调用wait休眠的线程。
2.notifyAll()
唤醒所有通过此对象调用wait休眠的线程。
3.wait()
调用此方法的线程会休眠直到被唤醒。
4.wait(long timeoutMillis)
调用此方法的线程会休眠直到被唤醒或达到传入的毫秒。
5.wait(long timeoutMillis, int nanos)
调用此方法的线程会休眠直到被唤醒或达到传入的毫秒加纳秒。
多线程通信问题:
当多个线程互相通信的时候,可能会出现数据不安全的问题,现以生产者和消费者机制来描述这个问题。
首先,为了确保数据安全,确保流程没有问题。我们需要让生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产。
也就是说,生产者生产一份然后等待消费者消费,当消费者拿走一份再等待生产者生产,而不是边生产边消费。这个操作就需要用到锁和等待。
如果不加锁也不加等待,就会导致消费者拿到的东西和他预期的东西不符的问题。
如果加了锁但是不加等待,就会导致要么生产者生产了多份,消费者才开始取走;要么消费者取走了多份(甚至没生产出来就取走了),生产者才开始生产的问题。
下面是一个厨师和服务员的实例:
public class Demo8 {
public static void main(String[] args) {
Food f = new Food(); //创建食物对象
new Cook(f).start(); //创建厨师线程并启动
new Waiter(f).start(); //创建服务员线程并启动
}
static class Cook extends Thread { //厨师线程
private Food f;
public Cook(Food f) {
this.f = f;
}
public void run() { //厨师做饭的线程要执行的任务
for(int i=0;i<100;i++) { //厨师总共做100次饭
if(i%2 == 0) { //两种饭各做50次
f.setNameAndSaste("第一种饭","酸味"); //厨师准备做第一种饭
}else{
f.setNameAndSaste("第二种饭","辣味"); //厨师准备做第二种饭
}
}
}
}
static class Waiter extends Thread{ //服务员线程
private Food f;
public Waiter(Food f) {
this.f = f;
}
public void run() { //服务员端饭的线程要执行的任务
for(int i=0;i<100;i++){ //服务员总共端100次饭
try {
Thread.sleep(100); //端饭耗费了100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get(); //服务员准备端饭
}
}
}
static class Food { //食物对象
private String name; //食物名称
private String taste; //食物口味
private boolean flag = true; //true表示可以做饭,false表示可以端饭,初始设为true
public synchronized void setNameAndSaste(String name,String taste) { //厨师准备做饭
if(flag) { //如果可以做饭
this.name = name; //先把食物放进锅里(设置食物名称)
try {
Thread.sleep(100); //做饭耗费了100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste; //给食物调味(设置食物口味)
flag = false; //厨师做饭完毕,设为false表示可以端饭,此时不可以再继续做饭
this.notifyAll(); //把服务员唤醒
try {
this.wait(); //厨师休眠,等待服务员唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){ //服务员准备端饭
if(!flag) { //如果可以端饭
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste); //端饭操作
flag = true; //服务员端饭完毕,设为true表示可以做饭,此时不可以再继续端饭
this.notifyAll(); //把厨师唤醒
try {
this.wait(); //服务员休眠,等待厨师唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在多线程通信的时候,一定要留意数据安全问题,在必要时加锁或者加休眠,确保流程不会出现问题。
4.8 线程的六种状态
通过关注线程的状态,来更好的理解线程所执行的流程。
线程可以处于以下状态之一:
NEW:线程刚创建出来,尚未启动。
RUNNABLE:线程正在执行。
BLOCKED:线程处于排队状态。
WAITING:线程正在无限期休眠,直到被唤醒。
TIMED_WAITING:线程正在休眠,直到被唤醒或到了指定时间。
TERMINATED:线程已经中止。
5 线程池Executors
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程
就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池就是一个容纳多个线程的容
器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:降低资源消耗、提高响应速度、提高线程的可管理性。
Java种有四种线程池:缓存线程池、定长线程池、单线程线程池、周期性任务定长线程池。
5.1 缓存线程池
缓存线程池的长度无限制,它的执行流程为:判断线程池是否存在空闲线程,存在则使用,不存在则创建线程并放入线程池,然后使用。
public class Demo9 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool(); //创建缓存线程池
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
try {
Thread.sleep(1000); //main休眠1秒,等待三个线程执行完毕再分配任务
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
此案例先连续给线程池分配3个任务,然后休眠1秒,再给线程池分配1个任务。执行代码可得结论:刚开始的3个任务是3个线程执行的,第4个任务分配的时候3个线程已经空闲了,所以不再创建新的线程去执行,而是由其中一个空闲的线程去执行。
5.2 定长线程池
定长线程池的长度是指定的数值,它的执行流程为:判断线程池是否存在空闲线程,存在则使用,若不存在空闲线程:当线程池未满的情况下,创建线程并放入线程池,然后使用;当线程池已满的情况下,则等待线程空闲。
public class Demo10 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2); //创建定长线程池,长度为2
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000); //假设任务需要执行3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000); //假设任务需要执行3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() { //指挥线程池执行新的任务
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
此案例连续给线程池分配3个任务,但是线程池的长度为2,只能拥有2个线程。前两个任务分别分配给两个线程,每个任务都需要3秒才能执行完毕,这样一来第3个任务就要等待线程空闲才能执行。
5.3 单线程线程池
效果与定长线程池创建时传入参数1效果一致,执行流程:判断线程池的那个线程是否空闲,空闲则使用,不空闲则等待那个线程空闲后使用。创建方法为ExecutorService service = Executors.newSingleThreadExecutor();
5.4 周期性任务定长线程池
定时执行,当某个时机触发时,自动执行某任务,执行流程和定长线程池一致,只是可以定时执行一次和周期性执行任务。
定时执行一次:
schedule(Runnable command, long delay, TimeUnit unit)
参数1.定时执行的任务
参数2.时长数字
参数3.时长数字的时间单位(TimeUnit的常量指定)
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); //创建周期性任务定长线程池,长度为2
service.schedule(new Runnable() {
public void run() {
System.out.println("任务执行了");
}
},5, TimeUnit.SECONDS); //任务在5秒后执行
}
周期性执行任务:
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
参数1.任务
参数2.延迟周期时长数字(第一次执行在什么时间以后)
参数3.周期时长数字(每隔多久执行一次)
参数4.时长数字的单位
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); //创建周期性任务定长线程池,长度为2
service.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println("任务执行了");
}
},5,1,TimeUnit.SECONDS); //首次任务在5秒后执行,之后每隔1秒执行一次
}