Java 内存模型简称为 JMM(Java Memory Model),是和多线程相关的一组规范,需要各个 JVM 来遵守实现
有了 JMM 就可以让程序在 windows 和 Linux 上有一样的执行效果,即屏蔽了底层的差异,实现 Write Once,Run Anywhere !,并且解决了 CPU 多级缓存、处理器优化、指令重排等导致的结果不可预期的问题。
编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是重排序
提高整体的运行速度
线程间对于共享变量的可见性问题,是由我们刚才讲到的这些 L3 缓存、L2 缓存、L1 缓存,也就是多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样,后续对于数据的修改也是先写入到自己的 L1 缓存中,然后等待时机再逐层往下同步,直到最终刷回内存。
主内存和工作内存的关系
JMM 有以下规定:
volatile 是 Java 中的一个关键字,是一种同步机制,它可以保证共享变量的可见性(禁用 CPU 缓存),也可以禁止指令重排序,保障有序性
synchronized 是由 CPU 原语层面支持的锁机制,既复合 happens-before 规则保证了可见性,也保证了操作的原子性
1.线程切换带来的原子性问题-->使用同步锁
2.多核 CPU 带来的缓存可见性问题-->利用好 happen-before 原则
3.编译优化带来的指令重排序问题--->使用 volatile
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(); } }
1.继承 Thread 类
2.实现 Runnable 接口(推荐,主要是因为有利于类的扩展)
3.使用 lambda 表达式
4.使用线程池,让一个线程启动
但是 interrupt 仅仅起到通知线程停止的作用,线程可以选择停止,也可以选择不停止
Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作
正确的场景
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); } }
临时暂停执行,再次回到 CPU 时间竞争队列中,等待 CPU 分配时间片
//在指定对象上加锁 synchronized doSomething1() { count++; } /** * 锁定当前对象 * */ synchronized doSomething2() { count++; doSomething1(); }
源代码注释:
英文部分的意思是说,在使用 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 的唤醒
相同点
不同点
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(); }
要点:使用可重入锁,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(); } }
要点:一把锁与 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(); } }
表现为响应时间慢,吞吐量低,内存占用过高等
1.调度开销(会发生上下文切换,和可能发生缓存失效)
上下文切换:在实际开发中,线程数远远高于 CPU 核数,为了尽量让每一个线程都得到执行,操作系统会按照调度算法给每一个线程分配时间片,让每一个线程都有机会得到执行。进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的
缓存失效:一旦进行了线程调度,切换到其他线程,CPU 就会去执行不同的代码,原有的缓存就很可能失效了,需要重新缓存新的数据,这也会造成一定的开销,所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数
2.协作开销
线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等
String 常量,
Integer
Long 等基础数据类型
细化锁,即减小锁的范围
要避免将锁对象发生变化
偏向锁->自旋锁->重量级锁,因此 Synchronized 的性能在某些场景下性能并不比 Atomicxx 这些类差,反而可能更好