目录
可见性
volatile
volatile保证内存可见性
volatile不保证原子性
synchronized也可以保证内存可见性
wait和notify
wait ()
notify()
notifyAll()
wait和sleep对比
顺序执行ABC三个线程
单例模式
饿汉模式
懒汉模式
懒汉模式和饿汉模式在多线程环境下调用getInstance,是否线程安全?
如何让懒汉模式线程安全?
模拟阻塞队列中的put和take方法
生产者消费者模型
定时器
定时器会使用在哪些场景
标准库中的定时器
可见性
一个线程对共享变量的修改,可以及时的被其他线程看到。
从JMM角度表述内存可见性问题:
1.线程之间的共享变量存在 主内存 (Main Memory).2.每一个线程都有自己的 " 工作内存 " (Working Memory) .3.当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 .4.当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .5.当t1线程进行读取,t2线程进行修改的时候,先修改工作内存的值,然后再把工作内存的内容同步到主内存中,但是由于编译器的优化,导致t1没有重新从工作内存同步到主内存,读到的结果就是修改之前的结果,工作内存=CPU寄存器+CPU的缓存cache;(CPU读取寄存器比读取内存快得多,因此会在CPU内部引入缓存cache,有的CPU可能没有cache,有的有1个,有的是多个,现在普遍是三级)
volatile
volatile保证内存可见性
代码在写入 volatile 修饰的变量的时候,改变线程工作内存中 volatile 变量副本的值, 将改变后的副本的值从工作内存刷新到主内存;代码在读取 volatile 修饰的变量的时候,从主内存中读取 volatile 变量的最新值到线程的工作内存中, 从工作内存中读取 volatile 变量的副本;
前面我们讨论内存可见性时说了, 直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
这种情况就称为内存可见性问题;也是一种线程安全问题。
✅由此可见:
内存可见性问题,一个线程针对一个变量进行读取操作,同时另一个线程针对变量进行修改,此时读到的值不一定是修改之后的值。这个读线程没有感知到变量的变化。
归根结底就是:jvm或者编译器在多线程优化环境下产生了误判!
✅此时就需要我们手动给flag变量添加volatile关键字(告诉编译器这个变量是易变的,每一次一定要重新读取这个变量的内存内容,指不定啥时候就变了,不能再进行激进的优化了)
编译器是否会优化是个玄学问题,我们最好加上volatile.
✅上述内存可见性 编译器优化 的问题,也并不是始终都会出现的,编译器只是可能会误判
✅其他知识:方法中的变量是存放在栈中的,每一个线程都有自己的内存空间,即使是同一个方法,被不同的线程掉用,这里的局部变量还是会处在不同的栈空间上,本质上还是不通的变量。
volatile不保证原子性
volatile 和 synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见 性.volatile不能处理并发++的情况;
synchronized也可以保证内存可见性
”synchronized 既能保证原子性 , 也能保证内存可见性“,这个观点存疑,没有办法用代码验证。线程的最大问题就是抢占式执行,随机性调度,我们需要控制线程之间的执行顺序,虽然线程在内核的调度都是随机的,但是可以通过一些api让线程主动阻塞,主动放弃cpu。
wait和notify
eg:t1,t2,两个线程,希望t1先干活,干的差不多了,再让t2干,就可以让t2先wait(阻塞,主动放弃cpu),等t1干的差不多了,再通过notify通知t2,唤醒t2,让t2继续干。
上述场景使用join()的话,只能是t1 100%执行完,t2才开始执行。
wait ()
调用wait方法,就会进入阻塞状态,进入waiting状态,这个可以通过interrupt异常唤醒;
wait()方法中不添加任何的参数,就是死等,除非有别的线程唤醒她。
wait 结束等待的条件 :其他线程调用该对象的 notify 方法 .wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本 , 来指定等待时间 ).其他线程调用该等待线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常 .
为什么有这个异常 ?
要理解wait操作是干啥:
1.先释放锁;
2.进行阻塞等待;
3.收到通知后,重新尝试获取锁,并且在获取锁之后,继续向下执行。
就好比是单身(没被加锁)还想着分手(就想释放锁)的事情,
所以wait要搭配synchronized来使用
public class demo44 { public static void main(String[] args) throws InterruptedException { Object object=new Object(); synchronized(object){ System.out.println("wait之前"); object.wait(); System.out.println("wait之后"); } } }
此时就只会打印出“wait之前”
这里虽然wait是阻塞了,阻塞在synchronized代码块里,实际上,这里的阻塞是释放了锁的,此时其他线程是可以获取到object这个对象的锁的~此时这里的阻塞,就处于waiting状态
wait()无参数版本就是死等的;
wait()带参数版本,是指定了等待的最大时间;
notify()
public class demo99 { public static void main(String[] args) throws InterruptedException { Object object=new Object(); Thread t1=new Thread(()->{ //这个线程负责等待; System.out.println("t1:wait之前"); synchronized(object){ try { object.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t2:wait之后"); }); Thread t2=new Thread(()->{ System.out.println("t2notify之前"); synchronized(object){ //notify必须获取到锁才能进行通知; object.notify(); } System.out.println("t2:notify之后"); }); t1.start(); Thread.sleep(500); t2.start(); } }
在代码的最后
写t1.start();t2.start();由于线程调度的不确定性,此时不能保证先执行wait,后执行notify,如果先调用notify,此时没有人wait,此处的wait没法被唤醒的,但是也没啥副作用
notifyAll()
notify 方法只是唤醒某一个等待线程 . 使用 notifyAll 方法可以一次唤醒所有的等待线程 .
wait和sleep对比
wait的待有时间参数的版本看起来和sleep有点像,其实有本质区别的,虽然都是指定等待时间,虽然也都能指定等待时间,虽然也能被提前唤醒,(wait是使用notify唤醒,sleep使用interrupted唤醒)
notify唤醒wait,不会有任何异常;
interrupt唤醒sleep则是出异常了。
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻 塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间 .1. wait 需要搭配 synchronized 使用 . sleep 不需要 .2. wait 是 Object 的方法 ,sleep 是 Thread 的静态方法 .
顺序执行ABC三个线程
public class demo999 {
public static void main(String[] args) {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
System.out.println("A");
synchronized(locker1){
locker1.notify();
}
});
Thread t2=new Thread(()->{
synchronized(locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("B");
synchronized(locker2){
locker2.notify();
}
});
Thread t3=new Thread(()->{
synchronized(locker2){
try {
locker2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
});
t2.start();
t3.start();
t1.start();
/***
* 如果程序先执行t1的notify,后执行t2的wait
*就僵住了
*
* **/
}
}
单例模式
⛅单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.⛅单例模式有很多种,这里介绍 懒汉模式 和 饿汉模式 (饿汉模式;类加载阶段就把实例创建出来了,类加载时比较靠前的 )。饿汉模式
class Singleton{ private static Singleton instanse=new Singleton(); /** * Singleton这个属性与实例无关,而是与类有关,java代码中的每一个类,都会在编译完成后得到.class文件,、 * JVM运行时就会加载这个.class文件读取其中的二进制文件,并且在内存中构造出对应的类对象 * 由于类对象在Java进程中只有唯一一份,因为类对象内部的类属性也是唯一一份; * **/ //如果需要获取这个instanse,就通过这个方法 public static Singleton getInstance(){ return instanse; } //为了避免Singleton被复制出来多份 //把构造方法设为private,在类外就无法通过new 的方式获取Singleton对象了; private Singleton(){ } } public class demo110 { public static void main(String[] args) { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1==s2); } }
1.此时打印结果就是true;2.在这里第二行的static是必要的,(1)static保证这个实例唯一;(2)static保证这个实例在一定时间被创建出来3.如何保证类对象唯一?(1)static 这个操作是让当前的instanse对象变成 类属性,类属性是长在类对象上的,类对象又是唯一实例的,只是在类加载的时候被创建一个实例。(2)构造方法设为private,在类外是无法new的。4.类加载模式是啥?要执行Java程序的前提就是让类先加载出来。
类对象本身与类属性无关,仅仅是因为类里面使用static修饰变量,会作为类属性,也就相当于这个属性对应的内存空间在类对象里面。static与类有关,与实例无关
懒汉模式
class SingletonLazy{ private static SingletonLazy instance=null; public static SingletonLazy getInstance(){ if(instance==null){ instance=new SingletonLazy(); //这个实例并不是类加载的时候就创建,而是在第一次使用的时候才去创建, //如果不用,就不创建了 } return instance; } private SingletonLazy(){}; } public class demo1000 { public static void main(String[] args) { SingletonLazy s1=SingletonLazy.getInstance(); SingletonLazy s2=SingletonLazy.getInstance(); System.out.println(s1==s2); } }
懒汉模式和饿汉模式在多线程环境下调用getInstance,是否线程安全?
饿汉模式里的getInstance里的操作只涉及到了读的操作;懒汉既有读也有写 ;懒汉并不是线程安全的,可能会进行多次new操作
如何让懒汉模式线程安全?
加锁
但是此时还是有问题,内存可见性问题:
加入有很多线程,同时进行getInstance操作,这个时候,是否还会有被优化的风险(只有第一次读使读内存,之后都是读寄存器/cache)。
new操作可能涉及指令重排序问题;
instance=new SingletonLazy()操作可以分成三个步骤:
1.申请内存空间;
2.调用构造方法,把这个内存空初始化成一个合理的对象;
3.把内存空间的地址赋值给instance引用;
正常情况下就是根据123执行,但是编译器还有一手,指令重排序~为了提高程序效率,调整代码执行顺序,顺序就有可能被调整。如123就可能变成132,如果是单线程,123和132就没有本质区别。
但是在多线程情况下,t1线程是按照132的顺序执行,t1执行完13后,执行2的时候被切出cpu由t2来执行,t2拿到的就是一个空对象非法的对象,还没构造完的不完整对象。
解决办法就是加volatile
1.解决内存可见性;
2.解决指令重排序问题;
class SingletonLazy1{
public volatile static SingletonLazy1 instance = null;
public static SingletonLazy1 getInstance(){
if (instance == null) {
synchronized(SingletonLazy1.class){
if(instance==null){
instance=new SingletonLazy1();
}
}
}
return instance;
}
private SingletonLazy1(){};
}
加锁不是说线程就赖在CPU上不走了,而是切换调度正常,但是其他线程尝试枷锁就阻塞;
这里加锁的作用是让原本不是原子的操作变成原子性操作;并且加锁是解决线程安全问题的关键,加锁必须是两个线程同时对同一个线程加锁才会产生线程阻塞;
模拟阻塞队列中的put和take方法
这里是普通队列
public void put(int value){ if(size==items.length){ //队列已经满了 return; } items[tail]=value; tail++; //记得对tail的处理 //第(1)种写法 tail=tail%items.length; //第(2)中写法 if(tail>=items.length){ tail=0; } } //出队列 public Integer take(){ if(size==0){ return null; } int result=items[head]; head++; if(head>=items.length){ head=0; } size--; return result; }
/**自己写阻塞队列; * 此处不考虑泛型,直接使用int代替 * */ /*** * 普通队列的实现加上阻塞功能就变成了阻塞队列,是在多线程环境下的阻塞; * 加上synchronized包裹整个方法体 * */ /** * 但是有一个问题: * 如果他们两个线程的wait同时触发了; * 那显然就不能在正确的唤醒了; * * * **/ class MyBlockingQueue{ private int[] items=new int[200]; private int head=0; private int tail=0; private int size=0; //入队列 public void put(int value) throws InterruptedException { synchronized(this){ while(size==items.length){ //队列已经满了,再放元素,就会产生阻塞 this.wait(); } items[tail]=value; tail++; //记得对tail的处理 //第(1)种写法 tail=tail%items.length; //第(2)中写法 if(tail>=items.length){ tail=0; } this.notify();//唤醒take()里面的wait; } } //出队列 public Integer take() throws InterruptedException { int result=0; synchronized (this){ if(size==0){//队列为空,还要求出队列,出现阻塞; this.wait(); } result=items[head]; head++; if(head>=items.length){ head=0; } size--; this.notify();//唤醒take()里面的阻塞等待; } return result; } }
生产者消费者模型
public static void main(String[] args) { MyBlockingQueue queue=new MyBlockingQueue(); Thread customer=new Thread(()->{ while(true){ try { int result=queue.take(); System.out.println("消费: "+result); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread producer=new Thread(()->{ int count=0; while(true){ try { System.out.println("生产 "+ count ); queue.put(count); count++; Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } count++; } }); }
定时器
定时器会使用在哪些场景
定时器是一种实际开发中非常常用的组件.比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).类似于这样的场景就需要用到定时器.
标准库中的定时器
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello"); } }, 3000); /** *标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule . *schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, *第二个参数指定多长时间之后执行 (单位为毫秒). */
定时器的实现
单独在定时器内部,搞个线程,让线程周期性扫描 ,到时间就执行。 扫描线程也只用扫描优先级队列的第一个元素。
2.一个定时器可以注册N个任务,N个任务会按照最初约定的任务,按照顺序执行;
优先级队列
此处的优先级队列会在多线程环境下使用,使用schedule是一个队列,扫描线程是另一个的队列;
定时器的构成:
1.一个带优先级的阻塞队列为啥要带优先级呢 ?因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的 . 使用带 优先级的队列就可以高效的把这个 delay 最小的任务找出来 .2.队列中的每个元素是一个 Task 对象 .3.Task 中带有一个时间属性.4. 同时有一个 worker 线程一直扫描队首元素 , 看队首元素是否需要执行
Task实现
static class Task implements Comparable<Task> { private Runnable command; private long time; public Task(Runnable command, long time) { this.command = command; // time 中存的是绝对时间, 超过这个时间的任务就应该被执行 this.time = System.currentTimeMillis() + time; } public void run() { command.run(); } @Override public int compareTo(Task o) { // 谁的时间小谁排前面 return (int)(time - o.time); } } }
Task 类用于描述一个任务 ( 作为 Timer 的内部类 ). 里面包含一个 Runnable 对象和一个 time( 毫秒时 间戳)这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口.
完整代码
public class Timer {
static class Task implements Comparable<Task> {
private Runnable command;
private long time;
public Task(Runnable command, long time) {
this.command = command;
// time 中存的是绝对时间, 超过这个时间的任务就应该被执行
this.time = System.currentTimeMillis() + time;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task o) {
// 谁的时间小谁排前面
return (int)(time - o.time);
}
}
// 核心结构
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();
// 存在的意义是避免 worker 线程出现忙等的情况
private Object mailBox = new Object();
class Worker extends Thread{
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
synchronized (mailBox) {
// 指定等待时间 wait
mailBox.wait(task.time - curTime);
}
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
public Timer() {
// 启动 worker 线程
Worker worker = new Worker();
worker.start();
}
// schedule 原意为 "安排"
public void schedule(Runnable command, long after) {
Task task = new Task(command, after);
queue.offer(task);
synchronized (mailBox) {
mailBox.notify();
}
}
// Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象.
//通过 schedule 来往队列中插入一个个 Task 对象.
public static void main(String[] args) {
Timer timer = new Timer();
Runnable command = new Runnable() {
@Override
public void run() {
System.out.println("我来了");
timer.schedule(this, 3000);
}
};
timer.schedule(command, 3000);
}
}
/***
* 当时间没到要执行的时候, CPU就把线程拿起来又放下,进行忙等,
* 所以我们选择“阻塞时等待”,使用wait,更方便唤醒,
* 使用wait等待,更方便随时唤醒,使用wait等待每次有新任务来了(有人调用schedule)
* 就重新检查时间,重新计算要等待的时间,并且wait也停工了一个带有:超过时间的版本
* */
/**
*堆的take:出堆顶元素 ;底层操作
* 交换堆顶元素和最后一个元素,进行向下调整
*堆的put:入,
* 先放在最后一个元素的位置,然后进行向上调整
* */
一个极端情况