多线程编程:线程的创建与使用

学习使用多线程之前,或许应该也听说过,不管是下载也好,游戏也好,为了使程序运行更快,我们可以选择使用多线程。那么如何使用多线程呢?

一、应用场景:如何更快地下载多个资源?

假设我们需要在网上下载10000多张图片,这种下载操作需要访问网络交换数据,同时将下载到的数据装载到内存,生成文件存放到硬盘。若是通过主线程运行的单线程方式,受限于自身和对方的网速及自身的cpu的计算速度,这种耗时操作需要花很久,同时阻塞了其他代码的运行。

public class Foo {
	private String url;//网络图片地址
	private String name;//保存的文件名
	public static void main(String[] args){
		//执行10000多次
		WebDownloader webDownloader = new WebDownloader();
		//下载器实现IO下载
		webDownloader.download(url,name);
		System.out.println("下载了文件名为:"+name);
	}
}

若是改用多线程的方式,需要将原类进行改造,首先认识第一种:继承Thread

public class FooThread extends Thread{
	private String url;//网络图片地址
	private String name;//保存的文件名
	//构造器传入参数
	public FooThread(String url,String name) {
		this.url=url;
		this.name=name;
	}
	public static void main(String[] args){
		//可将10000多张图片,分组给多个线程分批下载
		FooThread t1 = new FooThread("...","...");
		FooThread t2 = new FooThread("...","...");
		FooThread t3 = new FooThread("...","...");
		t1.start();
		t2.start();
		t3.start();
	}
	@Override
	public void run() {		
		WebDownloader webDownloader = new WebDownloader();
		webDownloader.download(url,name);
		System.out.println("下载了文件名为:"+name);
	}
}

多线程可以将原来单个线程做的事变成多个线程同时执行,是很方便的,作用很强大的。毕竟将一个人做的事,变成多个人一起做,那肯定会快啦!

接下来,先回顾下基础知识,再学习如何创建线程。

(一)前置知识:线程基础概念

1.什么是程序?
程序是硬盘上的一段可执行的静态代码,例如QQ.exe、LOL.exe、Google.exe等exe文件。
2.什么是进程?
进程可以理解成运行的exe程序,在进程管理器中可以看到很多进程。进程是操作系统资源调度的最小单位。一段程序有可以有多个进程,一个进程至少有一个线程。
3.什么是线程?
线程是进程中一个单一顺序的控制流,是进程中的实际运作单位,是操作系统运算调度的最小单位。例如对于java程序的JVM进程,有Main线程、gc线程。
4.程序是怎么运行的?
计算机通过IO总线从硬盘加载程序代码数据到内存中,CPU通过系统总线加载内存中的数据到寄存器中,通过ALU运算单元对寄存器的数据进行加法运算,并回写给主内存,然后通过PC程序计数器存储下一条执行指令的地址。CPU的执行速度很快,操作系统的线程调度器不停的提供各个程序的线程给CPU执行,做到变相的多任务处理。

(二)前置知识:Thread类基础概念

1.线程ID
线程ID由JVM进行管理,在进程内唯一。

Thread t1 = new Thread();
System.out.println(t1.getId());

2.线程名称
private String name,该属性保存一个Thread线程实例的名字。

Thread t1 = new Thread("线程名称");
System.out.println(t1.getName());

3.线程优先级
private int priority,保存一个Thread线程实例的优先级。
Java线程的最大优先级值为10,最小值为1,默认值为5。这三个优先级值为三个常量值,在Thread类中使用类常量定义,三个类常量如下:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

4.是否为守护线程
private boolean daemon=false,该属性保存Thread线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程。

