多线程基础知识总结三

什么是线程池?为什么要使用线程池?
  • 将线程池化,需要运行任务时就从里面拿出来一个,不需要了就放回去,不需要每次都new出来
    线程复用,节约资源,在操作系统中,每个进程能开启的线程数是有限的。
    带来好处
    1.降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
    创建和销毁线程都需要资源
    2.提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行,假设一个服务器完成一项任务所需时间为:T1+T2+T3,T1创建线程时间,T2在线程中执行任务的时间,T3是销毁线程时间,如果:T1+T3远大于T2,则可以采用线程池,已提高服务器性能,线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的,它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了,使用线程池可以省去T1和T3的时间
    3.提高我们线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
JDK中的线程池和工作机制
  • 线程池的创建各个参数含义
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
    corePoolSize
    线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
    如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程
    maximumPoolSize
    线程池中允许的最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
    keepAliveTime
    线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间,默认情况下,该参数只在线程数大于corePoolSize时才有用
    TimeUnit
    keepAliveTime的时间单位
    workQueue
    workQueue必须是BlockingQueue阻塞队列,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待,通过workQueue,线程池实现了阻塞功能
什么是阻塞队列
  • 队列:
    队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
    队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
  • 堵塞队列:
    1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满
    2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
    阻塞队列常用语生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
    抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException("Queuefull")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
    •返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
    •一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
    •超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
常用阻塞队列

•ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
•LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
•PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
•DelayQueue:一个使用优先级队列实现的无界阻塞队列。
•SynchronousQueue:一个不存储元素的阻塞队列。
•LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
•LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名Executors静态工厂里默认的threadFactory,线程的命名规则是"pool-数字-thread-数字"

RejecteExecutionHandler(饱和策略)面试必问

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
1)AbortPolicy:直接抛出异常,默认策略;
2)CallerRunsPolicy:用调用者所在的线程来执行任务;
3)DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务;
4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedEexeecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

线程池的工作机制

1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁)
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue.
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(执行这一步需要获取全局锁)
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

BlockingQueue组合队列容器
Runtime.getRuntime().availableProcessors();//获取cpu的核心数
使用RejectedExecutionHandler接口 自定义策略
BlockingQueue阻塞队列
阻塞插入方法:队列满,放元素被阻塞
阻塞移除方法:队列空,拿元素被阻塞

image.jpeg

add和remove 如果是慢的会抛异常

offer和poll

take和put,不论是放和取,如果满了都会被阻塞,直到可以拿到
ArrayBlockingQueue和LinkedBlockingQueue有什么区别
ArrayBlockingQueue必须传入队列有多大,LinkedBlockingQueue可以不指定大小
线程池创建的各个参数的含义和工作机制是必问的。

如何合理的配置线程池?
  • 想要合理地配置线程池,就必须首先分析任务特性,可以从几下角度来分析
    与任务的特性有关
    1.CPU密集型 当前的任务完全依赖于CPU 大量计算型的任务,不要超过你机器上面cpu的个数,同时运行的线程个数,使用Runtime.getRuntime().availableProcessors();//获取cpu的核心数
    2.IO密集型 磁盘、SD卡、内存、网络相关的,对CPU的要求不是很高,尽可能多的配置线程数,常规就是 2*机器上CPU的个数
    3.混合型 拆分
    任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
    任务的优先级:高、中和低
    任务的执行时间:长、中和短
    任务的依赖性:是否依赖其他系统资源,如数据库连接

性质不同的任务可以用不同的规模的线程池分开处理,cpu密集型任务应该配置尽可能小的线程,如配置Ncpu+1个线程的线程池,由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2乘Ncpu,混合型的任务,如果可以拆分将其拆分成一个cpu密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量,如果这两个任务执行时间相差太大,则没必要进行分解,可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

提交任务

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象

可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TImeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭

下面是线程池的实现

