线程相关知识总结


前言

这些内容是日常工作中或工作之余学习总结的一些东西,包括一些代码实现、方法总结或者面试题总结等,尽最大可能构建出一个完善的知识体系


一、线程的相关概念

1、并发和并行

  • 并发:有处理多个任务的能力,不一定要同时进行。例如吃饭时接到电话,放下碗筷去接电话
  • 并行:有同时处理多个任务的能力
  • 两者的区别在于对多个任务的处理能否做到同时进行

2、线程和进程

  • 进程:操作系统资源分配的最小单位,是系统运行程序的基本单位。例如我们电脑上的应用。
  • 线程:运行在进程中,是操作系统调度的最小单位,一个进程在其执行的过程中可以产生多个线程。

3、守护线程

  • 守护线程:是一种支持类型的线程,因为它主要被用作程序中后台调度以及支持性的工作,例如垃圾回收线程就是一个守护线程。当Java虚拟机中不存在非守护线程(用户线程)时,Java虚拟机会退出运行。通过Thread.setDaemon(true)将线程设置为守护线程,注意这行代码一定要在thread.start()之前,否则设置不生效。
  • 下面代码运行后,在控制台输出内容为主线程 用户线程0 用户线程1 守护线程0 守护线程1 守护线程2 守护线程3 守护线程4 守护线程5 守护线程6,可以看出,只要用户线程运行结束,守护线程会立刻停止。
public class 守护线程 implements Runnable{
   public static void main(String[] args) {
       System.out.println("主线程");
       Test testYH = new Test();
       Thread threadYH = new Thread(testYH);
       threadYH.start();
       守护线程 testSH = new 守护线程();
       Thread thread = new Thread(testSH);
       thread.setDaemon(true);
       thread.start();
   }
   @Override
   public void run() {
       for (int i = 0; i < 10; i++) {
           System.out.println("守护线程"+i);
       }
   }
   static class Test implements Runnable{
       @Override
       public void run() {
           for (int i = 0; i < 2; i++) {
               System.out.println("用户线程"+i);
           }
       }
   }
}

4、线程的状态及线程状体转换

  • 新建状态(new):Thread t = new Thread();线程对象创建完成进入新建状态。
  • 就绪状态(runnable):线程对象调用start方法进入就绪状态,这时线程不会执行,而是等待CPU的调度。
  • 运行状态(running):当CPU开始调度就绪状态的线程后,线程进入运行状态。
  • 阻塞状态(blocked):处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止运行,直到其再次进入就绪状态才有机会被CPU调度执行。阻塞分为三种情况,如下:
    • 等待阻塞:运行状态的线程执行wait方法,wait方法释放锁,需要notify或notifyAll方法来唤醒进入就绪状态。
    • 同步阻塞:线程获取同步锁失败。
    • 其他阻塞:线程执行sleep、join方法或发出了I/O请求,等上述方法执行完毕,线程再次进入就绪状态。
  • 死亡状态(dead):线程执行完毕或者因为异常退出运行。在这里插入图片描述

5、wait、sleep、yield和join的区别

  • wait是object类下的方法,sleep是thread类下的方法。
  • wait方法会释放锁,sleep方法不需要释放锁。
  • 线程执行wait方法后需要其他线程调用notify或notifyAll方法唤醒,sleep方法只是在这段时间内将CPU的使用权交出去,过了这段时间还会继续执行。
  • wait方法多用于线程间的通信,sleep方法用于暂停或延迟当前线程。
  • yield方法执行后线程就进入就绪状态,但是还保留被CPU调用资格,有可能下次CPU调度还是调用本线程。
  • join方法执行后线程进入阻塞状态,直到方法调用结束或中断。

6、线程如何创建

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 实现Callable接口创建线程
  • 通过线程池创建
  • Runnable接口中的run方法是void修饰的,只是纯粹的执行run方法中的代码块。Callable接口中的call方法是有返回值的,和Future、FutureTask配合使用可以获取异步执行的结果。

7、使用多线程带来的问题

  • 上下文切换的问题,频繁的上下文切换会影响程序的执行速度。
  • 会造成线程死锁问题。
  • 多线程受限于软硬件资源,也会影响程序执行速度。

8、线程死锁的四个条件

  • 互斥条件:一个资源只能被一个线程持有。
  • 请求与保持条件:一个线程去请求别的线程持有的资源产生阻塞时,不会释放本身持有的线程。
  • 不剥夺条件:线程持有的资源在未使用完之前不能被其他线程强行剥夺。
  • 循环等待条件:发生死锁时,等待的线程形成一个环路,造成永久阻塞。
  • 死锁的demo
