您的位置:首页 > 编程语言 > Java开发

2020Java面试总结(三)

2020-01-11 13:28 344 查看

1.什么是线程和进程?

  1. 进程是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建,运行,消亡的过程。

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

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

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

  1. 图解进程和线程的关系

    ​ 图片来源:https://camo.githubusercontent.com/a66819fd82c6adfa69b368edf3c52b1fa9cdc89d/68747470733a2f2f6d792d626c6f672d746f2d7573652e6f73732d636e2d6265696a696e672e616c6979756e63732e636f6d2f323031392d332f4a564de8bf90e8a18ce697b6e695b0e68daee58cbae59f9f2e706e67

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

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

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

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

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

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

==》线程的上下文切换?

CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。

而在多线程中,程序计数器就是用来记录当前线程执行的位置,所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

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

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

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

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

    1. 所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待)

      实例:线程a处理a业务,线程b处理b业务,线程a处理a业务的同时需要调用b的资源,线程b处理b业务的同时需要调用a的资源,双方都在等待另一个资源释放,从而导致死锁。

    2. 避免死锁

      我们只要破坏产生死锁的四个条件中的其中一个就可以了。

      破坏互斥条件

      这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

      破坏请求与保持条件

      一次性申请所有的资源。

      破坏不剥夺条件

      占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

      破坏循环等待条件

      靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

    4. 说说线程 sleep() 方法和 wait() 方法区别和共同点?

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

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

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

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

    6. synchronized 关键字

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

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

      另外,早期1.6之前的synchronized效率低,属于重量级锁。1.6后对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,所以现在的synchronized效率也优化得很不错。

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

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

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

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

      双重校验锁实现对象单例(线程安全)

      public class Singleton {
      
      private volatile static Singleton uniqueInstance;
      
      private Singleton() {
      }
      
      public static Singleton getUniqueInstance() {
      //先判断对象是否已经实例过,没有实例化过才进入加锁代码
      if (uniqueInstance == null) {
      //类对象加锁
      synchronized (Singleton.class) {
      if (uniqueInstance == null) {
      uniqueInstance = new Singleton();
      }
      }
      }
      return uniqueInstance;
      }
      }

      第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。

      第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

      需要注意的是,private volatile static Singleton uniqueInstance;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

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

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

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

      ①偏向锁

      引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。

      偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。

      但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

      ② 轻量级锁

      倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。

      轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!

      ③ 自旋锁和自适应自旋

      轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

      互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。

      一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。

      ④ 锁消除

      锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。

      ⑤ 锁粗化

      原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。

      大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

    4. 谈谈 synchronized和ReentrantLock 的区别

      ① 两者都是可重入锁

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

      ② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

      ③ ReenTrantLock增加了一些高级功能①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

        ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
      • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
        ReentrantLock(boolean fair)
        构造方法来制定是否是公平的。

      ④ 性能已不是选择标准JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了

    7. volatile关键字

    1. volatile特性

        保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
      • 禁止进行指令重排序。(实现有序性)
      • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
    2. 说说 synchronized 关键字和 volatile 关键字的区别

        volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
      • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
      • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
      • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

    8. ThreadLocal

    通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的

    ThreadLocal
    类正是为了解决这样的问题。
    ThreadLocal
    类主要解决的就是让每个线程绑定自己的值,可以将
    ThreadLocal
    类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

    ThreadLocal底层有一个ThreadLocalMap,可以把他理解为ThreadLocal类实现的定制化的HashMap,最终的数据都是存在ThreadLocalMap中,并不是存在ThreadLocal上。ThreadLocal可以理解为是ThreadLocalMap的封装。

    每个

    Thread
    中都具备一个
    ThreadLocalMap
    ,而
    ThreadLocalMap
    可以存储以
    ThreadLocal
    为key的键值对。

    ThreadLocal 内存泄露问题

    ThreadLocalMap
    中使用的 key 为
    ThreadLocal
    的弱引用,而 value 是强引用。所以,如果
    ThreadLocal
    没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,
    ThreadLocalMap
    中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用
    set()
    get()
    remove()
    方法的时候,会清理掉 key 为 null 的记录。使用完
    ThreadLocal
    方法后 最好手动调用
    remove()
    方法

    9.线程池

    1. 为什么要用线程池?

      池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

      这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处

        降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

      • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

      • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

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

      Runnable
      接口不会返回结果或抛出检查异常,但是**
      Callable
      接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用
      Runnable
      接口
      ,这样代码看起来会更加简洁。

      通过Executors工具类可以实现Runnable和Callable对象之间的转换。

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

      execute():用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否

      submit():用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功。

    4. 如何创建线程池?

      《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

      Executors 返回线程池对象的弊端如下:

        FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
      • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

      方式一:通过构造方法实现

      方式二:通过Executor 框架的工具类Executors来实现

      我们可以创建三种类型的ThreadPoolExecutor:

      • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
      • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
      • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
      import java.util.concurrent.ArrayBlockingQueue;
      import java.util.concurrent.ThreadPoolExecutor;
      import java.util.concurrent.TimeUnit;
      
      public class ThreadPoolExecutorDemo {
      
      private static final int CORE_POOL_SIZE = 5;
      private static final int MAX_POOL_SIZE = 10;
      private static final int QUEUE_CAPACITY = 100;
      private static final Long KEEP_ALIVE_TIME = 1L;
      public static void main(String[] args) {
      
      //使用阿里巴巴推荐的创建线程池的方式
      //通过ThreadPoolExecutor构造函数自定义参数创建
      ThreadPoolExecutor executor = new ThreadPoolExecutor(
      CORE_POOL_SIZE,
      MAX_POOL_SIZE,
      KEEP_ALIVE_TIME,
      TimeUnit.SECONDS,
      new ArrayBlockingQueue<>(QUEUE_CAPACITY),
      new ThreadPoolExecutor.CallerRunsPolicy());
      
      for (int i = 0; i < 10; i++) {
      //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
      Runnable worker = new MyRunnable("" + i);
      //执行Runnable
      executor.execute(worker);
      }
      //终止线程池
      executor.shutdown();
      while (!executor.isTerminated()) {
      }
      System.out.println("Finished all threads");
      }
      }

    引用:

    • 《深入理解 Java 虚拟机》
    • 《实战 Java 高并发程序设计》
    • 《Java并发编程的艺术》
    • http://www.cnblogs.com/waterystone/p/4920797.html
    • https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
    • https://www.journaldev.com/1076/java-threadlocal-example
      System.out.println(“Finished all threads”);
      }
    • 点赞 2
    • 收藏
    • 分享
    • 文章举报
    AiDd124 发布了5 篇原创文章 · 获赞 13 · 访问量 2405 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: