并发编程
线程
什么是线程和进程?
何为进程?进程是程序的一次执行过程,是系统运行程序的基本单位。比如说在windows系统上查询任务管理器,就可以看到.exe运行的进程
何为线程?一个进程在其执行的过程中可以产生多个线程。与线程不同的就是同类的多个线程共享进程中的堆和方法去的资源,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
Java 线程和操作系统的线程有啥区别?
现在的 Java 线程的本质其实就是操作系统的线程。
请简要描述线程与进程的关系,区别及优缺点?
一个进程中可以有多个线程,多个线程共享进程中的堆和方法区(JDK1.8之后的元空间)资源,但是每个线程都有自己的虚拟机栈、程序计数器和本地方法栈
总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一个进程的线程极有可能相互影响。
如何创建线程?
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池
- 使用CompletableFuture
严格来说,java只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方法,最终都依赖于new Thread().start()
说说线程的生命周期和状态
new初始化状态 runnable运行状态 调用start()等待运行的状态 blocked阻塞状态 需要等待锁释放 waiting等待状态 需要等待其他线程做出一些反应(通知或中断) time_waiting超时等待状态 在等待指定时间自行返回而不是像waiting那样一直等待 terminated终止状态 线程已经运行完毕
什么是线程上下文切换?
线程切换意味需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文。这就是所谓的上下文切换。
Thread#sleep() 方法和 Object#wait() 方法对比
共同点:两者都可以暂停线程的执行。
区别:
- 有没有释放锁:wait方法释放了锁,sleep方法没有释放锁
- 用途是:通常用于线程间交互/通信,sleep通常用于暂停执行
- 会不会自动苏醒,wait是需要别的线程调用同一个对象上的notify或者notifyall方法才会苏醒,sleep方法执行后,线程会自动苏醒,也可以使用wait(long timeout)超时后线程会自动苏醒。
- sleep是thread类的静态本地方法,wait则是object类的本地方法
为什么 wait() 方法不定义在 Thread 中?
wait是为获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象中都有对象锁,所以放在object中
sleep只是暂停线程,所以就放在thread中
可以直接调用 Thread 类的 run 方法吗?
不可以,调用start方法可启动线程并使线程进入就绪状态,直接运行run方法的话不会以多线程的方式执行。
多线程
并发与并行的区别
并发是在同一时间段内执行
并行是在同一时刻执行
同步和异步的区别
同步:发出一个调用之后,在没有得到结果之前,该调用就不可以返回,一直等待
异步:调用在发出之后,不用等待返回结果,该调用直接返回
为什么要使用多线程?
多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
利用好多线程机制可以大大提高系统整体的并发能力以及性能。
在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。
多核时代多线程主要是为了提高进程利用多核 CPU 的能力。
单核 CPU 支持 Java 多线程吗?
单核CPU是支持Java多线程的。
单核 CPU 上运行多个线程效率一定会高吗?
单核CPU同时运行多个线程的效率是否会高取决于线程的类型和任务的性质。
- CPU密集型:主要进行计算和逻辑处理,需要占用大量的CPU资源
- IO密集型:主要进行输入输出操作,如读写文件、网络通信等,需要等待IO设备的相应,而不占用太多的CPU资源
使用多线程可能带来什么问题?
使用并发访问是为了提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度,而且并发编程可能会带来很多问题,比如:内存泄漏、死锁、线程不安全等
如何理解线程安全和不安全?
线程安全和线程不安全是在多线程环境下对于同一份数据的访问是否能保证其正确性和一致性
- 线程安全:是指在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性
- 线程不安全:是指在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失
死锁
什么是线程死锁?
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止
产生死锁的四个条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
如何检测死锁?
使用jmap、jstack等命令查看jvm线程栈和堆内存的情况。
可以使用top、df、free等命令查看操作系统的基本情况,出现死锁可能会导致CPU、内存等资源消耗过高
如何预防和避免线程死锁?
如何预防死锁?破坏死锁产生的必要条件即可:
- 破环请求与保持:一次性申请所有的资源。
- 破环不剥夺条件:占用部分的线程进一步地申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
JMM(Java内存模型)
volatile关键字
如果保证变量的可见性?
如果我们把变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取,也就是说本来是每个线程独有的一个资源,将变量声明为volatile时,就将这个变量放到主存中变成了共享变量了
如何禁止指令重排序?
在java中,volatile关键字除了可以保证变量的可见性,还有一个重要的作用就是防止JVM的指令重排序。
如果我们将变量声明为volatile,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序。
volatile可以保证原子性吗?
不能,volatile关键字能保证变量的可见性,但不能保证对变量的操作是原子性的
乐观锁和悲观锁
什么是悲观锁?
悲观锁总是假定最坏的情况,认为共享资源每次访问的时候就会出现问题,所以每次在获取资源操作的时候就会上锁。也就是说共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。
什么是乐观锁?
乐观锁总是假定最好的情况,认为共享资源每次访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源是否被其他线程修改了。
- 悲观锁常用于写比较多的场景,可以避免频繁失败和重试影响性能,悲观锁的开销是固定的
- 乐观锁常用于写比较少的场景,可以避免频繁加锁影响性能
如何实现乐观锁?
乐观锁一般会使用版本号机制或CAS算法实现
Java中CAS是如何实现的?
在java中,实现CAS(Compare-And-Swap,比较并交换)操作的一个关键类是Unsafe
CAS算法存在哪些问题?
- ABA问题:初次读取是A,准备赋值的时候还是A,那么这个不一定是之前的那个A。解决方法是在变量前面追加上版本号或者时间戳
- 循环时间长开销大:CAS经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功
只能保证一个共享变量的原子操作
CAS操作仅能对单个共享变量有效
ThreadLocal线程本地
ThreadLocal有什么用?
threadlocal类允许每个线程绑定自己的值
ThreadLocal原理了解吗?
以后了解
ThreadLocal内存泄漏问题是怎么导致的?
以后了解
如何跨线程传递 ThreadLocal 的值?
线程池
什么是线程池?
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。这样子就可以减少线程创建和销毁所带来的资源消耗,提高资源的利用效率
为什么要用线程池?
其实就是池化技术,线程池、数据库连接池、HTTP连接池等对这个思想的应用。
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率
- 降低资源的消耗
- 提高相应速度
- 提高线程的可管理性
如何创建线程池?
- 方式一:通过ThreadPoolExecutor构造函数来创建
- 方式二:通过Executor框架的工具类Executors来创建
- FixedThreadPool:固定线程数量的线程池。
- SingleThreadExecutor: 只有一个线程的线程池。
- CachedThreadPool: 可根据实际情况调整线程数量的线程池。
- ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池。
为什么不推荐使用内置线程池?
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
线程池常见参数有哪些?如何解释?
- 三个最重要的参数
- 核心线程数:任务队列未达到队列容量时,最大可以同时运行的线程数量
- 最大线程数:任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
- 阻塞队列:新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
- 其他常见参数
- 线程工厂:executor创建新线程的时候会用到
- 拒绝策略:拒绝策略
- 存活时间:当线程池中的线程大于核心线程数的时候,即有非核心线程时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了存活时间才会被回收销毁
- 存活时间单位:存活时间的单位
首先,线程会达到核心线程数,然后继续添加线程的时候就会加入到阻塞队列中,如果阻塞队列达到最大的话,那么就会新的线程创建出来直到最大核心线程数,最大核心线程数都创建出来后,剩下的就是采用拒绝策略
线程池的核心线程会被回收吗?
threadpoolexecutor默认不会回收核心线程数,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程被用于周期性使用的场景,且频率不高,可以考虑将allowCoreThreadTimeOut(boolean value)的方法参数设置为true,这样就会回收空闲的核心线程了。
核心线程空闲时处于什么状态?
- 设置了核心线程的存活时间 :在空闲时,wating状态,等待获取任务。如果阻塞等待时间超过了核心线程存存活时间,则该线程会退出工作,线程状态变为terminated状态
- 没有设置核心线程的存活时间:核心线程在空闲时,会一直处于waiting状态,等待获取任务,核心线程会一直存活在线程池中