一、进程与线程
程序本身是静态的,是众多代码的组合产物,代码保存在文件中。如果程序要运行,则需要将程序加载到内存中,通过编译器将其编译成计算机能够理解的方式运行。
如果想启动一个Java程序,先要创建一个JVM进程。
进程是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程。多个线程各自拥有独立的局部变量、线程堆栈和程序计数器,能够访问共享的资源。
进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位;
一个进程中可以包含多个线程;进程与进程之间是相对独立的,进程中的线程之间并不完全独立,可以共享进程中的堆内存、方法区内存、系统资源等;
进程上下文的切换要比线程的上下文切换慢很多;
某个进程发生异常,不会对其它进程造成影响,但,某个线程发生异常,可能会对此进程中的其它线程造成影响;
二、线程组与线程池
1、线程组
线程组可以管理多个线程,顾名思义,线程组,就是把功能相似的线程放到一个组里,方便管理。
package com.guor.test;
public class ThreadGroupTest {
public static void main(String[] args) {
// 创建线程组
ThreadGroup threadGroup = new ThreadGroup("nezha");
Thread thread = new Thread(threadGroup,()->{
// 线程组名称
String groupName = Thread.currentThread().getThreadGroup().getName();
// 线程名称
String threadName = Thread.currentThread().getName();
System.out.println("groupName -- "+groupName);//groupName -- nezha
System.out.println("threadName -- "+threadName);//threadName -- thread
},"thread");
thread.start();
}
}
2、线程组和线程池有啥区别?
- 线程组中的线程可以跨线程修改数据,而线程组和线程组之间不可以跨线程修改数据;
- 线程池就是创建一定数量的线程,批量处理任务,当前任务执行完毕后,线程又可以去执行其它任务,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 线程池可以有效的管理线程的数量,避免线程的无限制创建,线程是很耗费系统资源的,动不动就会产生OOM,并且会造成cpu过度切换,也有强大的拓展功能,比如延时定时线程池。
三、用户线程与守护线程
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。
用户线程是最常见的线程,比如通过main方法启动,就会创建一个用户线程。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
JVM中的垃圾回收、JIT编译器线程就是最常见的守护线程。
只要有一个用户线程在运行,守护线程就会一直运行。只有所有的用户线程都结束的时候,守护线程才会退出。
编写代码时,也可以通过thread.setDaemon(true)指定线程为守护线程。
Thread daemonTread = new Thread();
// 设定 daemonThread 为 守护线程,默认false
daemonThread.setDaemon(true);
// 验证当前线程是否为守护线程,返回 true 则为守护线程
daemonThread.isDaemon();
守护线程的注意事项:
- thread.setDaemon(true)要在thread.start()之前设置,否则会抛出IllegalThreadStateException异常。你不能把正在运行的线程设置为守护线程;
- 在守护线程中产生的新线程也是守护线程;
- 读写操作或者计算逻辑不可以设置为守护线程;
四、并行与并发
并行指当多核CPU中的一个CPU执行一个线程时,其它CPU能够同时执行另一个线程,两个线程之间不会抢占CPU资源,可以同时运行。
并发指在一段时间内CPU处理多个线程,这些线程会抢占CPU资源,CPU资源根据时间片周期在多个线程之间来回切换,多个线程在一段时间内同时运行,而在同一时刻不是同时运行的。
并行和并发的区别?
- 并行指多个线程在一段时间的每个时刻都同时运行,并发指多个线程在一段时间内同时运行(不是同一时刻,一段时间内交叉执行)
- 并行的多个线程不会抢占系统资源,并发的多个线程会抢占系统资源;
- 并行是多CPU的产物,单核CPU中只有并发,没有并行;
五、悲观锁与乐观锁
1、悲观锁
还没干活就想着会出错,查询数据的后面跟上关键字for update;
悲观锁在一个线程进行加锁操作后使得该对象变为该线程的独有对象,其它的线程都会被悲观锁阻拦在外,无法操作。
悲观锁的缺陷:
- 一个线程获得悲观锁后其它线程必须阻塞。
- 线程切换时要不停的释放锁和获取锁,开销巨大。
- 当一个低优先级的线程获得悲观锁后,高优先级的线程必须等待,导致线程优先级倒置,synchronized锁是一种典型的悲观锁。
2、乐观锁
程序员自己控制,在表里面增加一个字段version(版本的意思),默认为0,每次操作,则递增。下次操作时,先核对版本号,如果对不上,则表示是旧的数据,得先查询再操作数据。
乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍直到成功为止,这个尝试的过程被称为自旋。乐观锁其实并没有加锁,但乐观锁也引入了诸如ABA、自旋次数过多等问题。
乐观锁一般会采用版本号机制,先读取数据的版本号,在写数据时比较版本号是否一致,如果一致,则更新数据,否则再次读取版本号,直到版本号一致。
Java中的乐观锁都是基于CAS自旋实现的。
六、CAS
1、什么是CAS?
Compare And Swap。
CAS(V, A, B) ,内存值V,期待值A, 修改值B(V 是否等于 A, 等于执行, 不等于将B赋给V)
2、CAS带来的问题
(1)ABA问题
CAS操作的流程为:
- 读取原值。
- 通过原子操作比较和替换。
- 虽然比较和替换是原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其它线程修改。
ABA问题有些时候对系统不会产生问题,但是有些时候却也是致命的。
ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。
(2)自旋次数过多
CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。
(3)只能保证一个变量的原子性
当对一个变量操作时,CAS可以保证原子性,但同时操作多个变量时CAS就无能为力了。
可以封装成对象,再对对象进行CAS操作,或者直接加锁。
七、那些年学过的锁
1、公平锁与非公平锁
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
2、独占锁与共享锁
- 独占锁:当多个线程则争抢锁的过程中,无论是读操作还是写操作,只能有一个线程获取到锁,其他线程阻塞等待。
- 共享锁:允许多个线程同时获取共享资源,采取的是乐观锁的机制,共享锁限制写写操作、读写操作,但不会限制读读操作。
3、可重入锁与不可重入锁
- 可重入锁:一个线程可以多次占用同一个锁,但是解锁时,需要执行相同次数的解锁操作;
- 不可重入锁:一个线程不能多次占用同一个锁;
八、死锁、活锁、饿死
1、死锁
多个线程互相持有对方需要的资源,导致多个线程相互等待,无法继续执行后续任务。
2、产生死锁的4个必要条件
- 互斥条件:指进程对所分配到的资源进行排它性使用,在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程被释放;
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不可剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 循环等待:一个等待一个,产生了一个闭环。
3、饥饿
饥饿指的是线程由于无法获取需要的资源而无法继续执行。
4、产生饥饿的主要原因
- 高优先级的线程不断抢占资源,低优先级的线程抢不到;
- 某个线程一直不释放资源,导致其他线程无法获取资源;
5、如何避免饥饿
- 使用公平锁分配资源;
- 为程序分配足够的系统资源;
- 避免持有锁的线程长时间占用锁;
6、活锁
活锁指的是多个线程同时抢占同一个资源时,都主动将资源让给其他线程使用,导致这个资源在多个线程之间来回切换,导致线程因无法获取相应资源而无法继续执行的现象。
7、如何避免活锁
可以让多个线程随机等待一段时间后再次抢占资源,这样会大大减少线程抢占资源的冲突次数,有效避免活锁的产生。
九、多线程锁的升级原理是什么?
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。
1、无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其它修改失败的线程会不断重试直到修改成功。
无锁总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,CAS是无锁技术的关键。
2、偏向锁
对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁。
如果线程处于活动状态,升级为轻量级锁的状态
3、轻量级锁
轻量级锁是指当锁是偏向锁的时候,被第二个线程B访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从er提升性能。
当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定次数时,轻量级锁便会升级为重量级锁,当一个线程已持有锁,另一个线程在自旋,而此时第三个线程来访时,轻量级锁也会升级为重量级锁。
注:自旋是什么?
自旋(spinlock)是指当一个线程获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
4、重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监听器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
5、锁状态对比
偏向锁 | 轻量级锁 | 重量级锁 | |
---|---|---|---|
使用场景 | 只有一个线程进入同步块 | 虽然很多线程,但没有冲突,线程进入时间错开因而并未争抢锁 | 发生了锁争抢的情况,多条线程进入同步块争用锁 |
本质 | 取消同步操作 | CAS操作代替互斥同步 | 互斥同步 |
优点 | 不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId) | 不会阻塞 | 不会空耗CPU |
缺点 | 适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗 | 长时间获取不到锁空耗CPU | 阻塞,上下文切换,重量级操作,消耗操作系统资源 |
6、锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,别切不会被其它线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。