1. 为什么要使用多线程?
在实际开发中,有时候需要处理大规模的计算任务时,可以将任务划分成多个子任务,并使用多线程并行执行。这样可以充分利用多核处理器的计算能力,提高计算性能和效率。
异步操作,当需要执行耗时的操作或等待外部事件完成时,可以使用多线程实现异步操作。通过将耗时的操作放在后台线程中执行,可以避免阻塞主线程,保持程序的响应性。
并发访问共享资源:当多个线程需要同时访问和修改共享的数据结构、变量、文件或数据库等资源时,需要使用多线程来确保数据的一致性和正确性。
-
提高系统并发性能: 充分利用多核 CPU,实现并行计算,提高系统的整体性能。在单核时代,通过多线程可以让单个进程更有效地利用 CPU 时间片,提高运算效率。
-
提高程序响应性: 在图形用户界面(GUI)应用中,通过使用多线程可以避免用户界面的卡顿,使用户体验更流畅。后台执行的任务可以在独立的线程中运行,不会阻塞主线程。
-
简化程序逻辑: 多线程可以将复杂、耗时的任务分解成多个线程并发执行,使程序结构更清晰,代码更简洁。例如,在网络编程中,可以使用一个线程处理用户请求,另一个线程处理日志记录等任务。
-
提高资源利用率: 多线程可以充分利用系统资源,同时处理多个任务,减少空闲时间,提高资源利用率。例如,在服务器端处理多个客户端请求时,使用多线程可以同时处理多个请求。
-
支持异步编程: 多线程可以用于实现异步编程模型,通过在一个线程中执行异步任务,不阻塞主线程,提高系统的响应性。这在处理网络请求、数据库查询等场景中很常见。
多线程是一种有效的编程手段,可以提高程序的性能、响应性,简化程序逻辑,并适应当前多核时代的计算机体系结构。然而,使用多线程也需要注意线程安全、死锁等问题,合理设计和管理多线程是编程中需要注意的重要方面。
面试题1:进程和线程的区别?为什么要有线程,而不是仅仅是用进程?
- 从概念上说,进程是系统中正在运行的应用程序,而线程是应用程序中的不同执行路径
- 从目的上说,进程和线程都是为了解决CPU、内存、IO设备之间读取速度相差过大的问题,不过线程的切换比进程更轻量
- 从开发上说,进程之间不能共享资源,而线程之间是可以共享同一进程的资源
面试题1:进程和线程的区别?为什么要有线程,而不是仅仅是用进程?
- 从概念上说,进程是系统中正在运行的应用程序,而线程是应用程序中的不同执行路径
- 从目的上说,进程和线程都是为了解决CPU、内存、IO设备之间读取速度相差过大的问题,不过线程的切换比进程更轻量
- 从开发上说,进程之间不能共享资源,而线程之间是可以共享同一进程的资源
多线程编程为什么容易出问题
在多线程编程中,我们遇到的问题都可以归纳到多线程的可见性、原子性、有序性上去。三个特性介绍如下。需要注意想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
- 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
为什么可见性有问题
为了解决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.
为什么有序性有问题
ini
复制代码
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内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
功能二:保证代码的原子性,可见性,有序性。JMM定义了volatile、synchronized 和 final 三个关键字,以及八项Happens-Before 规则的规范来解决可见性、原子性、有序性的问题。需要注意JMM只是定义规范,具体实现是由JVM完成的。
八项happens-before原则:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
可见性、原子性、有序性的问题的解决方式
- 解决可见性问题
如上图所示,java的 volatile、final、synchronized 关键字都可以实现可见性。
- 被 volatile 修饰的变量,它的值被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。通过这种方式保证可见性。
- synchronized 包裹的代码块或者修饰的方法,在执行完之前,会将共享变量同步到主内存中,从而保证可见性。
- 被final修饰的字段,初始化完成后对于其他线程都是可见的。需要注意的是,如果final修饰的是引用变量,对它属性的修改是不可见的。
- 解决有序性问题
- volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。
- 如果你了解过DCL单例模式,应该知道synchronized内部的代码是会指令重排序的。那为什么说synchronized能保证有序性呢?因为synchronized保证的有序性是指它修饰的方法或者代码块内部的代码,经过重排序不会在锁外,而不是确保synchronized内部的有序性
-
解决原子性问题
synchronized包裹的代码块或者修饰的方法保证了只有一个线程执行,确保了代码块或者方法的原子性。
内存屏障是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
面试题3:sychronied修饰普通方法和静态方法的区别?
使用synchronied修饰普通方法,等价于synchronized(this){},是给当前类的对象加锁;使用synchronied静态方法,等价于synchronized(class){},是给当前类对象加锁。需要注意,synchronized不可以修饰类的构造方法,但是可以在构造函数里面使用synchronied代码块。
面试题4:构造函数为什么不需要synchronized修饰方法?构造函数是线程安全的吗?
在java中,我们是通过new关键字来获取对象。如果多线程执行new,每个线程都会获取一个对象,因此构造函数不需要synchronized来修饰。
但是构造函数是线程安全的吗?答案是不安全的。原因有两个:
- 构造函数内部会指令重排序,比如构造函数内部的变量经过指令重排序,其位置可能在构造函数之外。
- 创建对象的指令不是原子性的,可能因为指令重排序造成各种问题
面试题5:volatile关键字做了什么?
volatile关键字保证内存可见性和禁止了指令重排。
volatile修饰的变量,它的值被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值
volatile修饰的变量禁止了指令重排序。volatile修饰的变量,在读写操作指令前后会插入内存屏障,这样指令重排序时就不会把后面的指令重排序到内存屏障前
面试题6:DCL中单例成员为什么需要加上volatile关键字
java
复制代码
public class SingletonClass {
private volatile static SingletonClass instance = null;
private SingletonClass() {
}
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
}
这是因为创建对象的指令不是原子性的,有三步
- 分配内存
- 初始化对象
- 将内存地址赋值给引用
如果发生了指令重排可能会导致第二步内容和第三步内容顺序发生变化,即还没初始化的对象已经赋值给引用。此时另一个线程会获取还没有初始化的对象,这时对对象的操作可能会造成各种问题。
作者:小墙程序员
链接:juejin.cn/post/734911…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。### 面试题5:volatile关键字做了什么?
volatile关键字保证内存可见性和禁止了指令重排。
volatile修饰的变量,它的值被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值
volatile修饰的变量禁止了指令重排序。volatile修饰的变量,在读写操作指令前后会插入内存屏障,这样指令重排序时就不会把后面的指令重排序到内存屏障前
面试题6:DCL中单例成员为什么需要加上volatile关键字
java
复制代码
public class SingletonClass {
private volatile static SingletonClass instance = null;
private SingletonClass() {
}
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
}
这是因为创建对象的指令不是原子性的,有三步
- 分配内存
- 初始化对象
- 将内存地址赋值给引用
如果发生了指令重排可能会导致第二步内容和第三步内容顺序发生变化,即还没初始化的对象已经赋值给引用。此时另一个线程会获取还没有初始化的对象,这时对对象的操作可能会造成各种问题。
面试题7:volatile和synchronize有什么区别?
- volatile 只能作用于变量,synchronized 可以作用于变量、方法。
- volatile 只保证了可见性和有序性,无法保证原子性,synchronized 可以保证有序性、原子性和可见性。
- volatile 不阻塞线程,synchronized 会阻塞线程
面试题8:为什么局部变量是线程安全的
如上图所示,局部变量都是放到了java调用栈里,而每个线程都有自己独立的调用栈。
面试题9:通过继承 Thread 的方法和实现 Runnable 接口的方式创建多线程,哪个好?
实现 Runable 接口好,原因有两个:
- ♠①、避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
- ♠②、适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。
面试题10:## 控制线程的其他方法
- sleep():使当前正在执行的线程暂停指定的毫秒数,也就是进入休眠的状态。需要注意的是,sleep 的时候要对异常进行处理。
- join():主线程等待这个线程执行完才会轮到后续线程得到 cpu 的执行权,使用这个也要捕获异常。
- setDaemon():将此线程标记为守护线程
- yield():方法是一个静态方法,用于暗示当前线程愿意放弃其当前的时间片,允许其他线程执行。然而,它只是向线程调度器提出建议,调度器可能会忽略这个建议