首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Java面试总结】多线程

【Java面试总结】多线程

作者头像
Rochester
发布2020-09-07 15:01:19
8630
发布2020-09-07 15:01:19
举报
文章被收录于专栏:牛人NR牛人NR

多线程

1. 什么是线程和进程?

进程:

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个即是一个进程从创建、运行到消亡的过程。

在Java中,当我们启动 main 函数时,其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)

线程:

线程比进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器、虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小的多,也正因如此,线程也被称作轻量级进程。

Java 天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程。

代码如下:

package multiThread;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * @author silentCow
 * @Date 2020/9/5 10:33
 */
public class MultiThread {
    public static void main(String[] args) {

        // 获取 Java 线程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID 和 线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("["+threadInfo.getThreadId() +"] "+threadInfo.getThreadName());
        }

    }
}

输出结果:(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行main 方法即可)

[6] Monitor Ctrl-Break
[5] Attach Listener  //添加事件
[4] Signal Dispatcher     // 分发处理给 JVM 信号的线程
[3] Finalizer    //调用对象 finalize 方法的线程
[2] Reference Handler     //清除 reference 线程
[1] main    //main 线程,程序入口

综上可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行。

2. 请简要描述线程与进程的关系,区别以及优缺点

从 JVM 角度说进程和线程之间的关系

详细内容请阅读:Java相关/可能是把Java内存区域讲的最清楚的一篇文章

图解进程和线程的关系

从上图可以看出:一个进程可以有多个线程,多个线程共享进程的方法区(JDK 1.8 之后的元空间)资源,但是每个线程有 自己的程序计数器虚拟机栈本地方法栈

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

扩展内容: 思考:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

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

程序计数器主要有两个作用:

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

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了:线程切换后能够恢复到正确的执行位置

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

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

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

一句话简单了解堆和方法区

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

3. 说说并发和并行的区别

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

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

先从总体上来说:

  • 从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能

再深入到计算机底层来探讨:

  • 单核时代:在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到100%了。
  • 多核时代:多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。

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

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

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

点击跳转【Java面试】Java基础(下篇)/#30-线程有哪些基本状态?线程有哪些基本状态

7. 什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

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

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

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

认识线程死锁

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

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

package deadLock;

/**
 * @author silentCow
 * @Date 2020/9/6 14:59
 */
public class DeadLockDemo {

    private static Object resource1 = new Object();// 资源1
    private static Object resource2 = new Object();// 资源2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();
        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 2,5,main]waiting get resource1
Thread[线程 1,5,main]waiting get resource2

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

产生死锁必须具备的条件:

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

如何避免线程死锁?

上面说了产生死锁的必备条件,那么为了避免死锁,只需破坏其中的一个条件即可:

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

线程 2 的代码修改成下面这样就不会产生死锁了

new Thread(() -> {
    synchronized (resource1) {
        System.out.println(Thread.currentThread() + "get resource1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread() + "waiting get resource2");
        synchronized (resource2) {
            System.out.println(Thread.currentThread() + "get resource2");
        }
    }
}, "线程 2").start();

输出:

Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

分析上面的代码为什么避免了死锁?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

9. 说说 sleep() 方法和 wait() 方法区别和共同点

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

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

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

总结: 调用 start 方法可启动线程并使线程进入就绪状态;而 run 方法只是 Thread 的一个普通方法,还是在主线程里执行。

11. synchronized 关键字

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

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

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较⻓的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗

暂无,后期写

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

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

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

4. 讲一下 synchronized 关键字的底层原理

暂 略

5. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

详细请点击阅读

6. 谈谈 synchronized和ReentrantLock 的区别

①、两者都是可重入锁

“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

②、synchronized 依赖于 JVM ,而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了虚拟机团队在 JDK1.6synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLockJDK层面实现的(也就是 API 层面,需要lock()unlock()方法配合try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③、ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。ConditionJDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

④、性能已不是选择标准

12. volatile关键字

暂略

13. ThreadLocal

暂略

14. 线程池

1. 为什么要用线程池?

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率

线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处

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

2. 实现Runnable接口和Callable接口的区别

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

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

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

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-09-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 多线程
    • 1. 什么是线程和进程?
      • 进程:
      • 线程:
    • 2. 请简要描述线程与进程的关系,区别以及优缺点
      • 图解进程和线程的关系
      • 程序计数器为什么是私有的?
      • 虚拟机栈和本地方法栈为什么是私有的?
      • 一句话简单了解堆和方法区
    • 3. 说说并发和并行的区别
      • 4. 为什么要使用多线程呢?
        • 5. 使用多线程可能带来什么问题?
          • 6. 说说线程的生命周期和状态?
            • 7. 什么是上下文切换?
              • 8. 什么是线程死锁?如何避免死锁?
                • 认识线程死锁
                • 如何避免线程死锁?
              • 9. 说说 sleep() 方法和 wait() 方法区别和共同点
                • 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用run() 方法?
                  • 11. synchronized 关键字
                    • 1.说一说自己对于 synchronized 关键字的了解
                    • 2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
                    • 3. synchronized 关键字最主要的三种使用方式
                    • 4. 讲一下 synchronized 关键字的底层原理
                    • 5. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
                    • 6. 谈谈 synchronized和ReentrantLock 的区别
                  • 12. volatile关键字
                    • 13. ThreadLocal
                      • 14. 线程池
                        • 1. 为什么要用线程池?
                        • 2. 实现Runnable接口和Callable接口的区别
                        • 3. 执行execute()方法和submit()方法的区别是什么呢?
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档