进程是资源(CPU,内存)分配的最小单位。进程其实就是一个运行的程序。
线程是程序执行的最小单位。一个进程包含了至少一个线程。
各个进程之间相互独立,进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,这种操作非常昂贵。线程是共享进程中的数据的,使用相同的地址空间,因此线程切换的性能消耗远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
2创建线程的几种方式?1继承Thread
2实现Runnable接口
3实现Callable接口+FutureTask【有返回值的方法】
其实:实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。
注意在单机环境中,当多个线程同时访问一个共享资源是会出现线程安全问题,这时候就可以采用
1同步代码块
2 同步方法
3锁机制
进行解决。
继承 Thread 类
需要重写 run() 方法【run方法代表了启动线程后,线程要完成的任务 】,调用 start() 方法来启动线程
public class MyThread extends Thread{ // 继承Thread实现run方法 @Override public void run() { for (int i = 0; i < 10; i++) { // 获取线程的名字 String name = MyThread.currentThread().getName(); System.out.println("线程:"+name+"执行了"+i+"个。"); } } public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }
实现 Runnable 接口
需要重写 run() 方法【run方法代表了启动线程后,线程要完成的任务 】。通过 Thread 调用 start() 方法来启动线程。
public class MyRunnable implements Runnable{ // 实现runnable接口重写run方法 @Override public void run() { for (int i = 0; i < 10; i++) { // 获取线程的名字 String name = Thread.currentThread().getName(); System.out.println("线程:"+name+"执行了"+i+"个。"); } } public static void main(String[] args) { MyRunnable myThread = new MyRunnable(); // 参数为Runnable类型 Thread thread = new Thread(myThread); // 启动线程 thread.start(); } }
实现 Callable 接口,
重写call方法,与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable3Thread中的start和run有什么区别?{ // 实现Callable重写call方法 @Override public Integer call() { int sum = 0; for (int i = 0; i < 3; i++) { // 获取线程的名字 String name = Thread.currentThread().getName(); System.out.println("线程:"+name+"执行了"+i+"个。"); sum = sum + i; } return sum; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建实例对象 MyCallable myThread = new MyCallable(); // 创建FutureTask封装对象 FutureTask futureTask = new FutureTask (myThread); // 参数为Runnable类型 Thread thread = new Thread(futureTask); // 启动线程 thread.start(); // 获取返回值 System.out.println(futureTask.get()); // 3 } }
start方法是启动线程的方法。会创建一个新线程【是为了实现多线程运行】,然后执行run()方法。
run方法是线程启动过后,线程需要执行的方法。
4Thread和Runnable的区别和联系?联系:Thread类实现了Runable接口。都需要重写里面run()方法。
区别:继承Thread只能是单继承,而实现Runnable接口是多实现(扩展性更强)。
拓展问题:Java为什么是单继承和多实现?若为多继承,当多个父类中有重复的属性和方法时,子类的调用结果会含糊不清,因此采用单继承。
为什么是多实现呢?
实现接口就是拓展类的功能,就算接口中有重复的方法也没有关系,因为实现接口,就必须重写接口中的方法。
那么各个接口中重复的变量又是怎么回事呢?
接口中,所有属性都是 static final修饰的,即常量,这个什么意思呢,由于JVM的底层机制,所有static final修饰的变量都在编译时期确定了其值,若在使用时,两个相同的常量值不同,在编译时期就不能通过。
5sleep 和 wait的区别?1sleep方法是Thread【线程】类的静态方法。wait方法是Object类的方法,任何实例都能调用。
2sleep方法不会释放锁【线程休眠一会儿再执行】。wait方法会释放锁【只有等待notify调用才会重新获取锁进入可运行状态】,调用wait的前提是当前线程有锁。
5.1线程的生命周期?线程的生命周期指的是线程从创建到销毁的整个过程、通常情况下,线程的生命周期包含5种。
新建状态New:创建(new)一个线程对象后,该线程对象就处于新建状态,jvm为其分配了内存。
可运行状态Runnable:当调用了start方法后,该线程就进入了就绪状态。具有了cpu的使用权(cpu在内存中运行),等待系统的调度。
运行状态Running:线程处于就绪状态并且具有了Cpu的使用权,并开始执行run方法,则该线程处于运行状态。【只有就绪状态才能转换成运行状态】
阻塞状态Blocked:指的是该线程因某种原因(一个线程试图获取某个对象的资源,但该资源被其他线程所持有,则会进行堵塞状态)会让出CPU的使用权,暂时停止自己的执行,进入堵塞状态,比如调用了wait,sleep方法,当调用notify和notifyAll()方法,唤醒过后,会进入可运行状态
终止状态Terminated:线程正常执行结束,或者调用了stop()方法,或者线程抛出一个未捕获的异常或者错误,一旦进行死亡状态,就不能转换成其他状态了。
6创建线程池的7个参数有那些?1corePoolSize 线程池最小核心线程数:线程池中会维护的最小的线程数量。
2maximumPoolSize 线程池最大线程数量:当前线程数达到corePoolSize后且工作队列也存满了,如果继续有任务需要线程,就会进行创建,但是不会无限制的创建,然后根据maximumpoolsize参数来限制这个线程数量的。【最大线程数-核心线程数=非核心线程数】
3keepAliveTime 空闲线程存活时间:如果一个线程处于空闲而且是非核心线程【即当前线程数超过corepoolsize的话】,那么keepalivetime时间后,该线程就会进行销毁。
4unit 空闲线程存活时间单位:存活时间的单位:秒分钟等
5workQueue 工作队列:存放待执行任务的队列,当任务达到核心线程数过后,就会存储到工作队列中,任务调度时候再取出任务。
6threadFactory 线程工厂:创建线程的工厂,可以设定线程名,线程编号等
7handler 拒绝策略:就是当工作队列和线程池中数量达到最大线程数,还有新的任务提交进来,就需要执行拒绝策略了。
6.1创建线程池的七种方式public class ThreadPoolTest { @Test public void testFixedThreadPool() throws InterruptedException { // 创建了一个固定线程数为3的线程池 ExecutorService pools = Executors.newFixedThreadPool(3); // 调用线程池的某个线程去执行任务(参数就是要执行的任务) // 参数类型是Runnable类型 for (int i = 0; i < 10; i++) { pools.execute(new Runnable() { @Override public void run() { // 获取执行当前方法线程的名字 String name = Thread.currentThread().getName(); System.out.println("线程名称为:" + name + "开始干活了。"); } }); } // 线程休眠,因为代码是自上而下的,不会等待线程执行完成才结束。 // 线程休眠就是为了等待其他线程完成。 Thread.sleep(2000); } }7为什么需要使用线程池?
如果没有线程池,当有1000个请求,就会创建1000个线程,比较浪费资源和浪费时间。
线程池就是提前创建多个线程放在一个容器中,当需要使用的时候就不需要进行创建,而是直接从线程池进行获取,避免了频繁的创建和销毁线程,提高了执行效率。并且能够对线程的数量,创建,启动,销毁进行管理。
8线程池的执行流程1当有请求(任务)提交到线程池,首先会判断核心线程是否已经满了,如果没满,则创建一个新的线程执行当前请求(即使线程池中有空闲的线程)。
2如果核心线程数满了,就会将当前请求尝试存放到工作队列中,如果工作队列也满了,就会判断当前线程数是否小于线程池最大线程数,如果小于就创建一个线程执行当前请求,否则就调用拒绝策略,表示当前线程池拒绝接收任务。
execute源码解析
public void execute(Runnable command) { //如果任务为null,则抛出nullPointerException异常 if (command == null) throw new NullPointerException(); //ctl中保存的线程池当前的一些状态信息 int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { //如果addWorker方法创建新的核心线程成功,return,方法结束 if (addWorker(command, true)) return; //如果addWorker方法创建新的核心线程失败,重新获取ctl的int值(线程池状态可能已被修改) c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { //再次获取ctl的int值(任务入队的过程中线程池状态可能已被修改) int recheck = ctl.get(); // 再次通过isRunning方法获取线程池状态,如果线程池状态不是RUNNING状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。 if (! isRunning(recheck) && remove(command)) reject(command); //如果当前线程池为空就新则创建新的非核心线程并执行。 else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
addWorker源码解析
addWorker(Runnable firstTask, boolean core) 方法就是向线程池添加一个带有任务的工作线程:
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { //每次for循环都需要获取最新的ctl值 int c = ctl.get(); //获取当前线程池状态 int rs = runStateOf(c); //首先是检查线程池状态,检查状态是否是running,shutdown是队列是否有数据 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; for (;;) { //获取工作线程 int wc = workerCountOf(c); if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //使用CAS的方法给ctl的worker的数量加1,成功则跳出最外层循环 if (compareAndIncrementWorkerCount(c)) break retry; //CAS不成功则重新获取ctl的值 c = ctl.get(); //如果CAS不成功的原因是状态变了则重新进行外层循环 if (runStateOf(c) != rs) continue retry; } } //workerStarted表示woker是否被执行 boolean workerStarted = false; //workerAdded表示worker是否成功添加到workers boolean workerAdded = false; Worker w = null; try { //创建一个新的工作线程 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { //先持有锁,再创建线程 final ReentrantLock mainLock = this.mainLock; //可重入锁加锁 mainLock.lock(); try { //获取线程池的工作状态 int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { //检查线程是否已经已启动 if (t.isAlive()) throw new IllegalThreadStateException(); //把worker添加到workers里面 workers.add(w); //工作线程数 int s = workers.size(); //如果工作线程数s大于largestPoolSize,设置largestPoolSize为s if (s > largestPoolSize) largestPoolSize = s; //设置workerAdded为true workerAdded = true; } } finally { //可重入锁解锁 mainLock.unlock(); } //如果workerAdded为true,启动线程 if (workerAdded) { t.start(); workerStarted = true; } } } finally { //如果工作线程启动失败,则删除此工作线程 if (! workerStarted) addWorkerFailed(w); } //返回工作线程启动结果 return workerStarted; }
以上两个方法是线程池执行过程中涉及到的两个重要方法的源码解析,线程池提交任务的方式是有两种,一种是通过execute,另一种就是通过submit;通过submit源码中可以看出也是通过execute。
publicFuture submit(Callable task) { if (task == null) throw new NullPointerException(); RunnableFuture ftask = newTaskFor(task); execute(ftask); return ftask; }
submit方法的不同之处是对runnable的封装。二者的不同可总结为:对返回值的处理不同、对异常的处理不同。
8.1线程池拒绝策略有几种?拒绝策略,当线程池任务超过 最大线程数+队列排队数 ,就会触发线程池的拒绝策略。
-
AbortPolicy【线程池默认的拒绝策略】:丢弃任务并抛出RejectedExecutionException【拒绝执行异常】异常;
-
DiscardPolicy丢弃任务,也不会抛出异常,相对而言存在一定的风险;
-
DiscardOldestPolicy丢弃队列中最老实的一个任务,然后重新尝试执行任务;
-
CallerRunsPolicy:当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
在我佳佳菜场发送站内信的时候使用了线程池,【用来对MQ的消息进行消费时创建线程池,通过线程池的线程进行消费消息,提高执行效率】
我知道一些技术使用到了线程池,比如Hystrix熔断器对资源进行隔离采用信号量隔离,是指用一个线程池来存储当前的请求,可以通过设置线程池的最大线程数和最大排队数来限制请求数量,超过这个总数的请求立即进行失败,返回兜底数据。
10ThreadLocal的作用&使用场景和底层原理?作用:ThreadLocal是线程变量,作用就是为了解决线程安全问题的,通过ThreadLocal线程变量给每一个线程提供一个独立的变量副本,该变量只属于当前线程,达到线程间数据隔离目的。
底层原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.在Thread线程对象中有一个ThreadLocalMap,底层是map结构,有一个Entry是一个键值对,key是当前ThreadLocal,Value是存储的变量副本。
当我们调用set方法时,会调用ThreadLocal.set方法时,会拿到当前Thread,获取到线程中的ThreadLocalMap,以当前ThreadLocal为key,变量副本为Value进行存储。
当我们调用get方法时,就会在当前线程里的ThreadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本Value。
使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求
11你用过JUC【线程并发库】中的类吗,说几个?线程并发库指的是【java.util.concurrent 】下的类,
比如说我们常见的Lock锁(显示锁):ReentrantLock(可重复锁)
Atomic(原子类)如:AtomicInteger ;AtomicLong 。为了实现原子性操作提供的一些类,使用的是乐观锁。
线程池相关的类:Callable,Executor(创建线程池的)
并发容器类:ConcurrentHashMap(线程安全)
12SpringMVC的Controller是单例还是多例,有没有并发安全问题,如何解决在spring中,bean默认都是单例的,controller也是交给spring容器管理的一个bean,因此它也是单例的。
单例的好处是减少了创建对象和垃圾回收的时间,节省了内存资源,但同时单例会造成线程不安全的问题,因为当所有请求访问同一个controller实例,controller中的成员变量是所有线程公用的,某个线程如果修改了这个变量,别的请求再来拿这个变量就是修改后的值了
要解决这个问题,最直接有效的方式就是不要在controller中定义成员变量,如果你非要定义成员变量,两种方式
第一种,可以给controller上加注解@Scope("prototype"),将controller设置为多例模式,每次请求都重新实例化一个controller
第二种,使用ThreadLocal线程变量,让每一个线程都有自己独立的变量【原理】,对变量进行操作时,就不会出现线程安全问题