public final void setDaemon(boolean on) {
    checkAccess();
    if (isAlive()) {
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

5.线程的状态
private int threadStatus,该属性以整数的形式保存线程的状态。

 public enum State {
    NEW,//创建,使用new关键字等创建线程对象后,所处的状态。直到出现start()这个线程。
    RUNNABLE,//可运行,处于就绪状态的线程获取CPU资源,就可以执行run()所处的状态。可运行状态,可能在运行,也可能不在运行。它可以变为阻塞状态、就绪状态和死亡状态。
    BLOCKED,//阻塞,线程执行sleep(睡眠),suspend(挂起)等方法,失去资源后所处的状态。
    WAITING,//等待或就绪,调用start()方法后,所处的状态。
    TIMED_WAITING,//计时等待,等待特定时间的等待状态。
    TERMINATED;//死亡状态,运行状态的线程完成任务或被终止所处的状态。
}

6.线程的启动和运行
方法一:start()方法用于线程的启动
方法二:run()方法作为用户代码逻辑的执行入口。

二、应用场景:如何互不干扰地模拟多人商店并发的售卖商品?

以商店的售货员售卖商品为例,这个案例涉及到三个对象,商店、售货员、商品。首先商店售卖物品,需要有一个商店的类,商店类应该有自己的属性,包含商品的属性。其中,售货员是操作方,需要操作商品数量的减少,要有一个售货员的类。
有两种需求:
(1)售货员各自卖各自的商品,比如售货员1卖零食、售货员2卖水果,互不干涉。
(2)多个售货员卖共享的资源,比如卖电影票,或手机、电脑等大件物品,获得提成,竞争性的卖出,当库存为0时,无法卖出。

(一)方法1:继承Thread类

可能有多个售货员并发的销售商品,因此售货员的类应该继承Thread类。
售货员的类中需要重写run()方法,将业务逻辑编写在其中。
执行类中的start()方法启动线程。

新线程如果需要并发执行自己的代码,需要做以下两件事情:
(1)需要继承Thread类,创建一个新的线程类。
(2)同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。

//商店类
public class Shop {
    private final static int max_amount=5;//商品数量
    //这里设计成内部类为方便访问外部类的成员属性和方法,跟线程的使用没有任何关系,也可放外部
    static class SellerThread extends Thread{
        private int goodsAmount= max_amount;
        public SellerThread(String s) {//传入线程名称的构造函数
            super(s);
        }
        //业务逻辑代码
        @Override
        public void run() {
            for (int i = 0; i <= max_amount; i++) {
            if(goodsAmount>0)
                System.out.println(Thread.currentThread().getName()+"售卖一件货物,还剩"+(--goodsAmount)+"件货物");
            }
        }
    }
    //执行
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new SellerThread("售货员-"+i).start();
        }
    }
}
--------结果--------
售货员-0售卖一件货物,还剩4件货物
售货员-0售卖一件货物,还剩3件货物
售货员-1售卖一件货物,还剩4件货物
售货员-0售卖一件货物,还剩2件货物
售货员-1售卖一件货物,还剩3件货物
售货员-0售卖一件货物,还剩1件货物
售货员-1售卖一件货物,还剩2件货物
售货员-0售卖一件货物,还剩0件货物
售货员-1售卖一件货物,还剩1件货物
售货员-1售卖一件货物,还剩0件货物
------------------

浓缩下其实就是:

Class MyThread extends Thread{
    @Override
    Void run(){
	}
}
new MyThread().start();

在该例子中可以看到很多问题:

其中,售货员需要继承Thread类,才能作为线程去并发的执行,但是由于java是单继承,如果售货员的类有父类应该怎么办?

所以,这里介绍第二种方法:实现Runnable接口。

(二)方法2:实现Runnable接口

比如,当一个销售Seller类继承了Employee类,再要继承Thread类就不行了。在已经存在继承关系的情况下,只能使用实现Runnable接口的方式。

该方法的具体步骤如下:
(1)定义一个新类实现Runnable接口。
(2)实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
(3)通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
(4)调用Thread实例的start()方法启动线程。
(5)线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。

//商店类
public class Shop {
    private final static int max_amount=5;//商品数量
    //实现Runnable接口
    static class SellerRunnable implements Runnable{
        private int goodsAmount= max_amount;
        @Override
        public void run() {
        //逻辑代码依然是一样的
            for (int i = 0; i < max_amount; i++) {
                System.out.println(Thread.currentThread().getName()+"售卖一件货物,还剩"+(--goodsAmount)+"件货物");
            }
        }
    }
    //执行
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            SellerRunnable target = new SellerRunnable();
            new Thread(target,"售货员-"+i).start();
        }
    }
}
--------结果--------
售货员-0售卖一件货物,还剩4件货物
售货员-1售卖一件货物,还剩4件货物
售货员-0售卖一件货物,还剩3件货物
售货员-1售卖一件货物,还剩3件货物
售货员-0售卖一件货物,还剩2件货物
售货员-1售卖一件货物,还剩2件货物
售货员-0售卖一件货物,还剩1件货物
售货员-1售卖一件货物,还剩1件货物
售货员-0售卖一件货物,还剩0件货物
售货员-1售卖一件货物,还剩0件货物
------------------

