在Android开发的面试中,Java多线程的问题是绕不开的。这个系列主要介绍面试过程中涉及到的多线程的知识点,以及相关的面试题。这是本系列的第一篇,介绍多线程的意图、多线程编程会出现问题的原因以及处理方式。

为什么需求多线程

咱们都知道CPU缓存、内存、IO设备之间读取速度相差非常大。如下图所示,程序的性能受限于IO设备,无论怎样提高CPU、内存的速度都没有用。。为了处理这个问题,操作体系就提出了多进程、多线程的机制,经过分配时刻片的方式来提高CPU的利用率。

Java多线程面试系列——为什么需求多线程

上面图片来历每个程序员都应该知道的推迟数字,是2020年的耗时数据。假如需求看最新的数据,能够看伯克利大学制作的网页

面试题1:进程和线程的差异?为什么要有线程,而不是仅仅是用进程?

  • 从概念上说,进程是体系中正在运行的应用程序,而线程是应用程序中的不同履行途径
  • 从意图上说,进程和线程都是为了处理CPU、内存、IO设备之间读取速度相差过大的问题,不过线程的切换比进程更轻量
  • 从开发上说,进程之间不能同享资源,而线程之间是能够同享同一进程的资源

面试题2:假如只需一个cpu,单核,多线程还有用吗 ?

操作体系运用多线程机制是为了处理CPU、内存、IO设备之间读取速度相差过大的问题。就算是只需一个CPU、单核,在处理IO操作时,也能够采用多线程机制来提高CPU的利用率。

多线程编程为什么简单出问题

在多线程编程中,咱们遇到的问题都能够归纳到多线程的可见性、原子性、有序性上去。三个特性介绍如下。需求留意想并发程序正确地履行,必需求确保原子性、可见性以及有序性。只需有一个没有被确保,就有可能会导致程序运行不正确

  • 可见性:当多个线程拜访同一个变量时,一个线程修正了这个变量的值,其他线程能够当即看得到修正的值。
  • 原子性:即一个操作或许多个操作,要么悉数履行,而且履行的过程不会被任何因素打断,要么就都不履行。
  • 有序性:程序履行的顺序依照代码的先后顺序履行。

为什么可见性有问题

Java多线程面试系列——为什么需求多线程

为了处理CPU、内存、IO设备之间读取速度相差过大的问题,除了操作体系的多进程、多线程机制外,CPU还增加了缓存,以均衡与内存的速度差异。

在单核年代,每个线程都共有一个缓存,因而不同的线程对变量的操作有可见性。可是在多核年代(如上图所示),每个 CPU 都有自己的缓存(L1和L2),当多个线程在不同的 CPU 上履行时,这些线程操作的是不同的 CPU 缓存,因而不同的线程对变量的操作就不具有可见性了。

为什么原子性有问题

多线程原子性的问题有两个原因。其一是大部分程序代码的履行不是原子性的。比如num++(num为0)这条代码需求三条CPU指令:过程1:把变量 num 从内存加载到 CPU 的寄存器;过程2:在寄存器中履行 +1 操作;过程3:将成果写入内存。其二是线程的切换,当线程1履行到过程1时,这时线程1时刻片用完了;假如此刻还有线程2,它也履行了过程1;这时两个线程履行的成果为 1,而不是2.

为什么有序性有问题

int i = 0;
boolean flag = false;
i = 1;                //句子1
flag = true;          //句子2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,句子1是在句子2前面的,那么JVM在真实履行这段代码的时分会确保句子1一定会在句子2前面履行吗?不一定,为什么呢?这里可能会发生指令重排序。指令重排序是指编译器为了优化性能,它有时分会改变程序中句子的先后顺序。需求留意指令重排序不会影响单个线程的履行,可是会影响到线程并发履行的正确性

怎么处理可见性、原子性、有序性的问题

在Java中,经过定义了JMM(Java内存模型)来处理这个问题。JMM主要有两个作用:

功能一:使java程序在各种平台下都能到达一致的并发作用。由于在不同的硬件生产商和不同的操作体系下,内存的拜访有一定的差异,所以会形成相同的代码运行在不同的体系上会出现各种问题。运用java内存模型(JMM)屏蔽掉各种硬件和操作体系的内存拜访差异,让java程序在各种平台下都能到达一致的并发作用。

详细模型如下图,Java内存模型规矩一切的变量都存储在主内存中,包括实例变量,静态变量,可是不包括局部变量和办法参数。每个线程都有自己的作业内存,线程的作业内存保存了该线程用到的变量和主内存的副本复制,线程对变量的操作都在作业内存中进行线程不能直接读写主内存中的变量。不同的线程之间也无法拜访对方作业内存中的变量。线程之间变量值的传递均需求经过主内存来完结。

Java多线程面试系列——为什么需求多线程

功能二:确保代码的原子性,可见性,有序性。JMM定义了volatile、synchronized 和 final 三个关键字,以及八项Happens-Before 规矩的标准来处理可见性、原子性、有序性的问题。需求留意JMM仅仅定义标准,详细实现是由JVM完结的。