/**
- 一个简单的死锁demo
- jps -l查看所有的进程,jstack+进程号查看对应进程是否含有JVM层面的死锁
*/
public class DeadLockDemo {
   public static Object A = new Object();
   public static Object B = new Object();

   public static void main(String[] args) {
       new Thread(()->{
           synchronized (A){
               System.out.println(Thread.currentThread().getName()+"持有A,去获取B");
               try {
                   Thread.sleep(5000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (B){
                   System.out.println(Thread.currentThread().getName()+"获得B");
               }
           }
       },"A").start();

       new Thread(()->{
           synchronized (B){
               System.out.println(Thread.currentThread().getName()+"持有B,去获取A");
               try {
                   Thread.sleep(5000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (A){
                   System.out.println(Thread.currentThread().getName()+"获得A");
               }
           }
       },"B").start();
   }
}

9、线程阻塞会造成进程阻塞吗

  • 在多对一的用户级线程模型下,由于该进程的所有线程都对应一个内核调度实体,当线程阻塞时,内核实体无法将其他线程调度,因此会阻塞进程;在一对一和多对多的用户级线程模型下,不会阻塞进程。

10、线程间通信的几种方式

  • synchronized+wait()+notify(),通过synchronized分别锁住线程A和线程B,在线程A中调用notify方法,B线程调用wait方法,等线程A执行完毕,线程B执行(notify不会释放锁,所以要等线程A执行完)
  • reentrantLock+condition,通过lock和unlock加锁和释放锁,A线程调用condition.signal方法,B线程调用condition.await方法
  • 基于volatile,使用共享变量,通过不同的变量值控制不同线程的执行,例如false执行A线程,true执行B线程
  • 通过countDownLatch,主要由await方法和countDown方法,若共享变量不为0,A线程执行,直到共享变量减为0,B线程执行

下面是集中线程交互执行的实现示例

/**
 * A、B两个线程交替输出
 * synchronized+wait/notifyAll
 * 设置一个标志位,将标志位的修改使用synchronized修饰,A、B线程分别执行不同的标志位来控制输出
 */
public class ThreadMixOutTest1 {
    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                try {
                    demo.changeFlagToFalse();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"A").start();
        }
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                try {
                    demo.changeFlagToTrue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"B").start();
        }
    }

    public static class Demo{
        private Boolean flag = true;
        public synchronized void changeFlagToFalse() throws InterruptedException {
            while(!flag){
                this.wait();
            }
            System.out.println(Thread.currentThread().getName()+"线程");
            flag = !flag;
            notifyAll();
        }
        public synchronized void changeFlagToTrue() throws InterruptedException {
            while(flag){
                this.wait();
            }
            System.out.println(Thread.currentThread().getName()+"线程");
            flag = !flag;
            notifyAll();
        }
    }
}
/**
 * A、B线程交替输出
 * Lock锁+condition条件实现
 * 同上,只不过这里将标志位改为数值条件,A、B线程分别执行自增或自减
 */
public class ThreadMixOutTest2 {
    public static void main(String[] args) {
        Demo demo = new Demo();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                demo.incr();
            },"A").start();
        }
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                demo.decr();
            },"B").start();
        }
    }

    static class Demo{
        //加减对象
        private int a = 0;
        //锁
        private Lock lock = new ReentrantLock();
        //条件
        private Condition condition = lock.newCondition();
        public void incr(){
            lock.lock();
            try{
                while(a != 0){
                    condition.await();
                }
                System.out.println(Thread.currentThread().getName()+"线程");
                a++;
                condition.signalAll();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }

        public void decr(){
            lock.lock();
            try{
                while(a == 0){
                    condition.await();
                }
                System.out.println(Thread.currentThread().getName()+"线程");
                a--;
                condition.signalAll();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
    }
}
/**
 * 通过信号量实现
 * 两个不同的信号量实现互斥的效果
 * 这里我定义了一个全局变量,用来看是否线程安全的,因为输出会受运行速度的影响,有时会造成相同的输出,这里用这个全局变量证明其实是线程安全的;或者对每个输出加上线程休眠,强制控制其输出速度,保证线程交替执行效果
 */
public class ThreadMixOutTest4 {
    private static Semaphore semaphoreA = new Semaphore(1);
    private static Semaphore semaphoreB = new Semaphore(1);
    private static int a = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            try {
                semaphoreA.acquire();
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"线程");
                    a++;
                },"A").start();
                semaphoreB.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                semaphoreB.acquire();
                new Thread(()->{
                    System.out.println(Thread.currentThread().getName()+"线程");
                    a++;
                },"B").start();
                semaphoreA.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Thread.sleep(100);
        System.out.println(a);
    }
}

