多线程基础部分

1. 线程与进程的关系

1个进程包含1个或多个线程。

1.1 多线程启动

线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。(这个是没有线程池的,后面会说到线程通过线程池实现的方式)

执行进程中的任务时才会产生线程,因此需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,需要实现Runnable接口并且重写run()方法,然后再将Runnable的实现对象作为参数传递给Thread类。

在这里插入图片描述
还可以采用继承Thread类并且重写run方法,然后调用start()启动线程。
在这里插入图片描述
通常情况下,实现Runnable接口然后启动线程是一个更好的选择,这可以提高程序的灵活性和扩展性,并且用Runnable接口描述任务也更容易理解。在后面的线程池调用中,也使用Runnable表示要执行的任务。

需要特别注意的是,执行start()方法的顺序不代表线程启动的顺序,在下面示例的ThreadTest中,我们按照顺序调用了8个线程的start()方法,但是线程的执行顺序并没有规律,而且每次运行的结果可能都不一样。

代码:
在这里插入图片描述
执行结果:
在这里插入图片描述
为什么会出现这样的运行结果呢?这主要是因为任务的执行靠CPU,而处理器采用分片轮询方式执行任务,所有的任务都是抢占式执行模式,也就是说任务是不排序的。可以设置任务的优先级,优先级高的任务可能会优先执行(多数时候是无效的)。任务被执行前,该线程处于自旋等待状态。

1.2 线程标识

Thread类用于管理线程,如设置线程优先级、设置Daemon属性、读取线程名字和ID、启动线程任务、暂停线程任务、中断线程等。
为了管理线程,每个线程在启动后都会生成一个唯一的标识符,并且在其生命周期内保持不变。当线程被终止时,该线程ID可以被重用。而线程的名字更加直观,但是不具有唯一性。

在这里插入图片描述
在这里插入图片描述
Name:Thread-0
id :8

1.2.1 Thread与Runnable

Runnable接口表示线程要执行任务。当Runnable中的run()方法执行时,表示线程在激活状态,run()方法一旦执行完毕,即表示任务完成,则线程将被停止。

1.3 线程状态

线程对象在不同的运行时期存在着不同的状态,在Thread类中通过一个内部枚举类State保存状态信息,了解线程状态对于并发编程非常重要。
在这里插入图片描述
Java中的线程存在6种状态,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。我们可以通过Thread类中的Thread.getState()方法获取线程在某个时期的线程状态。在给定的时间点,线程只能处于一种状态。

  1. NEW状态
    在这里插入图片描述

  2. RUNNABLE状态
    在这里插入图片描述

  3. BLOCKED状态
    BLOCKED为阻塞状态,表示当前线程正在阻塞等待获得监视器锁。当一个线程要访问被其他线程synchronized锁定的资源时,当前线程需要阻塞等待。
    代码测试如下,在主函数中分别启动了两个线程,它们都需要获得object对象的监视器锁后执行任务。第一个线程启动后,首先获得了object监视器锁。由于在run()方法中使用了死循环while(true),因此第一个线程获得了object监视锁后不会释放,这导致第二个线程长期处于阻塞等待状态。
    第一个线程的状态经历了NEW→RUNNABLE状态的变化;第二个线程的状态经历了NEW→RUNNABLE→BLOCKED等几个状态的变化
    在这里插入图片描述
    在这里插入图片描述
    测试结果如下:

Thread-0:状态:NEW
Thread-1:状态:NEW
Thread-0:状态:RUNNABLE
Thread-1:状态:RUNNABLE
Thread-0:状态:RUNNABLE
Thread-1:状态:BLOCKED
主线程:main:状态:RUNNABLE

4.WAITING状态
// todo

2.线程池入门

线程池与数据库连接池非常相似,目的是提高服务器的响应能力。线程池可以设置一定数量的空闲线程,这些线程即使在没有任务时仍然不会释放。线程池也可以设置最大线程数,防止任务过多,压垮服务器。

2.1 ThreadPoolExecutor

ThreadPoolExecutor是应用最广的底层线程池类,它实现了Executor和ExecutorService接口。在如下类的描述中,列举了ThreadPoolExecutor的构造函数和最常用的几个方法。
在这里插入图片描述

2.2 创建线程池

下面创建一个线程池,通过调整线程池构造函数的参数来了解线程池的运行特性。把核心线程数设置为3,最大线程数设置为8,阻塞队列的容量设置为5。
(1)当要执行的任务数小于核心线程数时,直接启动与任务数相同的工作线程。

package com.company;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadTest {
    public static void main(String[] args) {
        BlockingQueue<Runnable> bq = new LinkedBlockingQueue<>(5);

        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 8, 2000, TimeUnit.MILLISECONDS, bq);

        for (int i = 0; i < 2; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getId() + " is running...");
                    try {
                        Thread.sleep(800);
                    } catch
                    (Exception e) {
                    }
                }
            });
        }

        pool.shutdown();
    }
}

(2)当任务数量大于核心线程数时,超过核心线程数的任务会自动加入阻塞队列中,直到把阻塞队列装满。
调整任务数量为5:for(int i=0;i<5;i++) {…},观察程序运行结果如下。前面的3个任务启动了3个线程并加入线程池,后面的两个任务加入阻塞队列,等待前面的3个任务执行完毕。等前面3个任务完成后,程序会从阻塞队列中取出后面两个任务,然后仍然使用核心线程执行。因此会发现执行最后两个任务的线程号与前面的相同

