volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器

目录

可见性

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 , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第1张图片 这种情况就称为内存可见性问题;也是一种线程安全问题。

 volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第2张图片

✅由此可见:

内存可见性问题,一个线程针对一个变量进行读取操作,同时另一个线程针对变量进行修改,此时读到的值不一定是修改之后的值。这个读线程没有感知到变量的变化

归根结底就是:jvm或者编译器在多线程优化环境下产生了误判

此时就需要我们手动给flag变量添加volatile关键字(告诉编译器这个变量是易变的,每一次一定要重新读取这个变量的内存内容,指不定啥时候就变了,不能再进行激进的优化了

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第3张图片

编译器是否会优化是个玄学问题,我们最好加上volatile.

✅上述内存可见性 编译器优化 的问题,也并不是始终都会出现的,编译器只是可能会误判

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第4张图片

✅其他知识:方法中的变量是存放在栈中的,每一个线程都有自己的内存空间,即使是同一个方法,被不同的线程掉用,这里的局部变量还是会处在不同的栈空间上,本质上还是不通的变量。 

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 异常 .

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第5张图片

为什么有这个异常 ?

要理解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
         *就僵住了
         *
         * **/
    }

}

单例模式

⛅单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
⛅单例模式有很多种,这里介绍 懒汉模式 和 饿汉模式 (饿汉模式;类加载阶段就把实例创建出来了,类加载时比较靠前的  )。
volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第6张图片

饿汉模式

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第7张图片

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.类加载模式是啥?
volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第8张图片

  要执行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操作

如何让懒汉模式线程安全?

加锁volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第9张图片

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第10张图片

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第11张图片

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第12张图片

 但是此时还是有问题,内存可见性问题:

加入有很多线程,同时进行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.解决指令重排序问题;

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第13张图片

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(){};

}

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第14张图片

 加锁不是说线程就赖在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;
    }

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第15张图片

/**自己写阻塞队列;
 * 此处不考虑泛型,直接使用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 {
        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 {
        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 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:入,
 * 先放在最后一个元素的位置,然后进行向上调整 
 * */

一个极端情况

volatile,wait和notify,懒汉模式和饿汉模式,阻塞式队列,定时器_第16张图片 

 

你可能感兴趣的:(JavaEE,Java,java,jvm,开发语言)