线程的优点
进程与线程的区别
创建线程三个方法
结束线程的两个常用方法
等待一个线程 join()
获取当前线程的引用
Java线程共有⼏种状态?状态之间怎么切换的?
synchronized特点
volatile的特点
线程不安全问题及解决方案
wait() 和notify() 的作用
wait()和sleep()的对比
常见的锁策略
synchronized自身优化手段/synchronized实现原理
你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
介绍下读写锁?
什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
synchronized 是可重⼊锁么?
CAS
总结什么是CAS:
CAS主要解决了以下问题:
CAS内在的ABA问题是什么
为了解决ABA问题,可以采用以下方法:
线程的优点
- 创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
- 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
- 线程占⽤的资源要⽐进程少很多
- 能充分利⽤多处理器的可并⾏数量
- 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
- 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
- I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
进程与线程的区别
进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间. 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
创建线程三个方法
1.继承Thread类,重写run方法
public static void main(String[] args) {class MyThread extends Thread{@Overridepublic void run() {System.out.println("成功创建线程!");}}MyThread thread = new MyThread();thread.start();}
2.实现Runnable接口,重写run方法
public static void main(String[] args) {class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("成功创建线程!");}}MyRunnable runnable = new MyRunnable();Thread thread = new Thread(runnable,"继承Runnable接口实现");thread.start();}
3.lambda表达式创建Runnable子类对象(推荐)
public static void main(String[] args) {Thread t1=new Thread(()->{System.out.println("使用匿名内部类成功创建线程!");});thread.start();}
结束线程的两个常用方法
1.使用定义变量作为标志位,需要给标志物加上volatile保证变量的可见性,被修改时可以被看到
public class MyThread extends Thread {private volatile boolean running = true;@Overridepublic void run() {while (running) {// 执行你的任务// ...// 模拟一些工作try {Thread.sleep(100);} catch (InterruptedException e) {// 线程被中断时,可以选择停止运行running = false;}}}public void stopThread() {running = false;} }
2.使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记public class MyThread extends Thread {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {// 执行你的任务// ...// 模拟一些工作try {Thread.sleep(100);} catch (InterruptedException e) {// 线程被中断时,不处理中断,只是退出循环}}} }
等待一个线程 join()
thread.join()让主线程等待thread执行完再执行
public class Thread1 {public static void main(String[] args) throws InterruptedException {// 使用lambda表达式创建一个新线程Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("子线程正在执行: " + i);try {Thread.sleep(500); // 模拟耗时操作} catch (InterruptedException e) {// 如果子线程在sleep时被中断,这里只是简单地打印堆栈跟踪e.printStackTrace();// 通常,我们应该在子线程中适当地处理中断}}});// 启动线程thread.start();// 当前线程(主线程)等待子线程执行完毕thread.join();// 子线程执行完毕后,主线程继续执行System.out.println("子线程已经执行完毕,主线程继续执行");} }
获取当前线程的引用
当你在一个多线程环境中编写代码时,你经常需要知道当前正在执行代码的线程是哪个,这时就可以使用
Thread.currentThread()
来获取当前线程的引用。public class Thread1 {public static void main(String[] args) {Thread Thread = Thread.Thread();System.out.println("当前线程名称: " + Thread.getName());} }
Java线程共有⼏种状态?状态之间怎么切换的?
- NEW: 安排了⼯作, 还未开始⾏动. 新创建的线程, 还没有调⽤ start ⽅法时处在这个状态.
- RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作. 调⽤ start ⽅法之后, 并正在 CPU 上 运⾏/在即将准备运⾏ 的状态.
- BLOCKED: 使⽤ synchronized 的时候, 如果锁被其他线程占⽤, 就会阻塞等待, 从⽽进⼊该状态.
- WAITING: 调⽤ wait ⽅法会进⼊该状态.
- TIMED_WAITING: 调⽤ sleep ⽅法或者 wait(超时时间) 会进⼊该状态.
- TERMINATED: ⼯作完成了. 当线程 run ⽅法执⾏完毕后, 会处于这个状态.
synchronized特点
互斥性:当一个线程访问某个被
synchronized
修饰的代码块或方法时,其他线程无法同时访问这个代码块或方法。这种互斥性保证了同一时刻只有一个线程能执行某个代码段,从而避免了数据竞争和脏读等问题。可重入性:同一个线程可以多次获得同一个对象的锁,即允许一个线程在持有锁的情况下再次进入同步代码块或方法。这避免了死锁和不必要的线程切换开销。
可见性:当一个线程释放锁时,它会将修改后的共享变量的值刷新到主内存中,因此其他线程能够立即看到这些修改后的值。这保证了线程之间对共享变量的可见性。
隐式锁定和解锁:使用
synchronized
关键字时,不需要显式地调用锁定和解锁方法(如lock()
和unlock()
)。当线程进入synchronized
代码块或方法时,它会自动获得锁;当线程退出synchronized
代码块或方法时,它会自动释放锁。这简化了并发编程的复杂性。
volatile的特点
- 保证可见性:当一个线程修改了
volatile
变量的值,这个新值会立即对其他线程可见。这是通过禁止 CPU 和编译器优化来实现的,确保了对共享变量的修改能够立即被其他线程看到。- 禁止指令重排序:在程序运行时,为了提高性能,编译器和处理器可能会对指令进行重排序,但在多线程环境下,这可能导致逻辑错误。
volatile
修饰的变量在读、写时会加入内存屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。- 受限的原子性:对于单个
volatile
变量的读/写操作是原子的,但复合操作(如 i++)则不是。因此,在多线程环境中,volatile
并不能完全保证原子性。
线程不安全问题及解决方案
线程不安全的原因
并发访问共享资源:多个线程在没有同步的情况下同时访问和修改同一份数据或资源,导致数据不一致和错误。
原子性问题:像
count++
这样的操作并不是原子的,它们由多个步骤组成(读取-修改-写入),这些步骤可能在多线程环境中被其他线程打断,导致数据不一致。内存可见性问题:一个线程对共享变量的修改,对于其他线程来说可能不是立即可见的,这取决于JVM的内存模型和硬件的内存模型。
指令重排序:为了提高性能,编译器和处理器可能会对指令进行重排序,这种重排序在单线程环境中是安全的,但在多线程环境中可能导致问题。
解决方案
使用没有共享资源的模型
如果线程之间不共享任何资源,那么它们之间的交互将非常简单,也就不需要担心线程安全问题。使用共享资源只读,不写的模型
- 不需要写共享资源的模型: 如果多个线程只读取共享资源而不进行修改,那么就不会出现数据不一致的问题
- 使用不可变对象: 不可变对象一旦创建,其内容就不能被修改。因此,在多线程环境中,不可变对象是线程安全的。例如使用
String
、Integer
等不可变类,或者自定义不可变类。直接线程安全(重点)
- 保证原子性: 原子性是指一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。使用
synchronized
等机制来确保操作的原子性。- 保证顺序性: 在多线程环境中,由于线程调度的不确定性,可能会出现操作顺序的问题。我们需要确保关键操作的顺序性。使用
synchronized
块、volatile
关键字等机制来确保操作的顺序性- 保证可见性: 可见性是指当一个线程修改了某个变量的值,新值对于其他线程来说是立即可见的。使用
volatile
关键字确保当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。
wait() 和notify() 的作用
wait() 的作用
wait()
方法的主要作用是使当前线程进入等待(阻塞)状态,并释放对象的锁。这意味着,当线程调用某个对象的wait()
方法时,它会放弃对该对象的锁,并进入等待状态,直到其他线程调用了该对象的notify()
或notifyAll()
方法。在等待期间,线程不会消耗CPU资源,因为它被暂停执行了。notify() 的作用
notify()
方法的作用是唤醒在该锁上等待的单个线程(如果有的话)。被唤醒的线程会重新尝试获取对象的锁,并在成功获取锁后继续执行。需要注意的是,notify()
方法并不会立即释放对象的锁,而是在当前线程退出同步代码块或同步方法时释放。另外,notify()
方法只会唤醒一个等待的线程,如果有多个线程在等待,那么它会随机选择一个线程进行唤醒。
wait()和sleep()的对比
- wait: 主要是在不知道要等多久的前提使用,所谓的超时时间是兜底用的,一般在这之前就被notify唤醒了,wait需要搭配synchronized使用,wait是Object方法
- sleep: 一定是知道要多少时间的前提下使用,在时间到达之前是不会醒,除了异常情况的提前唤醒,sleep不用搭配synchronized使用,使用时候需要try-catch,sleep是Thread的静态方法
常见的锁策略
- 悲观锁:加锁钱预估锁冲突出现的概率大,因此加锁会做很多工作,加锁速度会慢,但是不容易出现问题,适用于锁冲突比较激烈的时候
- 乐观锁:预估锁冲突出现的概率小,加锁前不做太多工作,加锁会快,但是容易引起其他问题导致消耗更多cpu资源,适用于锁冲突不激烈的时候
- 重量级锁:类似悲观锁,加锁开销大且速度慢,
- 轻量级锁:类似乐观锁,加锁开销小且加锁速度快
乐观悲观指的是对未发生的事情进行预估
重量级轻量级指的是加锁后对结果的评价,两种角5/度描述同一件事情
- 自旋锁:轻量级的典型实现,如果获取锁失败,立即尝试加锁,无限循环,可以在其他线程释放锁的第一时间拿到锁,这种适用于锁冲突概率小的情况,也就是乐观锁
- 挂起等待锁:重量级的典型实现,在获取锁失败的时候就加入等待队列,锁释放的时候再参与锁竞争中,同时也是悲观锁
- 公平锁:简单理解就是,遵守先来后到,先来的线程先拿锁
- 非公平锁:所有线程都需要参与锁竞争
- 可重入锁:可以允许一个线程获取多把锁,多加锁也不会死锁
- 不可重入锁:一个线程只能获取一把锁,多了就死锁了
- 读写锁:包括读锁和写锁,加上读锁只能读,加上写锁当前线程只能写,允许多个线程读,但只允许一个线程写
- 互斥锁:加锁期间只有一个线程能安全的访问共享资源
synchronized自身优化手段/synchronized实现原理
synchronized基本特点
乐观锁/悲观锁自适应
轻量级锁/重量级锁自适应
自旋锁/挂起等待锁自适应
非公平锁
是可重入锁
不是读写锁
加锁过程
无锁->偏向锁->轻量级锁->重量级锁
- 偏向锁阶段:(假设是没有资源过来竞争)核心要点,非必要不加锁,此时并直到加锁了,只是做了个轻量标记,没有竞争就省去了这个操作,如果有竞争就会过渡到轻量级锁阶段
- 轻量级锁阶段:(假设有竞争但是不多),此时就是通过自旋锁的方式来实现,在其他线程释放锁的时候第一时间拿到锁,缺点消耗cpu,如果此时synchronized内部发现参与进程的线程比较多了就会进一步升级到重量级锁阶段
- 重量级锁阶段:此时拿不到锁就不会自旋了,而是进入"阻塞等待",机会让出cpu资源,如果有线程释放锁,就会随机唤醒一个线程参与锁竞争
锁粗化
- 粗细指的是锁的粒度粗细,也就是加锁代码涉及到的范围,{ }里的代码越多越粗
- 一段程序出现多次加锁解锁操作,编译器+JVM就会自动进行锁粗化,把几个"小锁合并成一个大锁"
锁消除
- 消除一些冗余的锁,消除的操作比较保守,只会那种一眼看着就是线程安全的代码进行消除
你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
- 悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
- 乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.
- 在访问的同时识别当前的数据是否出现访问冲突.
- 悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
- 乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突.
介绍下读写锁?
- 读写锁就是把读操作和写操作分别进⾏加锁.
- 读锁和读锁之间不互斥.
- 比特就业课写锁和写锁之间互斥.
- 写锁和读锁之间互斥.
- 读写锁最主要⽤在 "频繁读, 不频繁写" 的场景中.
什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
- 如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试
- 会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
- 相⽐于挂起等待锁,
- 优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景
- 下⾮常有⽤.
- 缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.
synchronized 是可重⼊锁么?
- 是可重⼊锁.
- 可重⼊锁指的就是连续两次加锁不会导致死锁.
- 实现的⽅式是在锁中记录该锁持有的线程⾝份, 以及⼀个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数⾃增
CAS
CAS是什么
在Java中,CAS(Compare and Swap)"比较和交换",也被称为无锁算法或乐观锁的一种实现。CAS操作用于解决并发环境下的数据竞争问题,特别是在多线程环境中对共享数据的访问和修改。
CAS操作包含三个参数:原数据(V)、旧的预期值(A)和需要修改的新值(B)。
1. CAS会比较A是否等于V
2. 如果比较相等,将B写入V,否则不做操作
3. 返回该操作是否成功
总结什么是CAS:
- 全称Compare and Swap,是一种天然原子操作,用于实现多线程环境下的无锁数据结构。CAS操作通过一条CPU指令来同时完成“读取内存、比较是否相等、修改内存”这三个步骤,从而确保这三个步骤不会被其他线程打断或干扰,实现了对共享数据的原子性读写
CAS主要解决了以下问题:
- 原子性问题:CAS操作可以保证对共享数据的原子性读写,从而避免了多线程并发访问时可能引发的数据不一致问题。
- 支持无锁算法:CAS操作不涉及加锁,也就不会阻塞,死锁,提高效率,提高系统并发性能,合理使用也可以线程安全
CAS内在的ABA问题是什么
- ABA问题指的是一个共享变量的值在操作期间从A变为B,然后再从B变回A,而CAS操作可能会错误地认为没有其他线程修改过这个值。
为了解决ABA问题,可以采用以下方法:
给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版本号⽐之前读到的版本号⼤, 就认为操作失败
JUC(java.util.concurrent)