java多线程-面试相关

Posted 尚墨1111

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程-面试相关相关的知识,希望对你有一定的参考价值。

1. 概念

在这里插入图片描述

1.1 区分程序、进程、线程

在这里插入图片描述

1.2 线程和进程有什么区别?

线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

⼀个进程中可以有多个线程,多个线程共享进程的堆和⽅法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地⽅法栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PxnrSiXQ-1620632161266)(image/多线程/1620444149647.png)]

程序计数器:⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置

虚拟机栈: ⽤于存储局部变量表、操作数栈、常量池引⽤等信息。
本地方法栈: 和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java方法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。

:是进程中最⼤的⼀块内存,主要用于存放新创建的对象 (所有对象都在这⾥分配内存)

方法区:主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.3. 说说并发与并行的区别?

并发: 同⼀时间段,多个任务都在执行 (单位时间内不⼀定同时执行);
并行: 单位时间内,多个任务同时执行。

1.4 多线程

1.4.1 为啥使用多线程

开销小,提高CPU的利用率

高并发的基础

可能会造成的问题:内存泄漏、上下文切换、死锁

1.4.2 啥叫上下文切换

当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下文切换

1.4.3 Java中如何实现多线程以及比较优缺

2.实现线程的方法 ———— 四种:
 *     |---继承 Thread 类,重写 run() 方法,里面写要执行的操作。创建实例对象。
 *         |--- class MyThread extends Thread {@Override public void run() {}}
 *         |--- MyThread thread1 = new MyThread();
 *         |--- thread.start(),启动线程,start()会调用 run()方法运行程序
 *
 *         |---不足:只能单继承,所以数据不能共享。
 *
 *     |---实现 Runnable 接口,重写 run() 方法,再把实例对象传入 Thread ,创建线程的实例对象
 *         |--- class MyThread implements Runnable{@Override public void run() {}}
 *         |--- 创建实例化对象:MyThread mythread = new MyThread();
 *         |--- 传入线程:Thread thread = new Thread(mythread);
 *         |--- 正常使用 thread.start()
 *
 *         |---不足:无返回值,数据共享
 *
 *     |---实现 Callable 接口,重写call(),多一层 FutureTask,call()run()更强大有返回值。
 *         |--- class MyThread implements Callable{@Override public void call() {}}
 *         |--- 创建实例化对象:MyThread mythread = new MyThread();
 *         |--- 实例化对象传入 FutureTask,FutureTask futureTask = new FutureTask(mythread);
 *         |--- 实例化对象传入 Thread,Thread thread = new Thread(futureTask)
 *         |--- 正常使用 thread.start()
 *
 *         |---不足:创建复杂,无法重复利用,有返回值。
 *
 *     |---线程池创建,一次创建多个,要就取,用完了再放回,可以重复使用。
 *        |--初始化创建池子:
 *            ExecutorService service = Executors.newFixedThreadPool(10);
 *            ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
 *        |-- 传入Thread实例对象,执行指定的线程池操作:service.execute(new Thread());
 *        |-- 关闭线程池:service.shutdown();

1.5 线程池

在这里插入图片描述

1.6 线程分类:

在这里插入图片描述

1.7 说说乐观锁与悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁.

2. Thread类

2.0 实现线程的四种方法

 实现线程的方法 ———— 四种:
 *     |---继承 Thread 类,重写 run() 方法,里面写要执行的操作。创建实例对象。
 *         |--- class MyThread extends Thread {@Override public void run() {}}
 *         |--- MyThread thread1 = new MyThread();
 *         |--- thread.start(),启动线程,start()会调用 run()方法运行程序
 *
 *         |---不足:只能单继承,所以数据不能共享。
 *
 *     |---实现 Runnable 接口,重写 run() 方法,再把实例对象传入 Thread ,创建线程的实例对象
 *         |--- class MyThread implements Runnable{@Override public void run() {}}
 *         |--- 创建实例化对象:MyThread mythread = new MyThread();
 *         |--- 传入线程:Thread thread = new Thread(mythread);
 *         |--- 正常使用 thread.start()
 *
 *         |---不足:无返回值,数据共享
 *
 *     |---实现 Callable 接口,重写call(),多一层 FutureTask,call()run()更强大有返回值。
 *         |--- class MyThread implements Callable{@Override public void call() {}}
 *         |--- 创建实例化对象:MyThread mythread = new MyThread();
 *         |--- 实例化对象传入 FutureTask,FutureTask futureTask = new FutureTask(mythread);
 *         |--- 实例化对象传入 Thread,Thread thread = new Thread(futureTask)
 *         |--- 正常使用 thread.start()
 *
 *         |---不足:创建复杂,无法重复利用,有返回值。
 *
 *     |---线程池创建,一次创建多个,要就取,用完了再放回,可以重复使用。
 *        |--初始化创建池子:
 *            ExecutorService service = Executors.newFixedThreadPool(10);
 *            ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
 *        |-- 传入Thread实例对象,执行指定的线程池操作:service.execute(new Thread());
 *        |-- 关闭线程池:service.shutdown();