浓缩下其实就是:

Class MyTask implements Runnable {
      Void run(){}          
}
new Thread( new MyTask()).start();

快速创建(推荐)

由于Runnable是一个函数式接口(有且仅有一个抽象方法,但是可以有多个非抽象方法的接口),因此可以用匿名类或lambda表达式便捷的创建,经过比较,显然lambda表达式更方便。

Runnable r = () -> {代码块};
Thread t = new Thread(r);
t.start();
//匿名类简写为
new Thread(new Runnable() -> {        
	@Override
    public void run() {
    	代码块
    }
}).start();
//lambda表达式简写为
new Thread(() -> {代码块}).start();

通过实现Runnable接口的方式创建线程目标类,有以下优点:
(1)可以避免由于Java单继承带来的局限性。如果异步逻辑所在类已经继承了一个基类,就没有办法再继承Thread类。
(2)逻辑和数据更好分离。通过实现Runnable接口的方法创建多线程更加适合同一个资源被多段业务逻辑并行处理的场景。

同时,也有以下缺点:
(1)所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程类的构造器,才能创建真正的线程。
(2)如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程。

这里需要解释一下
什么叫逻辑和数据更好分离?

注意到前面的例子中,售货员线程各自销售自己的产品,若是多个售货员销售共享的同一商品资源应该怎么办?

//商店类
public class Shop {
    private final static int max_amount = 5;//商品数量
    static class SellerRunnable implements Runnable {
        private int goodsAmount = max_amount;//共享的资源
        @Override
        public void run() {
            for (int i = 0; i <= max_amount; i++) {
                //商品数量大于0时,就售卖
                if(goodsAmount>0)
                System.out.println(Thread.currentThread().getName() + "售卖一件货物,还剩" + (--goodsAmount) + "件货物");
            }
        }
    }
    //执行
    public static void main(String[] args) {
    	//将执行目标类挪到外面,多个线程执行
        SellerRunnable target = new SellerRunnable();
        for (int i = 0; i < 2; i++) {
            new Thread(target, "售货员-" + i).start();
        }
    }
}
--------结果--------
售货员-0售卖一件货物,还剩4件货物
售货员-1售卖一件货物,还剩3件货物
售货员-0售卖一件货物,还剩2件货物
售货员-1售卖一件货物,还剩1件货物
售货员-0售卖一件货物,还剩0件货物
--------------------

这里的关键点是:2个线程共享了一个Runnable类型的target执行目标实例—SellerRunnable 实例。
共同访问SellerRunnable 实例的同一个商品数量goodsAmount,剩余数量从4卖到0

看起来一切都很正常!然而,这里2个线程还看不出来,若时是将线程改为3或更多时,有时候会发现错误,这里因为共享资源,会触发原子更新问题,线程变得不安全。

for (int i = 0; i < 3; i++) {
    new Thread(target, "售货员-" + i).start();
}
--------结果--------
售货员-1售卖一件货物,还剩4件货物
售货员-1售卖一件货物,还剩3件货物
售货员-1售卖一件货物,还剩2件货物
售货员-1售卖一件货物,还剩1件货物
售货员-1售卖一件货物,还剩0件货物
售货员-0售卖一件货物,还剩-1件货物
--------------------

所以该处应该将数据类型变为原子类型

private int goodsAmount = max_amount;//共享的资源
//改为
private final AtomicInteger goodsAmount = new AtomicInteger(max_amount);

//更改后,这里即使有更多线程,商品数量也不会超库存
for (int i = 0; i < 10; i++) {
    new Thread(target, "售货员-" + i).start();
}
--------结果--------
售货员-0售卖一件货物,还剩4件货物
售货员-0售卖一件货物,还剩1件货物
售货员-4售卖一件货物,还剩2件货物
售货员-1售卖一件货物,还剩3件货物
售货员-5售卖一件货物,还剩0件货物
--------------------

通过对比可以看出:
(1)通过继承Thread类实现多线程能更好地做到多个线程并发地完成各自的任务,访问各自的数据资源。
(2)通过实现Runnable接口实现多线程能更好地做到多个线程并发地完成同一个任务,访问同一份数据资源。
(3)通过实现Runnable接口实现多线程时,如果数据资源存在多线程共享的情况,那么数据共享资源需要使用原子类型(而不是普通数据类型),或者需要进行线程的同步控制,以保证对共享数据操作时不会出现线程安全问题。

