死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现 了这三种情况,即线程不再活跃,不能再正常地执行下去了。
本文分享给需要面试刷题的朋友,我特意整理了一下,里面的技术不是靠几句话就能讲清楚,多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。性能调优、Spring,MyBatis,Netty源码,数据结构,jvm,多线程等等,由于篇幅有限,以下只展示小部分面试题,有需要完整版的朋友可以点一点链接跳转领取,链接:戳这里免费下载,获取码:掘金
死锁
死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等 对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。
举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占 用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而 又得不到解决,
老师知道此事后就让他们相互还给对方,这样在外力的干预下他们 才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这 种情况没有外力干预还是会一直阻塞下去的。
活锁
活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。
活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿 到资源却又相互释放不执行。
当多线程中出现了相互谦让,都主动将资源释放给别 的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
饥饿
我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执 行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无 法得到执行,这就是饥饿。
当然还有一种饥饿的情况,一个线程一直占着一个资源 不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够 得到执行的,如那个占用资源的线程结束了并释放了资源。
无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时 只有一个线程能修改成功。
无锁典型的特点就是一个修改操作在一个循环内进行, 线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下 一次循环尝试。
所以,如果有多个线程修改同一个值必定会有一个线程能修改成功, 而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过 JDK 的 CAS 原理及应用即是无锁的实现。
可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使 用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合 下是非常高效的。
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地 址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一 个进程中的不同执行路径。
线程有自己的堆栈和局部变量,但线程之间没有单独的 地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
(1)继承 Thread 类实现多线程
(2)实现 Runnable 接口方式实现多线程
(3)使用 ExecutorService、Callable、Future 实现有返回结果的多线程
(4)通过线程池创建线程
只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的,必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run() 方法里面的代码。
stop 终止,不推荐。
NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。
RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中 状态。
BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。
WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如 通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方 法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线 程就进入了 RUNNABLE 状态继续运行。
TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒 后线程重新进行 RUNNABLE 状态继续运行。
TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到 RUNNABLE 状态 。
这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监视器,wait 方法会放弃这个对象的监视器。
Synchronized 关键字,Lock 锁实现,分布式锁等。
单核 CPU 上所谓的"多线程"那是 假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快, 看着像多个线程"同时"运行罢了。
多核 CPU 上的多线程才是真正的多线程,它能 让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU 的优势来,达到充 分利用CPU 的目的。
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因 为在单核CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但 是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使 用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返 回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。
多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻 塞,也不会影响其它任务的执行。
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么 就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成 几个小任务,任务B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运 行这几个任务,那就简单很多了。
wait/notify
实现Callable 接口。
一个非常重要的问题,是每个学习、应用多线程的 Java 程序员都必须掌握的。理 解 volatile关键字的作用的前提是要理解 Java 内存模型,这里就不讲 Java 内存模型 了,可以参见第31 点,volatile 关键字的作用主要有两个:
使用 volatile 则会对禁止语义重排 序,当然这也一定程度上降低了代码执行效率从实践角度而言,volatile 的一个重 要 作 用 就 是 和 CAS 结 合。
用 join 方法。
用 Semaphore。
我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方 式来创建并运行一个线程,线程少的话这不会是问题。
而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和 内存资源,也会造成 GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消 耗系统资源的。
如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以, 线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也 会自动销毁,而不会长驻内存。