Java 多线程 — 同步机制
📋 本文节选自《Java 多线程面试题》,返回 查看全部章节
16. 常用的几种线程池并讲讲其中的工作原理。什么是线程池?
很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。线程池的好处我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。线程池核心类在java.util.concurrent 包中我们能找到线程池的定义,其中ThreadPoolExecutor 是我们线程池核心类,首先看看线程池类的主要参数有哪些。如何提交线程如可以 先 随 便 定 义 一 个 固 定 大 小 的 线 程池ExecutorService es =Executors.newFixedThreadPool(3);提交一个线程es.submit(xxRunnble);es.execute(xxRunnble);submit 和 execute 分别有什么区别呢?execute 没有返回值,如果不需要知道线程的结果就使用execute 方法,性能会好很多。submit 返回一个 Future 对象,如果想知道线程结果就使用submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。如何关闭线程池es.shutdown();不再接受新的任务,之前提交的任务等执行结束再关闭线程池。es.shutdownNow();不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list 列表。
17. 线程池启动线程 submit()和 execute()方法有什么不同?
execute 没有返回值,如果不需要知道线程的结果就使用execute 方法,性能会好很多。submit 返回一个 Future 对象,如果想知道线程结果就使用submit 提交,而且它能在主线程中通过 Future 的 get 方法捕获线程中的异常。
18. CyclicBarrier 和 CountDownLatch 的区别?
两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:
1、 CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行;
2、 CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务3.CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了;
19. 什么是活锁、饥饿、无锁、死锁?
死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。死锁死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这种情况没有外力干预还是会一直阻塞下去的。活锁活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。饥饿我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如那个占用资源的线程结束了并释放了资源。无锁无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过JDK的CAS 原理及应用即是无锁的实现。可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合下是非常高效的。
20. 什么是原子性、可见性、有序性?
原子性、可见性、有序性是多线程编程中最重要的几个知识点,由于多线程情况复杂,如何让每个线程能看到正确的结果,这是非常重要的。原子性原子性是指一个线程的操作是不能被其他线程打断,同一时间只有一个线程对一个变量进行操作。在多线程情况下,每个线程的执行结果不受其他线程的干扰,比如说多个线程同时对同一个共享成员变量 n++100 次,如果n 初始值为0,n 最后的值应该是 100,所以说它们是互不干扰的,这就是传说的中的原子性。但n++并不是原子性的操作,要使用 AtomicInteger 保证原子性。可见性可见性是指某个线程修改了某一个共享变量的值,而其他线程是否可以看见该共享变量修改后的值。在单线程中肯定不会有这种问题,单线程读到的肯定都是最新的值,而在多线程编程中就不一定了。每个线程都有自己的工作内存,线程先把共享变量的值从主内存读到工作内存,形成一个副本,当计算完后再把副本的值刷回主内存,从读取到最后刷回主内存这是一个过程,当还没刷回主内存的时候这时候对其他线程是不可见的,所以其他线程从主内存读到的值是修改之前的旧值。像CPU 的缓存优化、硬件优化、指令重排及对 JVM 编译器的优化,都会出现可见性的问题。有序性我们都知道程序是按代码顺序执行的,对于单线程来说确实是如此,但在多线程情况下就不是如此了。为了优化程序执行和提高 CPU 的处理性能,JVM和操作系统都会对指令进行重排,也就说前面的代码并不一定都会在后面的代码前面执行,即后面的代码可能会插到前面的代码之前执行,只要不影响当前线程的执行结果。所以,指令重排只会保证当前线程执行结果一致,但指令重排后势必会影响多线程的执行结果。虽然重排序优化了性能,但也是会遵守一些规则的,并不能随便乱排序,只是重排序会影响多线程执行的结果。
21. 什么是守护线程?有什么用?
什么是守护线程?与守护线程相对应的就是用户线程,守护线程就是守护用户线程,当用户线程全部执行完结束之后,守护线程才会跟着结束。也就是守护线程必须伴随着用户线程,如果一个应用内只存在一个守护线程,没有用户线程,守护线程自然会退出。
22. 一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时 候 JVM 会 使 用 Thread.getUncaughtExceptionHandler() 来查询线程 的UncaughtExceptionHandler 并 将 线 程 和 异 常 作 为参数传递给handler 的uncaughtException()方法进行处理。
23. 线程 yield()方法有什么用?
Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定能占用 CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
24. 什么是重入锁?
所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
25. Synchronized 有哪几种用法?
锁类、锁方法、锁代码块。
26. Fork/Join 框架是干什么的?
大任务自动分散小任务,并发执行,合并小任务结果。
27. 线程数过多会造成什么异常?
线程过多会造成栈溢出,也有可能会造成堆异常。
28. 说说线程安全的和不安全的集合。
Java 中平时用的最多的 Map 集合就是 HashMap 了,它是线程不安全的。看下面两个场景:
1、 当用在方法内的局部变量时,局部变量属于当前线程级别的变量,其他线程访问不了,所以这时也不存在线程安全不安全的问题了;
2、 当用在单例对象成员变量的时候呢?这时候多个线程过来访问的就是同一个HashMap了,对同个HashMap操作这时候就存在线程安全的问题了;
29. 什么是 CAS 算法?在多线程中有哪些应用。
CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当然CAS 一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次CAS 操作失败,永远都不可能成功。java.util.concurrent.atomic 包下面的 Atom****类都有 CAS 算法的应用。
30. 怎么检测一个线程是否拥有锁?
java.lang.Thread类的holdsLock 方法