三、应用场景:调用不同网络请求,如何缩短访问时间得到返回值?

前面创建线程的两种方式有一个共同的缺陷,就是不能获取异步执行的结果。商店的售货员只知道在卖货,而无法收到他的反馈。这里问题还不明显,再看一个案例。

假设某个淘宝页面需要显示三块内容,比如订单信息,推荐商品、物流信息等等。是远程调用三个不同的API吗?
这很可能遇到跨域问题,通常的做法,是由后端代理请求,将结果汇总返回给前端。如果请求1要1s,请求2要2s,请求3要3s,那么汇总结果需要1+2+3s,顺序执行的话访问很慢,所以需要开启多线程,访问时间取决于最慢的请求3s时间。

介绍第三种方法:实现Callable接口。

(一)方法3:通过Callable和Future创建线程

具体步骤如下:
(1)创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
(2)使用Callable实现类的实例构造一个FutureTask实例。
(3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。

//api请求类
public class ApiRequest implements Callable<String> {
    private final String result;//返回结果
    private final Long delay;//延时

    public ApiRequest(String result, Long delay) {
        this.result = result;
        this.delay = delay;
    }
    //业务逻辑代码
    @Override
    public String call() throws Exception
    {
        long startTime = System.currentTimeMillis();//记录开始时间
        System.out.println(Thread.currentThread().getName()+"线程开始运行");
        Thread.sleep(delay);//模拟请求耗时
        System.out.println(Thread.currentThread().getName()+"线程结束运行");
        System.out.println(Thread.currentThread().getName()+"线程耗时:"+(System.currentTimeMillis() - startTime));//记录结束时间耗时
        return result;
    }
    //执行
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long startTime = System.currentTimeMillis();//记录开始时间
        ApiRequest r1 = new ApiRequest("结果1",1000L);//请求api1,耗时1s
        ApiRequest r2 = new ApiRequest("结果2",2000L);//请求api2,耗时2s
        ApiRequest r3 = new ApiRequest("结果3",3000L);//请求api3,耗时3s
        FutureTask<String> ft1 = new FutureTask<>(r1);
        FutureTask<String> ft2 = new FutureTask<>(r2);
        FutureTask<String> ft3 = new FutureTask<>(r3);
        new Thread(ft1).start();//开启线程1
        new Thread(ft2).start();//开启线程2
        new Thread(ft3).start();//开启线程3
        Thread.sleep(500);//模拟Main自身请求,耗时0.5s
        System.out.println("Main线程开始自身的任务");
        System.out.println("Main线程持续进行运行");
        System.out.println("获取子线程结果:"+ft3.get());//一直阻塞直到线程获取结果
        System.out.println("获取子线程结果:"+ft2.get());//一直阻塞直到线程获取结果
        System.out.println("获取子线程结果:"+ft1.get());//一直阻塞直到线程获取结果
        System.out.println("Main线程结束运行,时间占用:"+(System.currentTimeMillis() - startTime));
    }
}
--------结果--------
Thread-1线程开始运行
Thread-2线程开始运行
Thread-0线程开始运行
Main线程开始自身的任务
Main线程持续进行运行
Thread-0线程结束运行
Thread-0线程耗时:1016
Thread-1线程结束运行
Thread-1线程耗时:2003
Thread-2线程结束运行
Thread-2线程耗时:3012
获取子线程结果:结果3
获取子线程结果:结果2
获取子线程结果:结果1
Main线程结束运行,时间占用:3013
--------------------

1.Callable实例能否如同Runnable实例在Thread线程中执行?
不行,Thread的target属性类型为Runnable,而Callable接口没有集成Runnable接口。
2.如何使用Callable接口创建线程?
需要RunnableFuture接口牵线搭桥。

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

(1)RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标
(2)RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。
3.Future接口是干什么的?
Future接口至少提供了三大功能:
(1)能够取消异步执行中的任务。
(2)判断异步任务是否执行完成。
(3)获取异步任务完成后的执行结果。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);//取消异步执行
    boolean isCancelled();//判断异步是否执行取消,若任务完成前被取消,则返回true
    boolean isDone();//判断异步是否执行完成,若任务执行结束,则返回true
    V get() throws InterruptedException, ExecutionException;//获取异步执行完成的结果,该方法的调用是阻塞的如果没有执行完成,则一直被阻塞到完成
    V get(long timeout, TimeUnit unit)//设置时限,获取异步任务完成后执行的结果,若在时限内未完成,则抛出异常
        throws InterruptedException, ExecutionException, TimeoutException;
}

