线程的基础知识
聊一下并行和并发有什么区别?
- 并行:同时执行多个任务的能力
- 并发:同一时间应对多件事情的能力。假定现在只有一个处理器,但是有多个任务需要处理,在同一时刻一个处理器只能处理一个任务,如何设计一个好的调度算法让处理器去处理这些任务是并发问题的关键。
说一下进程和线程的区别?
进程:
- 进程是程序执行时的一个实例,每个进程都有自己独立的内存空间,因此进程是系统中资源分配的最小单位。
- 而一个进程中包含多个线程。而多个线程共享这一个进程的内存空间。如何保证线程之间资源分配、数据同步是多线程中重要的问题。因此我们也称线程是系统中资源调度的最小单位。
如果在java中创建线程有哪些方式?
- 继承Thread类
- 实现runable接口
- 实现Callable接口
- 线程池创建线程
runnable 和 callable 两个接口创建线程有什么不同呢?
- runable接口的run方法无返回值,而callable接口的call方法有返回值
- 异常处理方式不同,run方法不能抛出已检查异常,而call方法可以抛出任何异常
线程包括哪些状态,状态之间是如何变化的?
- 线程主要包含新建、可运行、阻塞、等待、有限时间等待、终结
- 当一个线程被创建时,未调用start方法时其处于新建状态。
- 当调用start方法后处于可运行状态(就绪),如果线程执行完毕其处于终结状态
- 当调用start方法后,获取锁失败其处于阻塞状态,进入阻塞队列等待
- 如果线程获取锁成功后,由于条件不足调用了wait()方法,这时会进入等待状态
- 还有一种情况是调用sleep(long)方法让其进入有限时间等待状态
线程中的 wait 和 sleep方法有什么不同呢?
- 这两个方法都可以让当前线程放弃cpu使用权,进入阻塞状态
- wait()方法属于object类,可以在任何对象上调用。sleep()方法属于Thread类,只能在线程上调用
- 在调用wait()方法后,线程会释放对象的锁进入等待状态。通过nodify方法对其进行唤醒。而调用sleep()方法,线程不会释放锁,只是暂停执行一段时间
现在举一个场景,新建 T1、T2、 T3 三个线程,如何保证它们按顺序执行?
- 可以使用线程的join()方法来解决。通过使用join方法让T3调用T2,让T2调用T1就可以保证执行顺序为T1、T2、T3
run方法和start方法有什么区别?
- start()方法用于启动线程并执行其任务。当调用satrt()方法时,会启动一个新的线程并调用线程的run()方法来执行任务
- 而run()方法定义了线程要执行的任务逻辑,所以直接调用run方法后只会在当前线程中执行任务,不会创建新的线程。
如何停止一个正在运行的线程呢?
- 使用退出标志
- 使用线程的stop方法
- 使用interrupt(中断)方法
线程中并发锁
讲一下synchronized关键字的底层原理?(6之前)
- 底层是用JVM级别的监视器(Monitor)来决定当前线程是否获得了锁。如果某一个线程释放了锁在没有释放之前其他线程不能得到锁。
- 属于悲观锁,性能较低
具体说下Monitor?
在Java当中,每个对象都通过对头象(存储的有锁相关信息)与一个监视器相关联
监视器中包含三个变量:
- WaitSet:保存处于等待中的线程
- EntryList:保存处于阻塞中的线程
- Owner:持有锁的线程
执行流程如下:
当线程执行同步代码时,首先会尝试获得该对象关联的监视器。如果该监视器未被其他线程持有,那么该线程就会加入获取监视器并继续执行同步代码块。如果获取失败就阻塞该线程将其放入EntryList。直到其他持有线程释放了锁,就会唤醒等待的线程,让它们去竞争这个对象锁。
synchronized 的锁升级的情况了解吗?
在Java6之前,synchronized锁是通过monitor来实现的,里面涉及到用户态和内核态切换,这会导致效率较低。在1.6之后引入了偏向锁和轻量级锁,用来提高在没有竞争的场景下使用重量级锁的效率
偏向锁:
- 主要是针对单线程场景的一种优化。当一个线程第一次进入同步代码块,偏向锁会记录下当前线程的ID,并设置偏向锁的标识,标识该线程持有偏向锁。如果其他线程尝试获取锁,将对偏向锁进行升级。
具体加锁实现步骤:
- 当某线程第一次进入同步代码块时,在线程创建锁记录并将obj字段指向锁对象。通过CAS操作将线程ID存储在对象头的markword当中,同时设置偏向锁的标识101。如果设置成功则标识该线程持有了偏向锁。
- 如果当前线程持有偏向锁,由于对象头中含有当前线程的id,只需要将锁记录设置为null,代表这是一个锁重入即可。
轻量级锁:
- 如果多个线程使用锁的时间是分开的,可以采用轻量级锁。如果多线程尝试获取同一个锁,轻量级锁会首先使用CAS操作将锁对象标志从无锁状态改为轻量级锁的状态,如果成功则获得了锁,如果失败则表示存在竞争关系,需要将锁膨胀为重量级锁。
具体加锁实现步骤:
- 当某线程第一次进入同步代码块时,在线程创建锁记录并将obj字段指向锁对象。通过CAS操作将锁记录的地址存储在对象头的markword中,如果处于无锁状态,则代表该线程获得了轻量级锁
- 如果CAS修改失败,则说明发生了竞争,需膨胀微重量级锁。
synchronized它在高并发量的情况下,性能不 高,在项目该如何控制使用锁呢?
- 在高并发下,我们可以采用ReentrantLock来加锁。
介绍一下ReentrantLock的使用方式和底层原理?
- 表示可重入的锁;通过lock方法来获取锁unlock方法来释放锁。此外它允许同一个线程多次获得同一把锁。
- 其底层是通过CAS和AQS实现的,支持公平锁和非公平锁(通过设置公平参数进行设置)
介绍一下CAS和AQS?
CAS:
- 全称为比较再交换,是一种原子操作。主要用来解决并发环境下的原子性问题
- 其基本思想为:当需要更新共享变量时,先比较当前值与期望值是否相等,如果相等则使用新值代替当前值,否则不进行操作(轻量级锁)
AQS:
- 使用了一个基于FIFO的等待队列来管理等待锁的线程。该队列是一个双向链表,tail指向最后一个元素,head指向等待最久的一个元素。此外通过state属性来表示资源的状态。
synchronized和Lock有什么区别 ?
- synchronized 是一个关键字,退出同步代码后锁会自动释放,而lock是一个接口,调用unlock方法来释放锁
- 在没有竞争时synchronized做了很多优化效率较高,在竞争激烈时Lock通常会有更好的性能
- Lock可以实现公平锁、读写锁、定时锁。而synchronized不能
什么是死锁?
- 两个或两个以上的线程互相持有对方想要的资源而陷入永久等待的状态
那如果产出了这样的,如何进行死锁诊断?
- 可以通过jdk自带的工具搞定,实现通过jps来查看正在运行的进程id
- 通过jstack来查看这个进程id即可定位具体代码
请谈谈你对 volatile 的理解?
- 它是一个关键字,用来修饰类的成员变量和类的静态成员变量,主要有两个功能。
- 保证可见性:当一个变量被volatile修饰,若一个线程对该变量进行了修改,那么这个修改对其他线程是立即可见的的
- 禁止指令重排序:通过内存屏障来禁止对屏障的指令进行重排序优化。
那你能聊一下ConcurrentHashMap的原理吗?
jdk1.7:
- 采用的是数组+链表。使用分段锁机制,将整个大的哈希表分割成多个小的片段,每个片段都有自己的锁。在进行修改操作时,首先先确定分段,然后获取该分段的分段锁(ReentrantLock)。由于每个分段都有自己的锁,所以可以允许多个线程同时进行写操作。
jdk1.8:
- 采用 CAS + Synchronized来保证并发安全
- CAS控制数组节点的添加,synchronized只锁定当前链表或红黑二叉树的首节点,在不冲突时不会发生并发问题
线程池
线程池的种类有哪些?
- 缓存线程池
- 固定大小线程池
- 单线程线程池
- 定时任务线程池
线程池的核心参数有哪些?
- 核心线程数目
- 最大线程数
- 生存时间
- 时间单位
- 工作队列
- 线程工厂
- 拒绝策略:抛异常(默认)、丢弃当前任务、丢弃最早任务、调用者执行任务
如何定义核心线程数?
- 根据cpu核数来确定。通常为cpu核数+1
线程池的执行原理?
- 首先判断线程池当中核心线程数是否都在执行任务,如果有空闲则创建一个新的线程来执行任务。
- 如果没有空闲,若工作队列没满,则将任务存储到该队列当中,否则判断线程池是够还有可用的线程,如果没有就交给拒绝策略
为什么不建议使用Executors创建线程池呢?
- 因为使用Executors创建线程池,默认的请求队列长度为Integer.MAX_VALUE。可能会导致OOM。所以采用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数
线程的使用场景?
如果控制某一个方法允许并发访问线程的数量?
- 可以使用信号量来限制线程的个数,它是一个正数。如果信号量为-1,就表示信号量用完了,其他线程需要阻塞了
那该如何保证Java程序在多线程的情况下执行安全呢?
- 原子性问题:synchronized、Lock
- 可见性问题:volatile、lock、synchronized
- 有序问题:join
你在项目中哪里用了多线程?
- 在我做电商项目的时候,里面有一个数据汇总的任务、用户在下单后,需要查询订单信息、也需要查看订单中商品的详细信息,和还需查看物流发货信息。这三个功能分别对应三个微服务。如果使用一个线程完成的话,互相等待时间较长。所以我们让多线程去同时处理,最终汇总结果即可(future获取每个线程执行结果)
其他
谈谈你对ThreadLocal的理解?
- 让每个线程各用各的资源,实现资源对象的线程隔离
- 实现线程内的资源共享
那你知道ThreadLocal的底层原理实现吗?
- 在其内部维持了一个ThreadLocalMap类型的成员变量
- set时,将自己作为key,资源对象作为value放入ThreadLocalMap。在get时以自己作为key,查找相关联的资源值
那关于ThreadLocal会导致内存溢出这个事情,了解吗?
- 主要是因为其底层ThreadLocalMap中key被设计为弱引用,这个key会被GC给被动的释放,但是value为一个强引用,不会被被动释放。
- 解决办法:使用ThreadLocal时将其作为静态变量(强引用),不会被动被GC回收。可以自动通过remove释放key,这样可以避免内存溢出