多线程面试问题
线程和进程?
进程指的是程序的一次执行的过程,是系统程序的基本单位,因此进程是动态的
线程:线程比进程更小的执行单位,一个进程的执行可以产生多个线程,
多个线程是共享进程的堆和方法区的,每个线程拥有自己的程序计数器,虚拟机栈,本地方法栈
程序计数器为什么是私有的?
程序计数器的作用
-
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
-
多线程的情况下的时候,程序计数器是用来记录线程当前的执行位置,当线程被切换回来的时候能够知道线程上次运行到哪里了
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
句话简单了解堆和方法区一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
说说并发与并行
并发:两个或者两个以上的作业在同一时间段运行
并行:两个或者两个以上的作业在同一时刻运行
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力
为什么要使用多线程呢?
先从总体上来说:
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
实现线程安全的方式
- 阻塞同步:加锁
- 非阻塞同步:CAS
- 无同步方案:本都存储
线程的生命周期
NEW–初始阶段,刚刚开始被构建
RUNNABLE–运行阶段 就绪和运行 都被称为运行中
BLOCKED–阻塞阶段
WAITING–等待阶段,表示线程进入等待状态,表示等待其他的线程做出一些特定的行为
TIME_WAITING-可以自行返回,不需要唤醒
TERMIINATED-终止阶段,表示一切都已经执行完毕了
上下文切换
线程在执行状态的时候会有自己的运行条件和状态(上下文)
- 线程的 cpu 时间片用完
- 垃圾回收 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
保存线程的当前线程的上下文,留待线程下次占用CPU的时候恢复现场,并加载下一个要占用的CPU的线程上下文,这就是上下文切换
死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
如何预防和避免线程死锁?
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
破除循环等待,来进行这个
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
Sleep和Wait方法
sleep( )方法没有释放锁,而wait方法释放了锁
-
wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。 -
wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)
超时后线程会自动苏醒
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
总结: 调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
new 一个 Thread,线程进入了新建状态。调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。 但是,直接执行 run()
方法,会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
Synchronized关键字
对象
对象 包括,对象头,实例数据, 对齐填充
对象头 MarkWord 锁信息,分代年龄,hashcode
对象类型指针,数组长度
synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
怎么使用Synchronized关键字
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
2.修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized
方法,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,而访问非静态 synchronized
方法占用的锁是当前实例对象锁。
3.修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。- 尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能!
手写单例模式
public class Singleton {
priavte volatile static Singleton uniqueInstance;
//保证instance在所有线程中同步
//private避免类在外部被实例化
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经进行实例化,如果没有实例化的话进入加锁的代码
if(uniqueInstance == null) {
//类对象加锁
synchronized(Singleton.class) {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
public class SingletonLazy
{
public static void main(String[] args)
{
President zt1=President.getInstance();
zt1.getName(); //输出总统的名字
President zt2=President.getInstance();
zt2.getName(); //输出总统的名字
if(zt1==zt2)
{
System.out.println("他们是同一人!");
}
else
{
System.out.println("他们不是同一人!");
}
}
}
class President
{
private static volatile President instance=null; //保证instance在所有线程中同步
//private避免类在外部被实例化
private President()
{
System.out.println("产生一个总统!");
}
public static synchronized President getInstance()
{
//在getInstance方法上加同步
if(instance==null)
{
instance=new President();
}
else
{
System.out.println("已经有一个总统,不能产生新总统!");
}
return instance;
}
public void getName()
{
System.out.println("我是美国总统:特朗普。");
}
}
构造方法可以使用 synchronized 关键字修饰么?
一.构造方法作用
构造方法,是一种特殊的成员方法,它是一个与类同名的方法。
对象的创建就是通过构造方法来完成,其功能主要是完成对象的初始化。
当类实例化一个对象时会自动调用构造方法。
构造方法和其他方法一样也可以重载。
总而言之,构造方法就是类构造对象时调用的方法,主要用来实例化对象。
二. 创建对象
类名 对象名 = new 类名();
例如: Student student1=new Student();
//创建了一个Student类型的名为student1的对象。
//Student()就是一个构造方法。
三.如何写构造方法
1.构造方法的规则
1.方法名和类名完全相同。
2.它不含返回值。(void也没有)
3.不能被static、final、abstract、native等等修饰。
4.构造方法可以重载,所以可以有参数,也可以没有参数,且四种权限符号都可以使用。
5.它的参数列表中的形参类型一定要和成员属性的参数类型匹配。
2.无参构造
public 类名()
{ }
如果我们没有写任何的构造方法,系统会默认提供一个无参的构造器。
如果我们已经写了有参构造方法,则默认的无参构造方法消失,需要自己写一个无参构造。
*ew 类名()这个被执行的时候,会在堆内存中开辟一块空间,
用来存放具体的对象,因为是无参构造,所以会给对象的成员属性赋初值,赋默认值。
3.有参构造
public 类名(参数列:int cj,String name)
{
this.变量名=变量名;
this.cj=cj;
this.name=name;
}
执行完构造方法后,对象的成员属性就会被赋值。
public class Student//类名
{
private int cj;
private String name;
public Student( )//无参构造
{ }
public Student(int cj,String name)
//有参构造,且参数列表中的形参类型一定要和成员属性的参数类型匹配。
{
this.cj=cj;
this.name=name;
}
public void set(int cj,String name)//用于赋值的方法
{
this.cj=cj;
this.name=name;
}
public int getcj()
{
int cj=this.cj;
return cj;
}
public String getname()
{
String name=this.name;
return name;
}
}
class Test
{
public static void main(String[] args)
{
Student student1=new Student();
//用无参构造创建了student1,这个时候student1的成员属性被赋初值,赋默认值
student1.set(91,"zhang");
//调用方法给student1的成员属性赋值
Student student2=new Student(98,"li");
//用有参构造创建了student2,这个时候student2的成员属性为98,"li" 。
int cj2=student2.getcj();
String name2=student2.getname();
System.out.println(cj2);
System.out.println(name2);
}
}
先说结论:构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说
synchronized关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面
synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
synchronized 和 ReentrantLock 的区别
可重入锁
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized
是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:
- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
Condition
是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()
方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized
关键字就相当于整个 Lock 对象中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition
实例的signalAll()
方法 只会唤醒注册在该Condition
实例中的所有等待线程
JMM
Java 内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LvmpLXA-1658039644295)(C:/Users/%E6%9D%91%E5%A4%B4/AppData/Roaming/Typora/typora-user-images/image-20220716155916808.png)]
- 原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
synchronized
可以保证代码片段的原子性。 - 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可见性。 - 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁止指令进行重排序优化。
说说 synchronized 关键字和 volatile 关键字的区别
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
lock vs synchronized
要求
- 掌握 lock 与 synchronized 的区别
- 理解 ReentrantLock 的公平、非公平锁
- 理解 ReentrantLock 中的条件变量
三个层面
不同点
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(选择不同的实现)
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
ThreadLocal
ThreadLocal简介
创建的变量可以被任何一个线程访问并修改的,
如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
创建一个Thread Local变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来,可以使用get( )和set( )方法来获取默认值或者将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的
不共享了,各用各的
作用
- ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
本质:就是一个Map集合,将线程ID作为KEY存进去,而且防止同一个线程再次获取连接池的时候,重复创建对象
每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象。而key是同一个ThreadLocal,是在不同的Map上进行操作的
原理
每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocalMap 的一些特点
- key 的 hash 值统一分配
- 初始容量 16,扩容因子 2/3,扩容容量翻倍
- key 索引冲突后用开放寻址法解决冲突(就是去找下一个位置。下一个空的位置,一般主要是使用的数据也不是很多)
ThreadLocal
弱引用 key
ThreadLocalMap 中的 key 被设计为弱引用,原因如下
- Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存
内存释放时机
- 被动 GC 释放 key
- 仅是让 key 的内存释放,关联 value 的内存并不会释放
- 懒惰被动释放 value
- get key 时,发现是 null key,则释放其 value 内存
- set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
- 主动 remove 释放 key,value
- 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
- 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收
线程池
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Runnable接口和Callable接口
Runnable
接口 不会返回结果或抛出检查异常,但是 Callable
接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable
接口 ,这样代码看起来会更加简洁。
@FunctionalInterface
public interface Runnable {
/**
* 被线程执行,没有返回值也无法抛出异常
*/
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}
Atomic 原子类
基本类型
使用原子的方式更新基本类型
AtomicInteger
:整型原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整型数组原子类AtomicLongArray
:长整型数组原子类AtomicReferenceArray
:引用类型数组原子类
引用类型
AtomicReference
:引用类型原子类AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。AtomicMarkableReference
:原子更新带有标记位的引用类型
对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器AtomicLongFieldUpdater
:原子更新长整型字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型字段的更新器
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
CAS问题
比较并交换 compareAndSwap的缩写
- ABA问题
- 自旋时间长
- 只能保证一个共享变量的原子操作
CAS的底层原理:
- automicInteger.getAndIncrement()
- Unsafe
- cas unsafe.getAndInt .底层汇编
CAS的缺点
原子的更新引用
cas --> unsafe -->CAS的底层–>ABA -->原子更新引用 -->如何规避ABA问题
ABA:狸猫换太子
现实生活中,出现多人合作的时候,会出现对方已经处理的真空地带
A-B-A 这两个A不是一个内存地址了
不代表这个过程是没有问题的
CAS会出现这样的问题
- CAS的好处就是保证数据一致性的同时,也保证了并发性
- CPU底层的指令原语的原子性是在修改的时候保证不受其他的线程的抢断,所以在A线程的10秒内,A并没有进行修改写回物理内存,其他线程可以随意的修改主内存的变量值
原子引用
时间戳原子引用:
新增机制,修改版本哈(类似于时间戳)
AutomaticStampedReferenced 用来解决这个问题
但是这个东西还是很鸡肋,我觉得使用传统的互斥同步,来解决这个问题会更好
自旋锁
尝试获取锁的线程不会立即阻塞,采用循环的方式来尝试获取锁,这样的好处是减少线程的上下文切换的消耗,缺点是循环消耗CPU
循环比较直到成功为止
自选的本质是 while
偏向锁
一段同步的代码,共享资源一直被一个线程方法使用,那么该线程获取这个锁的代价比较低
轻量级锁
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级成轻量级锁,其他线程通过自旋的形式尝试获取锁,不会阻塞,从而提高性能,但是自旋的次数有限,也就是我们的所说的自适应锁
使用CAS来会进行加锁减锁
重量级锁
互斥锁。涉及到CPU的介入。获取不到锁的进程,会被阻塞
互斥
典型代表–synchronized
AQS
AQS的核心思想使用一个被volatile修饰的int类型的成员变量表示同步状态(共享资源)。然后使用这个CAS来对这个同步状态进行修改,从而保证线程安全
如果被请求的共享资源空闲,则将当前的请求线程设置为有效的工作线程,将对应的共享资源设置为锁定状态(计数器使用1 和 0)
如果共享资源被占用了,则通过CLH同步队列将暂时用不到的线程封装到一个节点中去,加入到队列中,在适当的时候进行阻塞和唤醒。
acquireQueue
读写锁
- 多个读者都可以同时读
- 写者必须互斥(只允许一个写者写,也不能读者写者同时来写+
- 写者优先于读者(一旦有写者,后序读者是必须等待,唤醒的时候优先考虑写者)
悲观锁 vs 乐观锁
对比悲观锁与乐观锁
-
悲观锁的代表是 synchronized 和 Lock 锁
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
- 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
- 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
-
乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
- 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
- 它需要多核 cpu 支持,且线程数不应超过 cpu 核数
线程池
要求
- 掌握线程池的 7 大核心参数
七大参数
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
- 抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
- 由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
- 丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
- 丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
守护线程与非守护线程
Java分为两种线程:用户线程和守护线程
所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
使用:
Runnable tr=new TestRunnable();
Thread thread=new Thread(tr);
thread.setDaemon(true); //设置守护线程
thread.start(); //开始执行分进程
AQS源码解释
可重入锁
外层-》中层-》内层