2.1 Thread类的方法

Thread 类的基本方法:
 *     |--- start(),启动线程
 *     |--- run(),执行线程
 *     |--- currentThread(),当前执行的线程
 *     |--- getName()setName(),获取设定线程的名字
 *     |--- sleep(),进入阻塞状态,不释放资源
 *     |--- yield(),释放资源
 *     |--- join().挂起当前线程,执行另一个线程
 *     |--- isAlive(),线程是否活着

2.2 线程启动

为什么我们调⽤ start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间片后就可以开始运⾏了。 start() 会执行线程的相应准备⼯作,然后⾃动执行 run() 方法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() 方法,会把 run()方法当成⼀个 main 线程下的普通⽅法去执行,并不会在某个线程中执行它,所以这并不是多线程⼯作。

总结: 调用 start() 方法可启动线程并使线程进⼊就绪状态,直接执行 run() 方法的话不会以多线程的方式执行

start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。

2.3 线程的生命周期

说法不一,以下是Thread 类中State的定义,以源码为准
在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5SoqKdUn-1620632161274)(image/多线程/1620456443193.png)]

2.4 线程通信:wait()、sleep()、yield()、notify()、notifyAll() 辨析

sleep(),的作用是让当前线程休眠不放锁,正在执行的线程主动让出cpu,然后cpu就可以去执行其他任务,当时间过后该线程重新由“阻塞状态”编程“就绪状态”,从而等待cpu的调度执行,注意:sleep方法只是让出了cpu的执行权,并不会释放同步资源锁。

yield()的作用是让步,它能够让当前线程从“运行状态”进入到“就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权

wait()释放锁,让当前线程处于“等待(阻塞)状态”,直到其他线程**调用此对象的 notify() 方法或 notifyAll() **方法”,当前线程被唤醒(进入“就绪状态”)。

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

  • |— wait() 与 sleep()
  • |—wait():阻塞,释放监视器
  • |—sleep():阻塞,不放
    • |— wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
    • |— Thread类: sleep() , Object类中: wait()、notify()、notifyAll()

为什么把wait()等方法放在Object类中

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。

wait等的是某个对象上的锁,多个线程竞争这个锁,wait和notify方法都放到目标对象上,那这个对象上可以维护线程的队列,可以对相关线程进行调度。

如果将wait方法和线程队列都放到Thread中,线程正在等待的是哪个锁 就不明显了,那么就必然要求某个Thread知道其他所有Thread的信息。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

2.5 线程死锁

在这里插入图片描述
在这里插入图片描述

3 关键字

3.1 说说你对synchronized关键字的了解

在这里插入图片描述

3.2 双重校验锁

在这里插入图片描述

在这里插入图片描述

3.3 java内存模型(JMM)

在当前的Java内存模型下,线程可以把变量保存本地内存中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的変量值的拷贝,造成数据的不一致。

要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进行读取
在这里插入图片描述

3.4 并发编程三要素

  1. 原子性: 一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。synchronized可以保证原子性
  2. 有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序,volatile可以禁止指令进行重排序)
  3. 可见性: 一个县城对共享变量的修改,另一个线程能够立刻看到。volatile可以保证可见性。

3.5 volatile 和synchronized 的区别

在这里插入图片描述

4. ThreadLocal类:专属本地变量

如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副
本,他们可以使⽤ get 和 set ⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题

在这里插入图片描述

5. 线程池

5.1 submit 和excute 的区别是什么

在这里插入图片描述

5.2 如何创建线程池

在这里插入图片描述

5.3 excute 方法的原理

在这里插入图片描述


6.Auto 原子类

7. AQS原理


致谢

以上整理参考 javaGuide,及其他文章,在此感谢!

以上是关于java多线程-面试相关的主要内容,如果未能解决你的问题,请参考以下文章

Java进阶之光!2021必看-Java高级面试题总结

Java工程师面试题,二级java刷题软件

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

面试官:Java多线程有哪几种实现方式如何实现?

记一个Java多线程相关的面试题

Java面试知识点1——多线程和并发编程