1、进程、线程和程序
- 进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的;在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程
- 线程:线程与进程相似,但线程是一个比进程更小的执行单位;与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈;简单来说就是进程有资源,而线程几乎没有,它只是一个调度的单位,资源使用的是进程的
- 程序:指含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码,一个可执行的文件
2、虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程
- 本地方法栈:和虚拟机栈的作用相似,不同的是为使用到的Native方法服务(Native方法指用其他语言,比如c实现的方法)
- 所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
3、线程的生命周期
- 初始状态:线程被创建出来,但没有被调用
- 运行状态:线程被调用了start()等待运行的状态(这里包括了就绪和运行中两种状态)
- 阻塞状态:需要等待锁被释放
- 等待状态:表示该线程需要等待其他线程做出一些特定动作(通知或中断)
- 超时等待状态:可以在指定的时间后自行返回
- 终止状态:表示该线程已经运行完毕
注意: 这里没有区分就绪和运行两种状态是因为现代操作系统的时间片太小了,以至于区分二者没有太多意义。
4、上下文切换
- 上下文:线程在执行过程中会有自己的运行条件和状态,比如上文所说到过的程序计数器,栈信息等
- 上下文切换:在线程切换时保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文
5、线程死锁
1、线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放
2、四个必要条件:简单来说就是:“你不松手我也不松手”
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
6、预防死锁和死锁避免
1、预防死锁:破坏四个必要条件之一
2、避免死锁:银行家算法
7、sleep()方法和wait()方法
- 共同点:都可以暂停线程的执行
- 不同点:sleep方法没有释放锁,而wait方法释放了锁;sleep方法执行后,线程会自动苏醒,而wait方法需要notify方法唤醒;sleep是Thread类的静态本地方法,而wait是Object类的本地方法
8、可以直接调用Thread类的run方法吗
回答: 调用 start() 方法方可启动线程并使线程进入就绪状态,当分配到时间片后就可以开始自动执行run()方法,但如果直接执行 run() 方法的话不会以多线程的方式执行,而是被当做main线程下的一个普通方法去执行
9、volatile关键字
- volatile关键字可以保证变量的可见性,指示JVM这个变量共享且不稳定,每次使用它都得到主存中进行读取
- 还有一个作用就是防止JVM的指令重排序
- volatile关键字不能保证对变量的操作是原子性的,但synchronized可以
10、乐观锁和悲观锁
1、乐观锁:总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
- 通常用于读比较多的情况
- 版本号机制:每次修改数据都会更新版本号,每次提交更新数据时会比较版本号,只有版本号一致时才更新
- CAS算法:全称Compare And Swap(比较与交换),思想就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新;CAS是一条原子操作,因此当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
2、悲观锁:总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放(例如synchronized的独占锁);通常用于写比较多的情况
11、乐观锁存在的问题
1、ABA问题: 如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。
解决思路: ABA 问题的解决思路是在变量前面追加上版本号或者时间戳
2、循环时间长开销大: CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销
解决思路: 使用处理器提供的pause指令延迟流水线执行指令
3、只能保证一个共享变量的原子操作: CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效
解决思路: 我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来进行CAS操作
12、synchronized关键字
1、synchronized(同步): 主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
2、使用方式:
- 修饰实例方法:锁当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁
- 修饰代码块:对括号里指定的对象/类加锁
synchronized(object/类.class)
:表示进入同步代码库前要获得给定对象或者类的锁 - 注意:构造方法不能用synchronized关键字修饰,因为构造方法本身就属于线程安全的
13、synchronized和volatile有什么区别
- synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
14、公平锁和非公平锁
- 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
- 非公平锁 :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
15、可中断锁和不可中断锁
- 可中断锁 :获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁
- 不可中断锁 :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁
16、ThreadLocal
1、ThreadLocal类: 主要解决的就是让每个线程绑定自己的值
- 可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据
- 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来
- 他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
2、ThreadLocalMap: 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值
- 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对
3、内存泄漏:
- ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用
- 如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉
- 这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露
- ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
17、线程池
1、线程池: 顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务
2、创建方法:
- 方式一:通过ThreadPoolExecutor构造函数来创建(推荐)
- 方式二:通过 Executor 框架的工具类 Executors 来创建
18、Future类
1、Future类: 是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低
2、简单理解就是: 我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future 那里直接取出任务执行结果
19、AQS
1、AQS: 全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器,为构建锁和同步器提供了一些通用功能的实现
2、AQS的核心原理: 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中
- CLH 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)
- 使用 int 成员变量 state 表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作
20、Semaphore
1、synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量
2、Semaphore的原理: 是共享锁的一种实现,它默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行
21、CountDownLatch和CyclicBarrier关键字
1、CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕
2、CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
3、CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大
22、线程的常用方法
- 线程睡眠:sleep方法,休眠一会儿,睡到自然醒
- 线程等待:wait方法,停下别动,等待通知
- 线程让步:yield方法,让出CPU时间片给优先级高的线程
- 线程中断:interrupt方法,修改内部维护的中断标识符,但不会直接打断一个正在运行的线程
- 线程加入:join方法,等待子线程执行结束
- 线程唤醒:notify方法,动起来,开始干活
- 后台守护线程:setDaemon方法,提供公共服务,默默干些杂活,比如垃圾回收线程
参考
- Java并发常见面试题总结
- 《Offer来了:Java面试核心知识点》