面试题-12多线程

什么是线程和进程?

进程 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

进程与线程的区别

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight
Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight
Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

1.为什么要使用线程池?

创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。

2线程池有什么作用?

1、提高效率 创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。 2、方便管理
可以编写线程池管理代码对池中的线程同一进行管理,比如说启动时有该程序创建100个线程,每当有请求的时候,就分配一个线程去工作,如果刚好并发有101个请求,那多出的这一个请求可以排队等候,避免因无休止的创建线程导致系统崩溃。

线程池策略

corePoolSize:核心线程数;maximunPoolSize:最大线程数 每当有新的任务到线程池时, 第一步:
先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;
第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步; 第三步:
判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用饱和策略来处理这个任务。注意:
在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。
(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。)

饱和策略

当工作队列满且线程个数达到maximunPoolSize后所采取的策略
AbortPolicy:默认策略;新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRunsPolicy:既不抛弃任务也不抛出异常,使用调用者所在线程运行新的任务。
DiscardPolicy:丢弃新的任务,且不抛出异常。
DiscardOldestPolicy:调用poll方法丢弃工作队列队头的任务,然后尝试提交新任务
自定义策略:根据用户需要定制。

证明线程池ThreadPoolExecutor的核心线程数,最大线程数,队列长度的关系
关于线程池的几个参数,很多人不是很清楚如何配置,他们之间是什么关系,我用代码来证明一下。

package www.itbac.com;

import java.util.concurrent.*;

public class ExecutorTest {
public static void main(String[] args) {
// 创建线程池 , 参数含义 :(核心线程数,最大线程数,加开线程的存活时间,时间单位,任务队列长度)
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 8,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(2));
//设置:任务数 = 3 ~ 11 ,分析:任务数 与 活跃线程数,核心线程数,队列长度,最大线程数的关系。

int a = 3;
for (int i = 1; i <= a; i++) {
int j = i;
pool.submit(new Runnable() {
@Override
public void run() {
//获取线程名称
Thread thread = Thread.currentThread();
String name = thread.getName();
//输出
int activeCount = pool.getActiveCount();
System.out.println("任务:"+j+"-----,线程名称:"+name+"-----活跃线程数:"+activeCount);
}
});
}
//关闭线程池
pool.shutdown();

}
}
 
输出结果,观察关系:
//任务数 a = 3 , 活跃线程数3 , 任务数 < 核心线程数。
//任务数 a = 4 , 活跃线程数4 , 任务数 < 核心线程数。
//任务数 a = 5 , 活跃线程数5 , 任务数 = 核心线程数。
//任务数 a = 6 , 活跃线程数5 , 任务数 < 核心线程数5 + 队列长度2 。
//任务数 a = 7 , 活跃线程数5 , 任务数 = 核心线程数5 + 队列长度2 。
//任务数 a = 8 , 活跃线程数6 , 任务数 < 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加1个活跃线程。
//任务数 a = 9 , 活跃线程数7 , 任务数 < 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加2个活跃线程。
//任务数 a = 10 , 活跃线程数8 , 任务数 = 最大线程数8 + 队列长度2 。活跃线程数是在核心线程数5的基础上,加3个活跃线程。
//任务数 a = 11 , 活跃线程数8 , 任务数 > 最大线程数8 + 队列长度2 。抛出异常RejectedExecutionException

总结:

随着任务数量的增加,会增加活跃的线程数。 当活跃的线程数 = 核心线程数,此时不再增加活跃线程数,而是往任务队列里堆积。
当任务队列堆满了,随着任务数量的增加,会在核心线程数的基础上加开线程。 直到活跃线程数 = 最大线程数,就不能增加线程了。

如果此时任务还在增加,则: 任务数11 > 最大线程数8 + 队列长度2
,抛出异常RejectedExecutionException,拒绝任务。

在这里插入图片描述