11、start()和run()的区别

  • start方法用于启动一个线程,run方法用于执行线程的运行代码
  • start方法用来启动一个线程,使新建状态的线程进入就绪状态,执行线程相关准备工作,当获取到时间片就可以执行,是多线程调用的体现,只能调用一次
  • run方法调用相当于执行main线程下的普通方法,直接调用run方法必须等待run方法执行完毕才能继续执行下面的代码,没有多线程特质
public class test {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("我是bbb,我由"+Thread.currentThread().getName()+"执行");
        },"bbb").start();
        new Thread(()->{
            System.out.println("我是aaa,我由"+Thread.currentThread().getName()+"执行");
        },"aaa").run();
    }
}

在这里插入图片描述

二、线程池相关

前文说到创建线程可以通过线程池,而在日常工作中,使用线程池也是最常见的方法,因此下面记录一些线程池的相关内容。

1、四种常用线程池

  • Executors.newSingleThreadPool()单线程线程池
  • Executors.newFixedThreadPool(5)固定线程数量线程池,最大线程数自定义
  • Executors.newCachedThreadPool()可伸缩线程池,最大线程数量定义为Integer.MAX_VALUE
  • Executors.newScheduledThreadPool(3),支持周期任务的线程池

2、创建线程池的方式

  • 可以看出上面列的常用的线程池都是通过Executors类创建的,但是在阿里开发规范中明确规定了禁止使用Executors类创建线程池,而是推荐使用ThreadPoolExecutor创建。在这里插入图片描述
  • 使用ThreadPoolExecutor创建需要传入七个参数,这里做一下说明:
- 七大参数:
- public ThreadPoolExecutor(int corePoolSize,//核心线程数,也是线程池中所保存的线程数,需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。
-                               int maximumPoolSize,//最大线程数,需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。
-                               long keepAliveTime,//存活时间,当线程数大于核心时,多于的空闲线程最多存活时间
-                               TimeUnit unit,//时间单位
-                               BlockingQueue<Runnable> workQueue,//工作队列(一个阻塞队列),当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。
-                               ThreadFactory threadFactory,//执行程序时创建线程的线程工厂,一般情况下采用默认,不做修改
-                               RejectedExecutionHandler handler//拒绝策略,阻塞队列已满且线程数达到最大值时所采取的饱和策略,java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行
-                               )
-  - 三种BlockingQueue:
-  1、无界队列,LinkedBlockingQueue,使用时要注意,该队列最大大小是Integer.MAX_VALUE,因此,要注意当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
-  2、有界队列,常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
- 使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
-  3、同步移交,SynchronousQueue,如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
- CachedThreadPool就是采用这种,因此有任务进来就会创建线程,需要注意OOM问题
-  - 四大拒绝策略:
-  1、AbortPolicy中止策略,默认的饱和策略,在饱和时会抛出throw new RejectedExecutionException("Task " + r.toString() +" rejected from " +e.toString())异常,可以捕获处理
-  2、DiscardPolicy抛弃策略,public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {},饱和时不做任何处理,直接抛弃任务
-  3、DiscardOldestPolicy抛弃旧任务策略,将队列头移除,也就是移除最老的任务,代码public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
-             if (!e.isShutdown()) {
-                 e.getQueue().poll();
-                 e.execute(r);
-             }
-         }
-  4、CallerRunsPolicy调用者运行,既不抛弃任务也不抛出异常,直接运行任务的run方法,换言之将任务回退给调用者来直接运行。使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。代码public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
-             if (!e.isShutdown()) {
-                 r.run();
-             }
-         }

2、线程和进程

  • 进程:操作系统资源分配的最小单位,是系统运行程序的基本单位。例如我们电脑上的应用。
  • 线程:运行在进程中,是操作系统调度的最小单位,一个进程在其执行的过程中可以产生多个线程。

3、阻塞队列

