Java并发相关题目(面试使用)

72 阅读9分钟

掘金日新计划 · 6 月更文挑战」的第13天

Java并发

并行和并发的区别?

  • 并行是指两个或多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生;
  • 并行是在不同实体上的多个事件;并发是在同一实体上的多个事件;
  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。

所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

线程和进程的区别?

  • 进程是程序的一次执行过程,是系统运行程序的基本单位
  • 线程与进程类似,但线程是一个比进程更小的执行单位。一个进程执行过程中可以产生多个线程,

在Java中,启用一个main方法就是启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个线程,也称为主线程。

从JVM角度分析进程和线程的关系

根据JVM的内存划分,对于线程而言:多个线程共享进程的堆、方法区资源,但每个线程又有自己的程序计数器、虚拟机栈、本地方法栈。

也就是说,在一个JVM进程中,可以存在多个线程,每个线程都共享了这个JVM进程的方法区、堆;并且每个线程又都具有自己的虚拟机栈、本地方法栈、程序计数器等。

为什么方法区和堆是线程共享区?

  • 方法区(Method Area)  存储已被虚拟机加载的类信息、常量、静态变量等数据。方法区中又包含 运行时常量池 ,这部分区域储存Class文件信息和编译期生成的各种字面量和符号引用。
  • 堆(Heap)  堆内存储存了对象实例(比如new关键字创建的实例对象),它是JVM中内存区最大的一块区域。

所以,一个进程的启动可能包含了多个线程,而这个进程中的静态变量等都是随着类加载而加载的,他应该不属于某个线程独有,所以将其存储于方法区中。对象实例都储存在Java堆内存中,作为Java最大的一块内存区域,肯定不能是某个线程独占的。

为什么虚拟机栈和本地方法栈是线程独占区?

  • 虚拟机栈: 每个Java方法执行的同时都会创建一个栈帧储存局部变量表、操作数栈、方法出口等。从方法的执行到结束,对应将栈帧压入Java虚拟机栈和从虚拟机栈中弹出的过程。
  • 本地方法栈: 本地方法栈类似Java虚拟机栈,只不过Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。

程序计数器是什么?

掘金日新计划 · 6 月更文挑战」的第13天 程序计数器(Program Counter Register) :当前线程执行的字节码的行号指示器。每个线程都有独立的程序计数器。此内存区域是Java虚拟机中唯一一个没有任何OutOfMemoryError情况的区域。

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

并发编程的目的就是提高程序的执行效率,但并发编程可能造成:内存泄漏、上下文切换、死锁等问题

关于线程状态

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
  • 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
  • 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
  • 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
  • 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

什么是上下文切换?

简单来说,并发编程中实际线程的数量都可能大于CPU核心的个数,而CPU一个核心在任意时刻只能被一个线程使用,CPU为了保证并发的线程都有被执行,采用随机分配时间片并轮转的方式;而一个线程的时间片用户将保存并进入就绪状态直到下次分配时间片再执行,这个 任务从保存到再加载的过程就是一次上下文切换

说说sleep()方法和wait()方法的区别?

两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁

  • 两者都可以暂停线程的执行
  • wait()通常用于线程间交互/通信,sleep()通常用户暂停执行
  • wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。

什么是死锁?如何避免?

举例:线程A持有资源2,线程B持有资源1,在线程A、B都没有释放自己所持有资源的情况下(锁未释放),他们都想同时获取对方的资源,因为资源1、2都被锁定,两个线程都会进入相互等待的情况,这种情况称为死锁。

栗子:

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();
    }
}

Output:

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

线程1以resource1作为同步监视器,即可以轻松获取resource1同时也锁定了resource1,此时调用sleep让线程1等待1秒钟;此时线程2开始执行,他以resource2作为同步监视器同时也锁定了resource2,此时调用sleep让线程2等待1秒钟;而此时线程1等待1秒已经结束了,当他想要获取resource2时发现resource2已经被线程2锁定了,同理线程2结束等待后想要获取resource1时发现resource1已经被线程1锁定了。那么两者都无法同时获取对方的线程,便进入死锁状态。

因此产生死锁需要具备以下四个条件:

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

避免死锁就要破坏这四个条件中任意一个:

  1. 破坏互斥条件:这个条件我们无法破坏,因为我们用锁的目的就是想让他们互斥
  2. 破坏请求与保持条件:一次性申请所有资源
  3. 破坏循环等待条件:按照一定顺序申请资源,避免资源的循环使用

解决方案: 修改线程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();

Outout:

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
Process finished with exit code 0

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

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

总结:调用start方法可以启动线程并使线程进入就绪状态,而run()方法只是Thread的一个普通方法调用,还是在main主线程里执行,并不会在一个新线程中执行

synchronized关键字

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

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

  • 修饰实例方法: 给当前对象加锁,进入同步代码块前要获取当前对象实例的锁
// 此处的synchronized就相当于synchronized(this),锁定的是当前对象
public synchronized void add() {}
  • 修饰静态方法: 给当前类加锁(因为静态方法没有this),会作用于当前类的所有对象实例,因为静态成员不属于任何一个实例对象,是一个类成员。
// 此处的synchronized就相当于synzhronized(T.class),(T的当前类)
public synchronized static void add() {}
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码块之前要获取给定对象的锁