/**
* 线程池 不推荐使用这种方式
*/
public class ThreadPool {
public static void main(String[] args) {
ExecutorService thread1 = Executors.newSingleThreadExecutor();//单线程
//固定大小的线程
ExecutorService thread2 = Executors.newFixedThreadPool(5);
//可伸缩的 遇强则强
ExecutorService thread3 = Executors.newCachedThreadPool();

//用完要关闭
try {
for (int i = 0; i < 100; i++) {
thread1.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
thread1.shutdown();
}
}
}

在这里插入图片描述

银行例子:先开两个窗口(核心线程数),然后人多了
就去候客区等待(阻塞队列),候客区满了则开新的窗口(最大线程数),新窗口也满了,再来人则不让他进来办理业务(拒绝策略)

拒绝策略:

1 new ThreadPoolExecutor.AbortPolicy());//银行满了,还有人进来 抛出异常
2.new ThreadPoolExecutor.CallerRunsPolicy()); 哪来的回哪去 main线程执行main ok
3 new ThreadPoolExecutor.DiscardPolicy());任务满了,不抛出异常 丢掉任务
4.new ThreadPoolExecutor.DiscardOldestPolicy());任务满了,尝试和最早的任务竞争,如果最早的没结束依然丢掉任务,不抛出异常

最大线程数如何定义?

1、CPU 密集型:电脑几核 就是几,可以保证cpu 的效率最高
2、IO 密集型 :两倍任务数 判断你程序中十分耗IO的线程 程序,
15个大型任务,io十分占有资源 获取CPU线程数 Runtime.getRuntime().availableProcessors();

/**
* 线程池
* 拒绝策略:
* 1. new ThreadPoolExecutor.AbortPolicy());//银行满了,还有人进来 抛出异常
* 2.new ThreadPoolExecutor.CallerRunsPolicy()); 哪来的回哪去 main线程执行main ok
* 3. new ThreadPoolExecutor.DiscardPolicy());任务满了,不抛出异常 丢掉任务
* 4.new ThreadPoolExecutor.DiscardOldestPolicy());任务满了,尝试和最早的任务竞争,如果最早的没结束 依然丢掉任务,不抛出异常
*/
public class ThreadPool {
public static void main(String[] args) {
//自定义线程池 ThreadPoolExecutor 工作用
/**
* 最大线程数如何定义?
* 1、CPU 密集型:电脑几核 就是几,可以保证cpu 的效率最高
* 2、IO 密集型 :两倍任务数 判断你程序中十分耗IO的线程
* 程序, 15个大型任务,io十分占有资源
* 获取CPU线程数 Runtime.getRuntime().availableProcessors();
*/
ExecutorService thread2 = new ThreadPoolExecutor(
	3, //核心线程数
	Runtime.getRuntime().availableProcessors(), //最大线程数
	3, //加开线程的存活时间
	TimeUnit.SECONDS, //时间单位
	new LinkedBlockingQueue<Runnable>(1), //任务队列长度
	Executors.defaultThreadFactory(), //创建线程工厂
	new ThreadPoolExecutor.AbortPolicy()); //拒绝策略 Abort表示满了抛出异常
//用完要关闭
try {
//最大承载任务 = max + queue
//超出:RejectedExecutionException
//使用线程池来创建线程
	for (int i = 0; i < 7; i++) {
		thread2.execute(()->{
		System.out.println(Thread.currentThread().getName()+"ok");
		});
	}
} catch (Exception e) {
	e.printStackTrace();
} finally {
	thread2.shutdown();
}
}
}

3.说说几种常见的线程池及使用场景

· newFixedThreadPool(固定大小的线程池)
· newSingleThreadExecutor(单线程线程池) ·
. newCachedThreadPool(可缓存线程的线程池)
· newScheduledThreadPool

1、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

2、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3、newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

4、newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

4.OOM

什么是OOM? OOM,全称“Out Of
Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown
when the Java Virtual Machine cannot allocate an object because it is
out of memory, and no more memory could be made available by the
garbage collector.
意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

5. 线程池有哪几种工作队列

ArrayBlockingQueue (有界队列):是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue (无界队列):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出)
排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue(同步队列):
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
DelayQueue(延迟队列):一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
PriorityBlockingQueue(优先级队列): 一个具有优先级得无限阻塞队列。

5.9线程池重要的参数

corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
unit(必需):指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue(必需):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。
threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

7.执行流程