8 is running…
10 is running…
.9 is running…
10 is running…
.8 is running…

(3)继续增加任务数量为10:for(int i=0;i<10;i++) {…},观察程序的运行结果如下。仔细观察会发现一共启动了5个线程。为什么线程池中的工作线程为5呢?

原因如下:核心线程数为3,因此前面的3个任务会启动3个工作线程。阻塞队列数量为5,因此第4、5、6、7、8这5个任务会自动加入阻塞队列。这时阻塞队列已满,第9、10两个任务会再启动两个新线程。注意:现在的工作线程数量一共为5,小于线程池设置的最大线程数8

9 is running…
11 is running…
.12 is running…
.8 is running…
10 is running…
.9 is running…
11 is running…
.10 is running…
.12 is running…
.8 is running…

(4)继续增加任务数量为15:for(int i=0;i<15;i++) {…},观察程序的运行结果可以发现,当任务数大于“最大线程数+阻塞队列容量”时,会抛出RejectedExecutionException(拒绝执行任务)异常。当前线程池的设置参数,最大容量是8+5=13,当任务数超过13时,都会被拒绝

8 is running…
10 is running…
.12 is running…
.9 is running…
13 is running…
.11 is running…
.14 is running…
.15 is running…
.Exception in thread “main” java.util.concurrent.RejectedExecution
Exception:Task com.icss.pool.MyPool$1@1c7c054 rejected from…
12 is running…
.9 is running…
8 is running…
13 is running…
.10 is running…

2.3 关闭线程池

调用ThreadPoolExecutor的shutdown()方法或shutdownNow()方法,可以关闭线程池
在这里插入图片描述
shutdownNow()与shutdown()的主要区别是:shutdownNow()可以把已提交但是未执行的任务主动取消,并返回未执行的任务列表。

// Executor接口 TODO

创建线程的几种方法

创建线程一共有哪几种方法?

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程
  4. 使用线程池例如用Executor框架

第一个:
继承Thread类创建线程,首先继承Thread类,重写run()方法,在main()函数中调用子类实实例的start()方法。

public class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行");
    }
}

public class TheadTest {
    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();     
        threadDemo.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }
}

结果:(谁前谁后不一定)
main main()方法执行结束
Thread-0 run()方法正在执行

第二个:
实现Runnable接口创建线程:首先创建实现Runnable接口的类RunnableDemo,重写run()方法;创建类RunnableDemo的实例对象runnableDemo,以runnableDemo作为参数创建Thread对象,调用Thread对象的start()方法。

public class RunnableDemo implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中");
    }

}
public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo  runnableDemo = new RunnableDemo ();
        Thread thread = new Thread(runnableDemo);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}

第三个:
使用Callable和Future创建线程:

  1. 创建Callable接口的实现类CallableDemo,重写call()方法。
  2. 以类CallableDemo的实例化对象作为参数创建FutureTask对象
  3. 以FutureTask对象作为参数创建Thread对象。
  4. 调用Thread对象的start()方法。
import java.util.concurrent.Callable;

public class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call(){
        System.out.println(Thread.currentThread().getName() + " call()方法执行中");
        return 0;
    }
}

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("返回结果 " + futureTask.get());
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }
}


DEMO:
		// 组装考试成绩sheet信息
        FutureTask<List<List<String>>> scoreSheetTask = new FutureTask<>(() ->
                generateScoreAnswerSheet(courseName, courseSerial, scheduleTitle, scheduleSerial, studentInfoMap, studentExamList, checkList)
        );
        Thread scoreSheetThread = new Thread(scoreSheetTask);
        scoreSheetThread.start();
        // 组装考试明细-首次回答sheet的信息 考试明细-最高分数sheet的信息
        FutureTask<HashMap<String, List<List<String>>>> answerSheetTask = new FutureTask<>(() ->
                generateAnswerSheet(studentInfoMap, examList, examContentList, checkList)
        );
        Thread answerSheetThread = new Thread(answerSheetTask);
        answerSheetThread.start();
        // 组装能力评估明细sheet的信息
        FutureTask<List<List<String>>> assessmentSheetTask = new FutureTask<>(() ->
                generateAssessmentAnswerSheet(courseName, courseSerial, scheduleTitle, scheduleSerial, studentInfoMap, studentAssessmentList, checkList)
        );
        Thread assessmentSheetThread = new Thread(assessmentSheetTask);
        assessmentSheetThread.start();

第四个:
使用线程池例如用Executor框架: Executors可提供四种线程池,分别为:
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

下面以创建一个定长线程池为例进行说明

public class ThreadDemo extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行");
    }
}

class TestFixedThreadPool {
    public static void main(String[] args) {
        //创建一个可重用固定线程数的线程池      
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口       
        Thread t1 = new ThreadDemo();
        Thread t2 = new ThreadDemo();
        Thread t3 = new ThreadDemo();
        Thread t4 = new ThreadDemo();
        Thread t5 = new ThreadDemo();
        //将线程放入池中进行执行        
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        //关闭线程池       
        pool.shutdown();
    }
}

result:
		pool-1-thread-1正在执行
		pool-1-thread-1正在执行
		pool-1-thread-2正在执行
		pool-1-thread-1正在执行
		pool-1-thread-2正在执行

参考

1.书籍《Java多线程与线程池技术详解》肖海鹏 牟东旭
2.牛客并发编程部分

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

boy快快长大

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值