总体来说,Future是一个对异步任务进行交互、操作的接口。
4.怎么进行牵线搭桥?
FutureTask类实现了RunnableFuture接口,意味着既实现了Runnable接口,又实现了Future接口。既可以作为Thread线程实例的target目标,又可以获取并发任务执行的结果。

//FutureTask简化代码
public class FutureTask<V> implements RunnableFuture<V> {
    private Callable<V> callable;//用来保存并发执行的Callable<V>类型的任务
    private Object outcome; //用于保存callable成员call()方法的异步执行结果
    
    //FutureTask必须传入Callable进行初始化
    public FutureTask(Callable<V> callable) {
        this.callable = callable;
    }
    public void run() {
		V result = c.call();//调用callable的call方法
		set(result);   //保存异步任务执行结果
    }
    public V get() {
        return outcome;//获取异步任务执行结果
    }
    protected void set(V v) {
		outcome = v;
    }
}

四、应用场景:如何使用线程池?

Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
(1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
(2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。
Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。

如果不对线程进行控制与管理,反而会影响程序的性能。

(一)是否使用线程池创建线程的差异

//线程池测试类
public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);//创建包含5个线程的线程池
        for (int i = 0; i < 18; i++) {
            pool.execute(//线程执行,无返回值
                () -> {//Runnable的lambda表达式
                    System.out.println(Thread.currentThread().getName() + "的线程正在执行任务,开始时间:" + System.currentTimeMillis());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            );
        }
    }
}
--------结果--------
pool-1-thread-1的线程正在执行任务,开始时间:1661071696798
pool-1-thread-4的线程正在执行任务,开始时间:1661071696798
pool-1-thread-2的线程正在执行任务,开始时间:1661071696798
pool-1-thread-3的线程正在执行任务,开始时间:1661071696798
pool-1-thread-5的线程正在执行任务,开始时间:1661071696798
pool-1-thread-2的线程正在执行任务,开始时间:1661071699812
pool-1-thread-1的线程正在执行任务,开始时间:1661071699812
pool-1-thread-3的线程正在执行任务,开始时间:1661071699812
pool-1-thread-4的线程正在执行任务,开始时间:1661071699812
pool-1-thread-5的线程正在执行任务,开始时间:1661071699812
...
------------------

接下来,比较下使用线程池与否的性能差异。

 public static void main(String[] args) throws InterruptedException {
        //单线程
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
//            try {
//                Thread.sleep(1);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            list.add(new Random().nextInt());
        }
        System.out.println("single thread time:" + (System.currentTimeMillis() - beginTime) + "  size:" + list.size());
        //线程池
        list.clear();
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        beginTime = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            executorService.execute(
                    () -> {
//                        try {
//                            Thread.sleep(1);
//                        } catch (InterruptedException e) {
//                            e.printStackTrace();
//                        }
                        list.add(new Random().nextInt());
                    }
            );
        }

        executorService.shutdown();
        try{
            executorService.awaitTermination(1, TimeUnit.DAYS);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("thread pool time:" + (System.currentTimeMillis() - beginTime) + "  size:" + list.size());
        //多线程
        list.clear();
        beginTime = System.currentTimeMillis();
        final CountDownLatch latch = new CountDownLatch(2000);
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(
                    () -> {
//                        try {
//                            Thread.sleep(1);
//                        } catch (InterruptedException e) {
//                            e.printStackTrace();
//                        }
                        list.add(new Random().nextInt());
                        latch.countDown();
                    }
            );
            thread.start();
        }
        latch.await();
        System.out.println("mutiple thread time:" + (System.currentTimeMillis() - beginTime) + "  size:" + list.size());
    }

当线程需要执行的任务很轻量时,效率:单线程>线程池>多线程

这是因为多线程的创建、销毁、cpu切换线程上下文需要耗费时间

--------当线程无延时的结果--------
single thread time:8  size:2000
thread pool time:40  size:2000
mutiple thread time:123  size:2000
------------------

当开启Thread.sleep(1),线程池线程数为1时,效率:多线程>单线程>线程池
可以看到,当任务变重时,虽然多线程创建线程需要耗时,但是后续并行处理任务使得效率提高了。
而线程池和单线程与相比,虽然都只用了一个线程,但是多了一步创建线程池的操作。

