[从面试题看问题]线程篇(二)

来源:互联网 发布:简单数控车床编程100例 编辑:程序博客网 时间:2024/06/04 19:02

【面试题5】举例说明同步和异步。

同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求
不到,怎么办,A线程只能等待下去。

异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程
仍然请求的到,A线程无需等待。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

【面试题6】什么是线程池(thread pool)?

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。

在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。

所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。

线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Java 5中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

【面试题7】线程的基本状态以及状态之间的关系?

新建(new)
  新建一个线程的对象。

可运行(runable)
  线程对象创建后,其他线程调用该线程的start方法。或者该线程位于可运行线程池中等待被线程调用,已获取cpu的使用权。

运行(running)
  可运行的线程获取了cpu的使用权,执行程序代码/

阻塞(block)
  由于某些原因该线程放弃了cpu的使用权。停止执行。除非线程进入可运行的状态,才会有机会获取cpu的使用权。

  1. 等待阻塞:运行中的线程执行wait方法,这时候该线程会被放入等待队列。

  2. 同步阻塞:运行中的线程获取同步锁,如果该同步锁被别的线程占用,这个线程会成被放入锁池,等待其他线程释放同步锁。

  3. 其他阻塞:运行的线程执行sleep或者join方法这个线程会成为阻塞状态。当sleep超时,join等待线程终止,该线程会进入可运行状态。

死亡(dead)
  线程run mian 执行完毕后,或者因为某些异常产生退出了 run 方法,该线程的生命周期结束。

image

【面试题8】什么是死锁(deadlock)?

两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。

image

下面的代码可能导致死锁

image

若同时开启两个线程,可能会出现,线程1需要锁B单锁A还没有释放,线程2需要锁A但锁B还没有释放的情况,就会引起死锁。

延伸题8.1 如何确保N个线程可以访问N个资源同时又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

【面试题9】进程和线程的区别是什么?

image

image

【面试题10】为什么要使用volatile变量?

看了上面的问题,可能对volatile关键字的理解还不是很深刻,下面用图解来展示普通变量,volatile修饰,synchronize修饰的区别。

首先要理解以下的过程:

1.多线程操作普通变量的过程:

在jvm中,有一个主内存,主内存是多线程共享的。

每个线程有一个自己的工作线程,多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。

当new一个对象的时候,其被分配在主内存中,某个线程想要操作它,不能获取,而是从主内存获取该对象的副本

下面是某个线程操作对象时的执行流程:

  1. 从主存复制变量到当前工作内存
  2. 执行代码,改变共享变量值
  3. 用工作内存数据刷新主存相关内容

2.使用volatile关键字修饰的变量

image

可见,它保证了各个线程读取到的变量都是最新的,当然仅仅保证了同步,同步包括将读取变量和更新变量的值,但如果这个过程是多个操作组成,包括读取,操作,更新,而在操作时,你能然保证不了操作的是最新的值。所以volatile在执行非原子操作时可能会出现问题。

举例来说,++a就是一个非原子操作,其可以拆分成 获取a当前的值,执行对a进行+1操作,将操作后的值赋值给a。

可见,计算后的新值和原来自身的值有关的话它就是一个非原子操作,而例如 x=m+1这样和原来自身的值无关,就是一个原子操作。

下面举几个例子:

普通变量执行非原子操作:

image

被volatile修饰变量的非原子操作:

image

这样看来两种操作的区别很明显:普通变量要进过读取,操作,写入等步骤,而使用volatile关键字实际缩小了这一过程,直接在操作中进行获取值,更新值。但是如果操作是非原子性的,在获取旧值时还有可能出现问题。

可见volatile并不能保证操作变量的原子性。

但是我们在某些情况下需要可见性,即获取变量的最新值,比如说标识符变量。

普通变量执行原子操作:

image

可见,我们本来的用意可能是线程1和线程2同时执行后,flag的值仍未true,而操作执行1次,但是结果并不是这样。

被volatile修饰变量的原子操作:

image

除此之外,volatile还可以用在另一种情况,双重判断,比如如下操作:

自己体会一下。

image

延伸题10.1 Java 中能创建 Volatile 数组吗?

能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。

我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

延伸题10.2 volatile 能使得一个非原子操作变成原子操作吗?

一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。

为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。

但是对一个 volatile 型的 long 或 double 变量的读写是原子。

延伸题10.3 volatile 修饰符的有过什么实践?

一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。

double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。

volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。

意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。

延伸题10.4 volatile 类型变量提供什么保证?

volatile 变量提供顺序和可见性保证

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  
  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。

【面试题11】 个线程和 2 个线程的同步代码,哪个更容易写?

从写代码的角度来说,两者的复杂度是相同的,因为同步代码与线程数量是相互独立的。

但是同步策略的选择依赖于线程的数量,因为越多的线程意味着更大的竞争,所以你需要利用同步技术,如锁分离,这要求更复杂的代码和专业知识。

【面试题12】什么是多线程环境下的伪共享(false sharing)?

伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行,如下图所示:

image

原创粉丝点击