在 Java 5.0 提供了 java.util.concurrent(简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。
01. 多线程和单线程的区别和联系:
答:
-
在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用 CPU 的机制。
-
多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
**结论:**即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。
02. 如何指定多个线程的执行顺序?
**解析:**面试官会给你举个例子,如何让 10 个线程按照顺序打印 0123456789?(写代码实现)
答:
-
设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。
-
在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值!!不是,则 wait,是则执行本线程。
03. 线程和进程的区别 *
答:
-
进程是一个 “执行中的程序”,是系统进行资源分配和调度的一个独立单位;
-
线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其它资源(所以通信和同步等操作线程比进程更加容易);
-
线程上下文的切换比进程上下文切换要快很多。
-
(1)进程切换时,涉及到当前进程的 CPU 环境的保存和新被调度运行进程的 CPU 环境的设置。
-
(2)线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
04. 多线程产生死锁的 4 个必要条件?
答:
-
互斥条件:一个资源每次只能被一个线程使用;
-
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
-
不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
-
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
面试官:如何避免死锁?(经常接着问这个问题哦~)
答:指定获取锁的顺序,举例如下:
-
比如某个线程只有获得 A 锁和 B 锁才能对某资源进行操作,在多线程条件下,如何避免死锁?
-
获得锁的顺序是一定的,比如规定,只有获得 A 锁的线程才有资格获取 B 锁,按顺序获取锁就可以避免死锁!!!
05. sleep( ) 和 wait( ) 的区别:
答:
-
sleep 方法:是 Thread 类的静态方法,当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行可运行状态,等待 CPU 的到来。睡眠不释放锁(如果有的话);
-
wait 方法:是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。(wait(n)
@param
timeout, the maximum time to wait in milliseconds.)
06. synchronized 关键字:
答:
底层实现:
-
进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1;
-
当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。
含义:(monitor 机制)
Synchronized 是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争 情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。
该关键字是一个几种锁的封装。
07. volatile 关键字
****解析:****关于指令重排序的问题,可以查阅 DCL 双检锁失效相关资料。
答:
该关键字可以保证可见性不保证原子性。
功能:
-
主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
-
禁止 JVM 进行的指令重排序。
什么是内存屏障(Memory Barrier)?内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。(volatile实质:写操作后插入一个写屏障指令)
08. ThreadLocal(线程局部变量)关键字:
答:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其
get
或set
方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal
实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal 内部实现机制:
-
每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
-
Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
-
Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。
09. Atomic 关键字:
**答:**可以使基本数据类型以原子的方式实现自增自减等操作。
10. 线程池有了解吗? *
答:
java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:
当前线程池大小 :表示线程池中实际工作者线程的数量;
最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。
-
如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;
-
如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;
-
如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。