【java面试02-Java多线程】

1. 什么是线程和进程?

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多 个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

一个 Java 程序的运行是 main 线程和多个其他线程同时运行。

在这里插入图片描述
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

1.1程序计数器为什么是私有的?

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
    所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

1.2虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、 常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈 中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

1.3堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2. 并发与并行的区别?

并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生,而并行是物理上的同时发生。

3.为什么要使用多线程呢?

先从总体上来说:
从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换 和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少 了线程上下文切换的开销。
从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线 程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能 力以及性能。
再深入到计算机底层来探讨:
单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子: 当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我 们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了, 当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在 理想情况下达到 100%了。
多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复 杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就 可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。

4.使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁 。

5.线程的生命周期和状态?

在这里插入图片描述
通过 sleep(􏰍long millis)􏰐 方法或 wait(􏰍long millis􏰐) 方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。

6.什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个 线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转 的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文 切换和模式切换的时间消耗非常少。

7.什么是线程死锁?如何避免死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件。

8.sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于: sleep() 方法没有释放锁,而 wait() 方法释放了锁。 两者都可以暂停线程的执行。
wait() 通常被用于线程间交互/通信, sleep() 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或 者 notifyAll() 方法。 sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

9.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?【经典面试题】

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

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

10.说一说自己对于 synchronized 关键字的了解

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

11.说说自己是怎么使用 synchronized 关键字

synchronized 关键字最主要的三种使用方式:

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() { //业务代码

}
  1. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new 了多少个对象,只有一份)。所以,如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized void staic method() { //业务代码

}
  1. 修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。 synchronized(􏰖.class) 表示进入同步代码前要获得当前 class 的锁.
synchronized(this) { //业务代码

}

总结:

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

12.构造方法可以使用 synchronized 关键字修饰么?

构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说。

13.JMM(Java 内存模型)

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别 的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器) 中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷⻉,造成数据的不一致。
要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

14.说说 synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
volatile 关键字主要用于解决变量在多个线程之间的可⻅性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

15.ThreadLocal

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。
ThreadLocal 类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成 存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get􏰝􏰛 和 set􏰝􏰛 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

16.线程池

16.1为什么要用线程池?

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降 低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

16.2实现 Runnable 接口和 Callable 接口的区别

Runnable 自 Java 1.0 以来一直存在,但 Callable 仅在 Java 1.5 中引入,目的就是为了来处理 Runnable 不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

  1. Callable接口中call方法返有返回值,Runnable接口中run方法没有返回值
  2. Callable接口中call方法有声明异常,Runnable接口中run方法没有异常

工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。( Executors.callable􏰍Runnable task )或 Executors.callable􏰍Runnable task􏰑Object resule􏰐 )。

16.3执行 execute()方法和 submit()方法的区别是什么呢?

  1. execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future类型的对象,通过这个Future 对象可以判断任务是否执行成功,并且可以通过 Future的get()方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,而使用 get(􏰍long timeout 􏰑TimeUnit unit􏰐)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

16.4 Atomic 原子类

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所谓原子类说简单点就是具有原子/原子操作特征的类。

16.5 AQS了解么?

AQS 的全称为( AbstractQueuedSynchronizer ),这个类在java.util.concurrent.locks 包下面。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock , Semaphore,其他的诸如ReentrantReadWriteLock , SynchronousQueue, FutureTask等等皆是基于AQS 的。当然,我们 自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
AQS(AbstractQueuedSynchronizer)原理图:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值