线程、并发相关基础知识

线程、并发相关基础知识


一、线程、并发相关

1、线程的生命周期?线程有几种状态

线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于
    可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进
    入就绪状态,才有机会转到运行状态。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

2、sleep()、wait()、join()、yield()的区别

1.锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线
程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到
后会进入就绪队列进行等待cpu资源分配。

2.等待池
当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放
到锁池,而notifyAll()是将等待池的所有线程放到锁池当中

1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu
的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而
是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程
序。如果在睡眠期间其他线程调用了这个线程的interrupt方法

3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞
争到锁继续执行的。
yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,
所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队
列,直到线程A结束或中断线程

3、对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分
配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了
要还给操作系统,要不然就是内存泄漏。

栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈
互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语
言里面显式的分配和释放。

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己
的内存空间,而不能访问别的进程的,这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以
访问到该区域,这就是造成问题的潜在原因。

4、Thread、Runable的区别

Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new
Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简
单的执行一个任务,那就实现runnable。

5、守护线程

守护线程:为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保
姆;
守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪
天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;
注意: 由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因
为它不靠谱;
守护线程的作用是什么?
举例, GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就
不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线
程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
应用场景:(1)来为其它线程提供服务支持的情况;(2) 或者在任何情况下,程序结束时,这个线
程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要
正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都
是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个
IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
在Daemon线程中产生的新线程也是Daemon的。
守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作
的中间发生中断。
Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线
程就不能用Java的线程池。

6、ThreadLocal的原理和使用场景

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所
有ThreadLocal对象及其对应的值

ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构
成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该
key就会被垃圾收集器回收

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对
象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal对象为key,获取对应的value。

7、ThreadLocal内存泄露原因

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露
堆积后果很严重,无论多少内存,迟早会被占光,

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有
手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任
    何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

8、并发、并行、串行的区别

串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行

9、为什么用线程池

  1. 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
  2. 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
  3. 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。

corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会
消除,而是一种常驻线程

maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程
数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但
是线程池内线程总数不会超过最大线程数

keepAliveTime 、 unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会
消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过

setKeepAliveTime 来设置空闲时间

workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放
入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程

ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建
工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择
自定义线程工厂,一般我们会根据业务来制定不同的线程工厂

Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这
时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程
池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提
交的任务时,这是也就拒绝

10、线程池中阻塞队列的作用

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务
了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资
源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂
起,从而维持核心线程的存活、不至于一直占用cpu资源

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

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的
一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对
Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去
执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就
是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就
将所有任务的 run 方法串联起来。

上一篇:使用Telegraf + Influxdb + Grafana 监控SQLserver服务器的运行状况


下一篇:对ThreadLocal的理解