--------当开启Thread.sleep(1),线程池线程数为1时的结果--------
single thread time:2026  size:2000
thread pool time:2050  size:2000
mutiple thread time:129  size:2000
------------------

当开启Thread.sleep(1),线程池线程数为100时,效率:线程池>多线程>单线程
可以看出线程池明显提高了效率,相比于开启2000个线程的多线程,开启100个线程的线程池既节约了资源,效率也更高

--------当开启Thread.sleep(1),线程池线程数为100时的结果--------
single thread time:2023  size:2000
thread pool time:58  size:2000
mutiple thread time:119  size:2000
------------------

具体线程池应该开多少线程,应该根据环境自行调试,才能获取最佳性能。否则使用线程池,甚至会出现效率变得更低的情况。

在这里插入图片描述

(二)了解Executors的4种创建线程池的方法

1.单线程池

ExecutorService pool1 = Executors.newSingleThreadExecutor();

单线程化的线程池的特点:
(1)单线程化的线程池中的任务是按照提交的次序顺序执行的。
(2)池中的唯一线程的存活时间是无限的。
(3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
适用场景:任务按照提交次序,一个任务一个任务地逐个执行的场景。

2.固定线程池

ExecutorService pool2 = Executors.newFixedThreadPool(3);

“固定数量的线程池”的特点大致如下:
(1)如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
(2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。

3.可缓存线程池

ExecutorService pool3 = Executors.newCachedThreadPool();

“可缓存线程池”的特点大致如下:
(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

4.计划线程池

ExecutorService pool4 = Executors.newScheduledThreadPool(3);

适用场景:周期性地执行任务的场景。Spring Boot中的任务调度器,底层借助了JUC的ScheduleExecutorService“可调度线程池”实现,并且可以通过@Configuration配置类型的Bean。

(三)线程池标准创建方式

虽然Executors工厂类快捷创建线程池很方便,但是大多会禁止使用。一般,会使用构造方法创建。

public ThreadPoolExecutor(
	int corePoolSize, //核心线程数,及时线程空闲,也不回收                          
	int maximumPoolSize,//线程数的上限
	long keepAliveTime,//线程最大空闲时间
	TimeUnit unit,//时间单位
	BlockingQueue<Runnable> workQueue,//任务的排队队列
	ThreadFactory threadFactory,//新线程的产生方式
	RejectedExecutionHandler handler//拒绝策略
) 
{
}
线程池架构

1.线程池的简单创建:运用Executors工厂方法

//创建一个包含三个线程的线程池
private static ExecutorService pool = Executors.newFixedThreadPool(3);

2.ExecutorService的操作方法
ExecutorService接口继承于Executor接口,它有三个方法

//方法一:execute(),执行Runnable 类型的target目标实例,无返回值
void execute(Runnable command);    
//方法二:submit(),提交Callable类型或Runnable 类型的target目标实例,返回Future异步任务实例
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);

五、线程各操作方法

1.线程信息

Thread.currentThread()//返回代码段正在被哪个线程调用的信息。
getName()//获得线程名称
getId()//获得线程的唯一标识
isAlive() //判断当前的线程是否处于活动状态。活动状态就是线程已经启动且尚未终止
sleep() //当前正在执行的线程休眠特定的毫秒数,不释放锁,阻塞其他线程。

2.停止线程

一般用抛出异常的方法,停止线程的执行

class MyThread extends Thread {
    private int count = 5000000;
    @Override
    public synchronized void run() {
        super.run();
        try {
            while (count > 0) {
                count--;
                if (this.isInterrupted()) {
                    System.out.println("中断状态,要退出了!!!");
                    throw new InterruptedException();
                }
                System.out.println("count="+count);
            }
        } catch (InterruptedException e) {
            System.out.println("捕获异常,结束运行");
            e.printStackTrace();
        }
    }
}

3.暂停线程
可以使用suspend和resume方法暂停和恢复线程的执行

suspend()
resumee()

不过这两个方法存在安全隐患,已作废
4.线程礼让资源
yield()的作用是放弃当前的CPU资源,将它让其他的任务去占用CPU执行时间。

class MyThread extends Thread {
    public long i = 0;
    @Override
    public synchronized void run() {
        super.run();
        long begin = System.currentTimeMillis();
        while (i<50000000){
            //Thread.yield();
            i++;
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-begin)+"ms");
    }
}
//使用yield 耗时3380ms
//注释yield 耗时16ms
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值