八项happens-before原则:

  1. 程序次第规矩:一个线程内,依照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规矩:一个unLock操作先行发生于后面临同一个锁的lock操作
  3. volatile变量规矩:对一个变量的写操作先行发生于后面临这个变量的读操作
  4. 传递规矩:假如操作A先行发生于操作B,而操作B又先行发生于操作C,则能够得出操作A先行发生于操作C
  5. 线程发动规矩:Thread目标的start()办法先行发生于此线程的每个一个动作
  6. 线程中止规矩:对线程interrupt()办法的调用先行发生于被中止线程的代码检测到中止事情的发生
  7. 线程完结规矩:线程中一切的操作都先行发生于线程的终止检测,咱们能够经过Thread.join()办法结束、Thread.isAlive()的返回值手法检测到线程现已终止履行
  8. 目标完结规矩:一个目标的初始化完结先行发生于他的finalize()办法的开始

可见性、原子性、有序性的问题的处理方式

Java多线程面试系列——为什么需求多线程

  • 处理可见性问题

如上图所示,java的 volatile、final、synchronized 关键字都能够实现可见性。

  1. 被 volatile 润饰的变量,它的值被修正后会马上刷新到主内存,当其它线程需求读取该变量时,会去主内存中读取新值。经过这种方式确保可见性。
  2. synchronized 包裹的代码块或许润饰的办法,在履行完之前,会将同享变量同步到主内存中,从而确保可见性。
  3. 被final润饰的字段,初始化完结后对于其他线程都是可见的。需求留意的是,假如final润饰的是引证变量,对它属性的修正是不行见的。
  • 处理有序性问题
  1. volatile关键字是运用内存屏障到达制止指令重排序,以确保有序性。
  2. 假如你了解过DCL单例模式,应该知道synchronized内部的代码是会指令重排序的。那为什么说synchronized能确保有序性呢?由于synchronized确保的有序性是指它润饰的办法或许代码块内部的代码,经过重排序不会在锁外,而不是确保synchronized内部的有序性
  • 处理原子性问题

    synchronized包裹的代码块或许润饰的办法确保了只需一个线程履行,确保了代码块或许办法的原子性。

内存屏障是一类同步屏障指令,是CPU或编译器在对内存随机拜访的操作中的一个同步点,使得此点之前的一切读写操作都履行后才能够开始履行此点之后的操作。

面试题3:sychronied润饰普通办法和静态办法的差异?

运用synchronied润饰普通办法,等价于synchronized(this){},是给当时类的目标加锁;运用synchronied静态办法,等价于synchronized(class){},是给当时类目标加锁。需求留意,synchronized不能够润饰类的结构办法,可是能够在结构函数里边运用synchronied代码块。

面试题4:结构函数为什么不需求synchronized润饰办法?结构函数是线程安全的吗?

在java中,咱们是经过new关键字来获取目标。假如多线程履行new,每个线程都会获取一个目标,因而结构函数不需求synchronized来润饰。

可是结构函数是线程安全的吗?答案是不安全的。原因有两个:

  1. 结构函数内部会指令重排序,比如结构函数内部的变量经过指令重排序,其位置可能在结构函数之外。
  2. 创建目标的指令不是原子性的,可能由于指令重排序形成各种问题

面试题5:volatile关键字做了什么?

volatile关键字确保内存可见性和制止了指令重排。

volatile润饰的变量,它的值被修正后会马上刷新到主内存,当其它线程需求读取该变量时,会去主内存中读取新值

volatile润饰的变量制止了指令重排序。volatile润饰的变量,在读写操作指令前后会插入内存屏障,这样指令重排序时就不会把后面的指令重排序到内存屏障前

面试题6:DCL中单例成员为什么需求加上volatile关键字

publicclassSingletonClass{
    privatevolatilestaticSingletonClassinstance=null;
    privateSingletonClass() {
    }
    publicstaticSingletonClassgetInstance() {
        if(instance==null) {
             synchronized(SingletonClass.class) {
                   if(instance==null) {
                     instance=newSingletonClass();
                  }
            }
        }
        returninstance;
    }
}

这是由于创建目标的指令不是原子性的,有三步

  1. 分配内存
  2. 初始化目标
  3. 将内存地址赋值给引证

假如发生了指令重排可能会导致第二步内容和第三步内容顺序发生变化,即还没初始化的目标现已赋值给引证。此刻另一个线程会获取还没有初始化的目标,这时对目标的操作可能会形成各种问题。

面试题7:volatile和synchronize有什么差异?

  • volatile 只能作用于变量,synchronized 能够作用于变量、办法。
  • volatile 只确保了可见性和有序性,无法确保原子性,synchronized 能够确保有序性、原子性和可见性。
  • volatile 不堵塞线程,synchronized 会堵塞线程

面试题8:为什么局部变量是线程安全的

Java多线程面试系列——为什么需求多线程

如上图所示,局部变量都是放到了java调用栈里,而每个线程都有自己独立的调用栈。

参考