public class MyThreadPool2 {

// 线程池中默认线程的个数为5

private static int WORK_NUM = 5;

// 队列默认任务个数为100

private static int TASK_COUNT = 100;

// 工作线程组

private WorkThread[] workThreads;

// 任务队列,作为一个缓冲

private final BlockingQueue taskQueue;

private final int worker_num;//用户在构造这个池,希望的启动的线程数

// 创建具有默认线程个数的线程池

public MyThreadPool2() {

this(WORK_NUM,TASK_COUNT);

}

// 创建线程池,worker_num为线程池中工作线程的个数

public MyThreadPool2(int worker_num,int taskCount) {

if (worker_num<=0) worker_num = WORK_NUM;

if(taskCount<=0) taskCount = TASK_COUNT;

this.worker_num = worker_num;

taskQueue = new ArrayBlockingQueue<>(taskCount);

workThreads = new WorkThread[worker_num];

for(int i=0;i

workThreads[i] = new WorkThread();

workThreads[i].start();

}

// int count = Runtime.getRuntime().availableProcessors();

// Runtime.getRuntime().availableProcessors()*2

}

// 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定

public void execute(Runnable task) {

try {

taskQueue.put(task);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁

public void destroy() {

// 工作线程停止工作,且置为null

System.out.println("ready close pool.....");

for(int i=0;i

workThreads[i].stopWorker();

workThreads[i] = null;//help gc

}

taskQueue.clear();// 清空任务队列

}

// 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数

@Override

public String toString() {

return "WorkThread number:" + worker_num

  • " wait task number:" + taskQueue.size();

}

/**

  • 内部类,工作线程

*/

private class WorkThread extends Thread{

@Override

public void run(){

Runnable r = null;

try {

while (!isInterrupted()) {

r = taskQueue.take();

if(r!=null) {

System.out.println(getId()+" ready exec :"+r);

r.run();

}

r = null;//help gc;

}

} catch (Exception e) {

// TODO: handle exception

}

}

public void stopWorker() {

interrupt();

}

}

}

线程池的使用

public class TestMyThreadPool {

public static void main(String[] args) throws InterruptedException {

// 创建3个线程的线程池

MyThreadPool2 t = new MyThreadPool2(3,0);

t.execute(new MyTask("testA"));

t.execute(new MyTask("testB"));

t.execute(new MyTask("testC"));

t.execute(new MyTask("testD"));

t.execute(new MyTask("testE"));

System.out.println(t);

Thread.sleep(10000);

t.destroy();// 所有线程都执行完成才destory

System.out.println(t);

}

// 任务类

static class MyTask implements Runnable {

private String name;

private Random r = new Random();

public MyTask(String name) {

this.name = name;

}

public String getName() {

return name;

}

@Override

public void run() {// 执行任务

try {

Thread.sleep(r.nextInt(1000)+2000);

} catch (InterruptedException e) {

System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"

+Thread.currentThread().isInterrupted());

}

System.out.println("任务 " + name + " 完成");

}

}

}

什么是原子操作?如果实现原子操作?
  • 假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对批次来说是原子的。
    实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,知道该线程释放锁,这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?还有一种情况,如果有大量的线程来竞争资源,那CPU将会话费大量的时间和资源来处理这些竞争,同时还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿笨重。
    实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样罢了,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少,循环CAS就是在一个循环里不断的做CAS操作,直到成功为止。
image.jpeg
CAS实现原子操作的三大问题

ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
ABA问题的解决思路就是使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A,举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后你的同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作,比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij,从Java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

JDK中相关原子操作类的使用
AtomicInteger
•int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
•boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
•int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
•int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
AtomicIntegerArray
主要是提供原子的方式更新数组里的整型,其常用方法如下。
•int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
•boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

悲观锁和乐观锁
悲观锁,对某个数据进行处理的时候,总认为会有线程改我的数据,所以要先抢到锁,再进行工作,不管是synchronized和lock都是悲观锁机制
乐观锁,应该不会有人抢我的锁

1.get到数据 oldValue

2.写回 newValue

CAS(compare and swap)比较并且交换

1)比较数据是不是old value

  1. 如果没人改就写回 new value

volatile 易变的,保证可见性不能保证原子性

如果用volatile关键字修饰某个变量的时候就是告诉虚拟机这个变量很容易发生变化,用它的时候从主内存去读取,保证了数据的可见性,不能保证数据的一定正确。

面试题

synchronized修饰普通方法和静态方法的区别?什么是可见性?
  • 对象锁用于对象实例方法的,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象的,我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类对应的class对象,类锁和对象锁之间也是互不干扰的。
    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看得到修改的值。
    由于线程对变量的所有操作都必须在工作内存中进行,而不是直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存,可是并不会及时的刷到主存中,而是会有一定时间差,很明显,这个时候线程A对变量V的操作对于线程B而言就不具备可见性了。
    要解决共享对象可见性这个问题,我们可以使用volatile关键字或者加锁。
CAS无锁编程的原理
  • 使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望值A和一个新值B,操作的时候如果这个地址存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作.
    CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少,循环CAS就是在一个循环里不断的做cas操作,直到成功为止。
ReentrantLock的实现原理
  • 线程可以重复进入任何一个它已经拥有的锁同步着的代码块,synchronized、ReentrantLock都是可重入的锁,在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每释放一次锁、进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。
Synchronized static 与非static锁的区别和范围
  • 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的,我们知道,类的对象实例可以由很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的事每个类的对应的class对象,类锁和对象锁之间也是互不干扰的。
volatile和synchronized有什么区别?
  • volatile是最轻量的同步机制。
    volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其它线程来说是立即可见的,但是volatile不能保证操作的原子性,因此多线程下的写符合操作会导致线程安全问题。
    关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
什么是守护线程?你是如何退出一个线程的?

Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作,这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程,我们一般用不上,比如垃圾回收线程就是Daemon线程。

线程的中止:
  • 要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop(),但是这些API是过期的,也就是不建议使用的,因为会导致程序可能工作在不确定状态下。
    安全的中止则是其它线程通过调用某个线程A的interrupt()方法对其进行中断操作,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断线程是否被中断,比不过Thread.interrupted()会同时将中断标识位改写为false。
sleep、wait、yield的区别,wait的线程如何唤醒他?
  • yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的,也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
    yield()、sleep()被调用后,都不会释放当前线程锁持有的锁。
    调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才回去执行wait方法后面的代码。
    wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。
    wait的线程使用notify/notifyAll()进行唤醒。
sleep是可中断的吗?

sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。

线程生命周期
  • Java中线程的状态分为6种:
    1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
    2.运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为"运行"。
    线程对象创建后,其它线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready),就绪状态的线程在获得CPU时间片后变为运行中状态(running).
    3.阻塞(BLOCKED):表示线程阻塞于锁。
    4.等待(WAITING):进入该状态的线程需要等待其它线程做出一些特定动作(通知或中断).
    5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
    6.终止(TERMINATED):表示该线程已经执行完毕。
线程池基本原理
  • 在开发过程中,合理地使用线程池能够带来3个好处。
    第一:降低资源消耗,第二:提高效应速度。第三:提高线程的可管理性。
    1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁).
    2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
    3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
    4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

有三个线程T1,T2,T3,怎么确保它们按顺序执行?
可以用join方法实现。

你可能感兴趣的:(多线程基础知识总结三)