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 这些类差,反而可能更好
原文:https://www.cnblogs.com/zhangyibing/p/15221094.html