多线程面试问题目录
多线程面试问题
1.什么是线程?什么是进程?
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。
2.什么是线程安全,什么是线程不安全?
2.1、线程安全:
指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行程序时出现意外结果。
2.2、线程不安全:
是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
2.3、线程不安全的几要素,或是什么情况下会造成线程不安全?
- 抢占式执行(狼多肉少)
- 多个线程修改同一个变量
- 非原子性操作
- 内存可见性
- 指令重排序
详情可以参照博客:线程安全问题(线程不安全因素)
(在第二个问题中你提到了,锁,脏数据,引导面试官的提问)
3.你为什么会使用多线程、或者说什么情况下使用多线程?
(1) 连续的操作,需要花费忍无可忍的过长时间才可能完成
(2) 并行计算
(3) 为了等待网络、文件系统、用户或其他I/O响应而耗费大量的执行时间
(在第三个问题中你提到了并行)
4.多线程解决的是什么是问题
CPU、内存、I/O设备的速度是有极大的差异的,为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
-
CPU增加了缓存,以均衡与内存的速度差异;//导致可见性问题
-
操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU和I/O设备的速度差异;//导致原子性问题
-
编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。//导致有序性问题
5.什么是并行?什么是并发?(不用死记硬背,理解为主)
5.1并行
概念:
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并行的特点:
对多处理器而言,多个程序在同一时刻发生
5.2、并发
概念:
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
并发的特点
对单处理器而言,多个程序在同一时间段发生
并发编程三要素
1)原子性
原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
2)可见性
可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
3)有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。
5.3并发和并行的区别和联系
计算机操作系统中把并行性和并发性明显区分开,主要是从微观的角度来说的,具体是指进程的并行性(多处理机的情况下,多个进程同时运行)和并发性(单处理机的情况下,多个进程在同一时间间隔运行的)。
1.并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
2.在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。
3.若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
6. 多线程有哪几种创建方式?
1.继承Thread类创建多线程:继承java.lang.Thread类,重写Thread类的run()方法,在run()方法中实现运行在线程上的代码,调用start()方法开启线程。
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法
2.实现Runnable,Runnable规定的方法是run(),无返回值,无法抛出异常
3实现Callable,Callable规定的方法是call(),任务执行后有返回值,可以抛出异常
4通过线程池创建线程. 线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
7sleep()和wait()的区别
1.所属类不同,sleep()属于Thread类wait()属于Object类
2.sleep()不会释放锁,如果设置睡眠时间,到时后会继续执行,而wait(),线程会释放锁。进入等待。
3.sleep()可以在代码中的任意地方使用,而wait()只能用于同步方法或同步代码块中
4.sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
8.synchronized和lock区别 ?
从功能来看:
lock和synchronized都是java中解决线程安全问题的一个工具
从特性来看:
1.synchronized是一个同步关键字,lock是juc包里面提供的一个接口,这个接口他有很多的实现类,其中包括Reentrantlock这个重入锁的实现。
从锁粒度来看:
synchronized可以通过两种方式控制锁的粒度:
1.把synchronized关键字修饰在方法层面
2.修饰在同步代码块上
并且我们可以通过synchronized加锁对象的生命周期,来控制锁的作用范围,比如锁对象是静态对象,或者类对象,那么这个锁就是属于全局锁,如果锁对象是实例对象,那么这个锁的范围取决于这个对象的生命周期。
Lock锁的粒度是通过它里面提供的lock()和unlock()方法决定的:
1.包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。
2.Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()这两个方法就行,同时Lock还提供了非阻塞的竞争锁方法tryLock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。
3.Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。
5.Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized只提供了一种非公平锁的实现。
性能方面
synchronized在性能方面和lock相差不大,在实现上会有一个区别synchronized引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock则用到了自旋锁的方式实现性能优化。
(提到了偏向锁,轻量级锁,重量级锁,以及锁的升级机制,自旋锁等)
9.Synchronized锁升级的原理
Java 1.6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而降低使用锁带来的性能消耗。
9.1锁的分级
9.1.1锁的分级
1、无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
2、偏向锁
一个程序一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。 偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。
3、轻量级锁
轻量级锁是指当锁是偏向锁的时候,被第二个线程B所访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 当前只有一个等待线程,则该线称将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
4、重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁通过对象内部的监视器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
二、锁分级别原因
没有优化以前,sychronized是重量级锁(悲观锁),使用wait 和notify、notifyAll 来切换线程状态,这样上下文切换(线程的挂起和唤醒)非常消耗系统资源,影响性能。所以JVM对sychronized关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁。
三、锁的升级的目的
为了减低锁带来的性能消耗。在Java6之后优化synchronized,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
四、Synchronized锁升级的原理
在锁对象的对象头里面有一个threadId字段,在第一次访问的时候 threadId为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断 threadId是否与其线程id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级。
10.volatile关键字有什么用?
volatile关键字有两个作用:
1.可以保证在多线程环境下共享变量的可见性。
2.通过增加内存屏障防止多个指令之间的重排序。
我理解的可见性,是指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值。
可见性问题,我认为本质上是CPU层面的高速缓存造成的。对于增加了volatile关键字修饰的共享变量,JVM虚拟机会自动增加一个#Lock汇编指令,这个指令会根据CPU型号自动给CPU加锁,禁用CPU的缓存让数据直接存到硬盘中。
所谓重排序,就是指令的编写顺序和执行顺序不一致,在多线程环境下导致可见性问题。指令重排序本质上是一种性能优化的手段。如果对共享变量增加了volatile关键字,那么在编译器层面,就不会去触发编译器优化,同时再JVM里面,会插入内存屏障指令来避免重排序问题。
11.synchronized和volatile的区别
1.volatile比synchronized更轻量级,volatile是变量修饰符,而synchronized则作用于一段代码或方法。
2.volatile不需要加锁,不会造成线程的阻塞;synchronized需要加锁,可能会造成线程的阻塞。
3.volatile只能保证可见性,不能保证原子性,而synchronized既可保证可见性,又能保证原子性
4.volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
(原因是:JVM对于64位的数据类型(long/double)有非原子性协议!!long和double占用的字节数都是8,也就是64bits。在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据。这样对double和long的赋值操作就会有问题:如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。因此需要使用volatile关键字来防止此类现象。volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。)