· 当有任务进入时,线程池创建线程去执行任务,直到核心线程数满为止 · 核心线程数量满了之后,任务就会进入一个缓冲的任务队列中
1.当任务队列为无界队列时,任务就会一直放入缓冲的任务队列中,不会和最大线程数量进行比较
2.当任务队列为有界队列时,任务先放入缓冲的任务队列中,当任务队列满了之后,才会将任务放入线程池,此时会拿当前线程数与线程池允许的最大线程数进行比较,如果超出了,则默认会抛出异常。如果没超出,然后线程池才会创建线程并执行任务,当任务执行完,又会将缓冲队列中的任务放入线程池中,然后重复此操作。

8.无界队列和有界队列

有界队列:即长度有限,满了以后ArrayBlockingQueue会插入阻塞。
无界队列:就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常
有界队列
1.初始的poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。
2.当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。
3.有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
4.如果3中也无法处理了,就会走到第四步执行reject操作。
无界队列
与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略

9单机上一个线程正在处理服务,如果忽然断电了怎么办
(正在处理和阻塞队列里的请求怎么处理)

我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。
即: 阻塞队列持久化,正在处理事物控制。断电之后正在处理的回滚,日志恢复该次操作。服务器重启后阻塞队列中的数据再加载

9.1拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现RejectedExecutionHandler接口,并实现rejectedExecution(Runnable
r, ThreadPoolExecutor executor)方法。不过Executors框架已经为我们实现了4种拒绝策略:
AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

10线程常见面试题

java实现线程有哪几种方式? 1、继承Thread类实现多线程 2、实现Runnable接口方式实现多线程
3、使用ExecutorService、Callable、Future实现有返回结果的多线程

多线程同步有哪几种方法?

Synchronized关键字,Lock锁实现,分布式锁等。

Runnable和Thread用哪个好?

Java不支持类的多重继承,但允许你实现多个接口。所以如果你要继承其他类,也为了减少类之间的耦合性,Runnable会更好。

Java中notify和notifyAll有什么区别?

notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。

为什么wait/notify/notifyAll这些方法不在thread类里面?

这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

为什么wait和notify方法要在同步块中调用?

主要是因为Java
API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。

什么是死锁?如何避免死锁?
死锁就是两个线程相互等待对方释放对象锁。

启动线程方法start()和run()有什么区别?

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。

说明白一点:
我们一般使用thread都是new一个线程,然后调用start方法启动,使用start方法才真正实现了多线程运行,因为这个时候不用等待我们的run方法执行完成就可以继续执行下面的代码,这才叫多线程嘛!因为thread线程有5种状态,创建-就绪-运行-阻塞-死亡这五种,那么我们的start方法呢就是就绪这一步,因为这个时候我们的线程并没有立即的执行,而是得等待,等到我们的cpu有空闲的时候,才会执行线程里面的run方法,等run方法执行完了,线程就结束了。
那么我们直接使用thread执行run方法会咋样呢?因为run方法是thread里面的一个普通的方法,所以我们直接调用run方法,这个时候它是会运行在我们的主线程中的,因为这个时候我们的程序中只有主线程一个线程,所以如果有两个线程,都是直接调用的run方法,那么他们的执行顺序一定是顺序执行,所以这样并没有做到多线程的这种目的。

下面来写一段简单的代码,运行一下看看效果,加深大家的印象。

public class ThreadDemo {
    public static void main(String[] args){
        Thread thread1 = new Thread(new MyRunnable());
        Thread thread2 = new Thread(new MyRunnable());
        thread1.start();
        thread2.start();
    }
}
class MyRunnable implements Runnable {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                Thread.sleep(100);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

根据我们上面的分析,这两个线程都是使用start方法开启的,所以并不需要等待另一个完成,所以他们的执行顺序应该是并行的,我们运行看一下结果:
在这里插入图片描述

和我们分析的一样,那么我们改改代码,执行使用run方法,看看是否顺序执行

 public static void main(String[] args){
    Thread thread1 = new Thread(new MyRunnable());
    Thread thread2 = new Thread(new MyRunnable());
    thread1.run();
    thread2.run();
}

在这里插入图片描述

多线程之间如何进行通信?
wait/notify

什么是活锁、饥饿、无锁、死锁?

死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。
举个例子,A同学抢了B同学的钢笔,B同学抢了A同学的书,两个人都相互占用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS原理及应用即是无锁的实现。
可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合下是非常高效的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zyf_fly66

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

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

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

打赏作者

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

抵扣说明:

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

余额充值