【JUC】一些线程基础
2021/9/2 23:07:27
本文主要是介绍【JUC】一些线程基础,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
1.Java 内存模型
1.什么是 Java 内存模型?
Java 内存模型简称为 JMM(Java Memory Model),是和多线程相关的一组规范,需要各个 JVM 来遵守实现
2.为什么需要 JMM?
有了 JMM 就可以让程序在 windows 和 Linux 上有一样的执行效果,即屏蔽了底层的差异,实现 Write Once,Run Anywhere !,并且解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
3.什么是指令重排序?
编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序
4.为什么要重排序?
提高整体的运行速度
5.重排序的 3 种情况
- 编译器优化
- CPU 重排序
- 内存的“重排序”
6.主内存和工作内存
- CPU 多级缓存示意图
线程间对于共享变量的可见性问题,是由我们刚才讲到的这些 L3 缓存、L2 缓存、L1 缓存,也就是多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样,后续对于数据的修改也是先写入到自己的 L1 缓存中,然后等待时机再逐层往下同步,直到最终刷回内存。
- 什么是主内存和工作内存?
主内存和工作内存的关系
JMM 有以下规定:
- 所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;
- 线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;
- 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。
7.volatile 和 synchronized 有什么区别?
volatile 是 Java 中的一个关键字,是一种同步机制,它可以保证共享变量的可见性(禁用 CPU 缓存),也可以禁止指令重排序,保障有序性
synchronized 是由 CPU 原语层面支持的锁机制,既复合 happens-before 规则保证了可见性,也保证了操作的原子性
8.并发编程 Bug 的源头
1.线程切换带来的原子性问题-->使用同步锁
2.多核 CPU 带来的缓存可见性问题-->利用好 happen-before 原则
3.编译优化带来的指令重排序问题--->使用 volatile
2.线程
创建线程的 3 种方式
public class HelloThread { // 1.继承 Thread 类,重写 Run 方法 static class MyThread extends Thread { @Override public void run() { System.out.println("hello extends thread"); } } //2.实现 Runnable 接口,重写 Run 方法 static class MyThread01 implements Runnable { @Override public void run() { System.out.println("hello implements runnable"); } } //3.使用 lambda 表达式 public static void main(String[] args) { new MyThread().start(); new Thread(new MyThread01()).start(); //lambda 表达式 new Thread(() -> { System.out.println("hello lambda"); }).start(); } }
启动线程的 4 种方式
1.继承 Thread 类
2.实现 Runnable 接口(推荐,主要是因为有利于类的扩展)
3.使用 lambda 表达式
4.使用线程池,让一个线程启动
3.如何正确停止线程?(使用 interrupt)
使用 interrupt 通知线程停止,禁止使用已经被舍弃的 stop()、suspend() 和 resume()
但是 interrupt 仅仅起到通知线程停止的作用,线程可以选择停止,也可以选择不停止
为什么 Java 不提供强制停止线程的能力呢?
Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作
如何用 interrupt 停止线程
Sleep 期间是否可以感受到中断信号?
- 可以的,并且会抛出 InterruptException 异常,但是需要注意抛出异常的同时,会清除中断的标记位,所以在 catch 块里需要重新打上中断标记位,如上图所示。
- 如果在 sleep 期间,响应了中断,那么当前线程会抛出中断异常,并继续往下执行,在某种程度上也算是一种唤醒
休眠期间响应中断的 2 种最佳处理方式
- 抛出异常,让本方法的调用者继续处理
- 在 catch 块里重新打上中断标记位
为什么 volatile 标记位的停止方法在某些场景下错误的?
正确的场景
public class VolatileInterrupt implements Runnable { private volatile boolean cancled = false; @Override public void run() { int num = 0; while (!cancled) { System.out.println(num++); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { VolatileInterrupt runable = new VolatileInterrupt(); Thread task = new Thread(runable); task.start(); Thread.sleep(5000); runable.cancled = true; System.out.println("Main Thread Is Over"); } }
在生产者消费者模型下失效的场景
原因:生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理
// 生产者 class Producer implements Runnable { public volatile boolean canceled = false; BlockingQueue storage; public Producer(BlockingQueue storage) { this.storage = storage; } @Override public void run() { int num = 0; try { while (num <= 100000 && !canceled) { if (num % 50 == 0) { storage.put(num); System.out.println(num + "50 的倍数,被放到仓库中了。"); } num++; } } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("生产者结束运行"); } } } //消费者 class Consumer { BlockingQueue storage; public Consumer(BlockingQueue storage) { this.storage = storage; } public boolean needMoreNums() { if (Math.random() > 0.97) { return false; } return true; } } public static void main(String[] args) throws InterruptedException { ArrayBlockingQueue storage = new ArrayBlockingQueue(8); Producer producer = new Producer(storage); Thread producerThread = new Thread(producer); producerThread.start(); Thread.sleep(500); Consumer consumer = new Consumer(storage); while (consumer.needMoreNums()) { System.out.println(consumer.storage.take() + "被消费了"); Thread.sleep(100); } System.out.println("消费者不需要更多数据了。"); //一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来 producer.canceled = true; System.out.println(producer.canceled); } }
4.六种线程状态之间的转换(必须要熟练背过!!!)
- New(新创建)
- Runnable(可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(被终止)
Yield 方法
临时暂停执行,再次回到 CPU 时间竞争队列中,等待 CPU 分配时间片
Synchronized 是可重入的
//在指定对象上加锁 synchronized doSomething1() { count++; } /** * 锁定当前对象 * */ synchronized doSomething2() { count++; doSomething1(); }
5.wait/notify/notifyAll 方法的使用注意事项?
- 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?
源代码注释:
英文部分的意思是说,在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁
这样设计有什么好处呢?分析如下代码
class BlockingQueue { Queue<String> buffer = new LinkedList<String>(); public void give(String data) { buffer.add(data); notify(); // Since someone may be waiting in take } public String take() throws InterruptedException { while (buffer.isEmpty()) { wait(); } return buffer.remove(); } }
由于这段代码在 CPU 层面并不是原子操作,可能会存在这样的场景:判断完 isEmpty 返回 true,发生线程切换,此时完整执行了 give()方法,因此也执行了 notify()方法,但此时 take 线程还没有执行到 wait()方法,也就是 notify()方法是没有效果的,而此时 take 获得了 CPU 时间片,执行了 wait()方法,那么这种情况下 take 线程会陷入无休止的等待状态,因为他完美的错过了 notify 的唤醒
- 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
- 在 Java 中,每个对象都有一个可以上锁的叫做 monitor 的监视器锁,,在对象头中有一个位置来保存锁信息,这个锁是对象级别的,Object 是所有对象的父类,所以把这些方法定义在 Object 类中较为合适
- 一个线程可能会持有多把锁,来实现较为复杂的逻辑,把锁定义在 Thread 类中,不合适
- wait/notify 和 sleep 方法的异同?
相同点
- 它们都可以让线程阻塞。
- 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
不同点
- 获取锁:wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
- 释放锁:在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
- 设置时间:sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
- 所属类****:wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
6.三种实现生产者消费者模型的方法
1.BlockQueue(最简单)
public static void main(String[] args) { //线程安全的阻塞队列 BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); Thread producer = new Thread(new Runnable() { @Override public void run() { while(true){ try { Thread.sleep(1000); queue.put(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "Producer"); Thread consumer = new Thread(new Runnable() { @Override public void run() { while(true) { try { Thread.sleep(1000); queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } } }, "consumer"); producer.start(); consumer.start(); }
2.Condition
要点:使用可重入锁,unlock 一定要写在 finally 里面,new 两个 condition,注意 while 自旋检查队列长度
public class ConditionTest { static LinkedList<Integer> queue = new LinkedList<>(); static ReentrantLock lock = new ReentrantLock(); static Condition notEmpty = lock.newCondition(); static Condition notFull = lock.newCondition(); static class Consumer implements Runnable { @Override public void run() { while(true) { lock.lock(); try { Thread.sleep(1000); while (queue.size() == 0) { notEmpty.await(); } queue.pollLast(); notFull.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } } static class Producer implements Runnable { @Override public void run() { while(true) { lock.lock(); try { Thread.sleep(1000); while (queue.size() == 10) { notFull.await(); } queue.offer(10); notEmpty.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } } public static void main(String[] args) { new Thread(new Producer(), "Producer-1").start(); new Thread(new Producer(), "Producer-2").start(); new Thread(new Consumer(), "Consumer>>>>>1").start(); new Thread(new Consumer(), "Consumer>>>>>2").start(); } }
3.Wait/notify
要点:一把锁与 Synchronized 搭配使用,注意 While 自旋检查队列长度
public class WaitNotifyTest { private static final Object lock = new Object(); private static LinkedList<Integer> queue = new LinkedList<>(); static class Producer implements Runnable { @Override public void run() { while(true) { synchronized (lock) { while (queue.size() == 10) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } queue.offer(1); lock.notifyAll(); } } } } static class Consumer implements Runnable { @Override public void run() { while(true) { synchronized (lock) { while (queue.size() == 0) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } queue.pollLast(); lock.notifyAll(); } } } } public static void main(String[] args) { new Thread(new Producer(),"Producer-1").start(); new Thread(new Producer(),"Producer-2").start(); new Thread(new Consumer(),"Consumer>>>1").start(); new Thread(new Consumer(),"Consumer>>>2").start(); } }
7.为什么多线程会带来性能问题?
什么是性能问题?
表现为响应时间慢,吞吐量低,内存占用过高等
为什么多线程会带来性能问题?
1.调度开销(会发生上下文切换,和可能发生缓存失效)
上下文切换:在实际开发中,线程数远远高于 CPU 核数,为了尽量让每一个线程都得到执行,操作系统会按照调度算法给每一个线程分配时间片,让每一个线程都有机会得到执行。进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的
缓存失效:一旦进行了线程调度,切换到其他线程,CPU 就会去执行不同的代码,原有的缓存就很可能失效了,需要重新缓存新的数据,这也会造成一定的开销,所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数
2.协作开销
线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等
那么什么情况会导致密集的上下文切换?
- 程序频繁地竞争锁,
- IO 读写等原因导致频繁阻塞
8.synchronized
Synchronized 不能使用的锁对象
String 常量,
Integer
Long 等基础数据类型
Synchronized 优化
细化锁,即减小锁的范围
锁对象发生变化,则锁失效
要避免将锁对象发生变化
锁定方法和非锁定方法是可以同时执行的
Synchronized 有锁升级的概念
偏向锁->自旋锁->重量级锁,因此 Synchronized 的性能在某些场景下性能并不比 Atomicxx 这些类差,反而可能更好
这篇关于【JUC】一些线程基础的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-10Rakuten 乐天积分系统从 Cassandra 到 TiDB 的选型与实战
- 2025-01-09CMS内容管理系统是什么?如何选择适合你的平台?
- 2025-01-08CCPM如何缩短项目周期并降低风险?
- 2025-01-08Omnivore 替代品 Readeck 安装与使用教程
- 2025-01-07Cursor 收费太贵?3分钟教你接入超低价 DeepSeek-V3,代码质量逼近 Claude 3.5
- 2025-01-06PingCAP 连续两年入选 Gartner 云数据库管理系统魔力象限“荣誉提及”
- 2025-01-05Easysearch 可搜索快照功能,看这篇就够了
- 2025-01-04BOT+EPC模式在基础设施项目中的应用与优势
- 2025-01-03用LangChain构建会检索和搜索的智能聊天机器人指南
- 2025-01-03图像文字理解,OCR、大模型还是多模态模型?PalliGema2在QLoRA技术上的微调与应用