- 阻塞队列原理:通过ReentrantLock锁和Condition多条件控制阻塞,例如ArrayBlockingQueue的构造函数中定义了notEmpty和notFull两个条件,通过这两个条件的single()await()方法实现线程的
- 通信(这两个方法的具体实现在AQS中),从而控制元素的入队和出队,入队和出队通过lock锁锁定
- 添加元素方法:
-  1add()方法:添加成功返回true,超出容量抛出异常java.lang.IllegalStateException: Queue full
-  2offer()方法:添加成功返回true,添加失败返回false
-  3put()方法:void方法,若容量满了会一直阻塞到容量不满
- 删除元素方法:
-  1poll()方法:删除队列头部元素,如果队列为空,返回null。否则返回元素
-  2remove()方法:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false
-  3take()方法:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除
- 常用的阻塞队列:
-  1、ArrayBlockingQueue:通过可重入锁及其锁条件进行并发控制,只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行,保证了数据的原子性;add方法内部调用了Queue类的offer方法来添加,offer和put方法都会调用checkNotNull
- 方法来判断是否满,只不过后者会有锁条件来等待,导致阻塞;三个删除方法都会调用notFull.signal方法通知正在等待队列满情况下的阻塞线程
-  2、LinkedBlockingQueue:一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表;内部有两个锁,一个拿锁takeLock,一个放锁putLock,添加数据和删除数据是可以并行进行的,因此,定义的元素个数采用原子类AtomicInteger定义
- 当然添加数据、删除数据只能有1个线程执行,添加元素和删除元素的操作都会加锁;而且,不仅在消费数据的时候进行唤醒插入阻塞的线程,同时在插入的时候如果容量还没满,也会唤醒插入阻塞的线程;remove()方法由于删除元素的位置不确定,所以操
- 作时拿锁和放锁都会加锁
-  3、SynchronousQueue,其内部没有存储空间,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费
- 线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程

4、如何合理设置线程池的大小

  • IO密集型:一般用于IO等待时间较长且IO任务很多的情况,可以使用较大的线程池,一般是CPU核心线程数*2
  • CPU密集型:CPU使用频率高,若开启过多的线程,只会增加线程上下文切换带来的时间开销,因此设为CPU核心线程数+1
  • 获取CPU核心线程数Runtime.getRuntime().availableProcessors()

5、线程池如何关闭

  • 调用shutdown方法将线程池状态置为shutdown,但是这并不会立即停止,而是先停止接收外部的提交的任务,内部正在执行的任务和队列等待的任务会继续执行直至完成。
  • 调用shutdownNow方法将线程池状态置为stop,一般会立即停止。停止接收外部提交的任务,忽略队列中等待的任务,将正在执行的线程中断,返回未执行的任务列表。

6、线程池submit和execute的区别

  • execute:用于提交不需要返回值的任务
  • submit:用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个对象可以判断任务是否执行成功,并且可以通过future的get方法获取返回值

7、线程池异常怎么处理

  • try-catch捕获异常
  • 通过submit方法执行,获取返回的future对象,解析该对象去处理异常
  • 重写ThreadPoolExecutor.afterExecute方法,处理传递的异常引用
  • 实例化时,传入自定义ThreadFactory,设置Thread.UncaughtExceptionHandler处理未检测的异常

8、线程池的五种状态

  • RUNNING:接收新任务,处理阻塞队列中的任务
  • SHUTDOWN:调用shutdown方法,不接收新任务,处理阻塞队列中的任务,队列为空且线程池执行的任务为空,进入TIDYING状态
  • STOP:调用shutdownNow方法,不接收新任务,不处理阻塞队列中的任务,中断正在执行的任务,线程池中执行的任务为空,进入TIDYING状态
  • TIDYING:该状态表明所有任务已经运行终止,记录的任务数量为0,调用terminated()执行完毕,进入TERMINATED状态
  • TERMINATED:该状态表示线程池彻底终止

9、线程池的工作流程

在这里插入图片描述

10、线程池中阻塞队列的作用?为什么先添加阻塞队列而不是创建最大线程数?

阻塞队列的作用:阻塞队列可以通过阻塞保留当前想要继续入队的任务,可以保证任务队列中没有任务时阻塞获取任务的线程,释放cpu资源,自带阻塞和唤醒功能,不需要额外处理
因为创建线程的时候需要获取全局锁,这时候其他线程就要阻塞,影响了整题效率;而且线程池的初衷就是避免频繁的创建销毁线程,若先创建最大线程就违背了使用线程池的初衷

11、线程池中线程复用原理?

在线程池中,同一个线程可以从阻塞队列中不断获取任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建线程,而是让每个线程去执行一个循环任务,在这个循环任务中不断检查是否有任务需要执行,若有,直接调用run方法执行

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雅俗共赏zyyyyyy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值