• 一篇博客带你学会JUC并发编程(后端必会、五万字笔记)


    并发编程

    因为我自己学过几遍了,同时操作系统学校教的也不错,所以一些理论性的东西就记得很简洁通俗,适合有基础的人看。
    不想多写那么多难以理解的东西,尽量用白话去解释。
    标签太多了,就不在文章头部放标签了,可以在左侧自行点击目录查看所需。

    进程、线程

    对比

    简单来说,就像一个软件启动就是一个进程,线程就是软件中的不同功能。

    • 线程是调度的最小单位,进程则是用于资源分配的最小单位。

    • 进程拥有共享资源,如内存空间,线程在进程的内部,所以通过其拥有的内存,即可进行通信。不同的计算机上的进程需要通过协议通信,如http。

    异步

    简单来说,就是不需要等待返回结果。
    就比网络爬虫,你发送一个请求,它需要很久才能返回回来,难道你在等待的时候什么时候都不做就会照成cpu浪费,效率也低。这时候就可以开个新线程处理其他事情。

    当然,不是什么适合都开线程合适,线程切换也是有开销的。
    同时,单核下,线程切换只是为了让其他进程可以执行,多核下跑的多线程才是我们平时认知,提升效率。

    多线程 三种 实现方法

    继承 Thread 的方法

    1. 自定义类继承 Thread 重写run方法
    2. new子类,然后调用 start() 方法,启动线程
    3. 对象setName(“线程1”) 可以设置线程名字,在类中可以用getName() 来获取

    例:

    package com.study;
    // 自定义类继承 Thread
    public class MyThread extends Thread{
    
    	// 重写run方法
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ": helloworld");
            }
        }
    }
    
    package com.study;
    
    public class ThreadDemo {
        public static void main(String[] args) {
    
    		// new子类
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    		// 设置线程名字
            t1.setName("线程1");
            t2.setName("线程2");
    
    		// 调用 start() 方法,启动线程
            t1.start();
            t2.start();
        }
    }
    

    运行截图:
    在这里插入图片描述

    实现 Runnable接口 的方法(推荐使用)

    1. 定义一个类实现Runnable接口
    2. 重写run方法
    3. 创建自己的类对象
    4. 创建Thread对象,开启线程

    注意:也可以setName,但是要记住,这种方法,类中不能直接使用getName方法,因为并没有继承Thread,自然类中没有此方法。但是可以在类中获取当前正在运行线程的对象,来获取name。

    例子:

    package com.study.threaddemo2;
    // 定义一个类实现Runnable接口
    public class MyRun implements Runnable{
    
    	// 重写run方法
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                Thread h = Thread.currentThread(); // 获取当前线程的对象
                System.out.println(h.getName() + ":helloworld");
            }
        }
    }
    
    package com.study.threaddemo2;
    
    public class Test2 {
        public static void main(String[] args) {
    
    		// 创建自己的类对象 
            MyRun r1 = new MyRun();
            MyRun r2 = new MyRun();
    
    		// 创建Thread对象
            Thread t1 = new Thread(r1);
            Thread t2 = new Thread(r2);
    		// 设置线程名字
            t1.setName("线程1");
            t2.setName("线程2");
    		// 开启线程
            t1.start();
            t2.start();
        }
    }
    

    运行截图:在这里插入图片描述

    runnable原理

    和thread内部一致,如果从传入了able,就会赋值给Thread中的target变量,执行之前会判断,如果不为空,就执行able的run方法。

    在这里插入图片描述

    实现 Callable接口 并利用 FutureTask类 来接收返回值 的方法

    1. 实现Callable接口
    2. 重写call方法,有返回值
    3. 创建自己类的对象
    4. 创建FutureTask对象(管理多线程运行结果)
    5. 创建Thread类对象且启动

    注意:Callable泛型,填入返回的类型。
    例子:

    package com.study.threaddemo3;
    
    import java.util.concurrent.Callable;
    // 实现Callable接口
    public class MyCallable implements Callable<Integer> {
    
    	// 重写call方法
        public Integer call() {
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
    
    package com.study.threaddemo3;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class Test3 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    		
    		// 创建自己类的对象
            MyCallable mc = new MyCallable();
    
    		// 创建FutureTask对象
            FutureTask<Integer> ft = new FutureTask<>(mc);
    
    		// 创建Thread类对象且启动
            Thread t1 = new Thread(ft);	
            t1.start();
    
    		// 获取返回值
            Integer res = ft.get();
            System.out.println(res);
        }
    }
    

    运行结果:
    在这里插入图片描述

    我的理解 和 总结

    继承Thread类 的方法,实际就是自己创建了一个流水线,并同时创建了任务,也就是流水线运输和处理的内容。 自然我们可以在流水线中知道此流水线的名字。

    实现Runnable接口 的方法,实际是创建了一个任务,我们还需要创建流水线,去运行此任务。 我们不能根据任务就知道运行此任务的流水线名字,因为可能有多个流水线执行此任务,所以我们要先获取正在执行此任务的流水线,这样就能知道正在运行的流水线名了。

    实现Callable接口 的方法,此方法就可以用FutureTask这个员工来获得流水线处理任务后的结果(返回值),弥补了上面两种方法的不足。

    这样解释是不是很好理解呢?当然这只是通俗的解释一下,方便理解,具体原理肯定还是要看代码的。

    可以根据每种方法的优缺点来进行选择使用。
    在这里插入图片描述

    Thread 中的各种方法

    1. setName() 设置名字 getName() 获取名字
      默认是Thread-序号,Thread本身构造方法也可传入名字来设置,可以在继承它的子类构造方法中利用super来调用。
    2. static Thread currentThread() 静态的,获取当前正在执行的线程的对象
      jvm虚拟机启动后,会自动调用多个线程,其中就包含main来执行main方法中的代码。
    3. static void sleep(long time) 哪条线程执行到此方法,就会停留对应时间,单位毫秒ms
      睡眠过后对于线程或被自动唤醒。

    1、2两个方法在介绍线程创建时已经展示过,这里展示一下sleep方法的使用。

    package com.study.threaddemo4;
    
    public class Test4 {
        public static void main(String[] args) throws InterruptedException {
    
            System.out.println("开始");
            Thread.sleep(5000);
            System.out.println("结束");
        }
    }
    

    运行结果:
    “开始” 打印后,main线程睡眠5s, 然后自动唤醒后,打印"结束"。
    在这里插入图片描述

    内部细节

    线程栈之间是相互独立的,堆内的数据是共享的。

    lambda 写法

    Thread t = new Thread(() -> {
        log.debug("running");
    }, "tname");
    
    t.start();
    

    接口上只有一个方法时时,上面会有这个注解,就可以用lamdba。
    在这里插入图片描述

    线程运行

    进程的交替运行,我们控制不了,任何一个进程都有可能将运行。

    查看和杀死进程

    windows

    tasklist 查看进程 
    taskkill 杀死进程 
    jps 查看Java进程
    

    linux:

    ps -fe 查看所有进程 
    ps -fe | grep java 查看java进程
    ps -fT -p  查看某个进程(PID)的所有线程 
    kill 杀死进程 
    top 动态查看进程信息
    top 按大写 H 切换是否显示线程 
    top -H -p  查看某个进程(PID)的所有线程信息 
    

    在这里插入图片描述

    java自带的:

    jps 查看java进程信息
    jstack  查看该进程的全部线程信息,不过只是那一刻的  
    

    jconsole 连接远程进程查看

    先上传一个java程序。
    在这里插入图片描述

    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类
    

    赋值这段,然后把地址等换成自己的(去掉引号),设置端口号,是否安全连接或认证自己用false即可。

    java -Djava.rmi.server.hostname=172.18.0.1 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=12345 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false st1
    

    打开自己设置的防火墙,把设置的端口号打开。

    firewall-cmd --add-port=12345/tcp --permanent
    firewall-cmd --reload
    systemctl restart firewalld.service
    

    云服务,记得去控制台也开启。

    打开jconsole

    在这里插入图片描述

    离谱,就是连不上,不知道什么原因,暂时不弄了。

    线程运行原理

    栈帧

    • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

    所以一个栈对应一个线程,随机调用。

    内存图

    在这里插入图片描述

    上下文切换

    因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的:

    • 线程的 cpu 时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

    当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

    • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
    • Context Switch 频繁发生会影响性能

    sleep 与 yield

    sleep

    • 调用 sleep 会让当前线程从 Running进入 Timed Waiting 状态(阻塞)
    • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
    • 睡眠结束后的线程未必会立刻得到执行
    • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

    yield

    • 调用 yield 会让当前线程从 Running 进入 Runnable就绪状态,然后调度执行其它线程
    • 具体的实现依赖于操作系统的任务调度器
      线程优先级
    • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
    • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

    线程优先级

    • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
    • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

    打断标记

    睡眠中的线程被打断时,会抛出异常,把打断标记置为false,而不是变为true,wait和join也是。

    打断 sleep,wait,join 的线程
    这几个方法都会让线程进入阻塞状态
    打断 sleep 的线程, 会清空打断状态,以 sleep 为例。

    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "c.Test1")
    public class st2 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                log.debug("sleep..");
                try {
                    Thread.sleep(1000); // wait join
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            log.debug("打断");
            t1.interrupt();
            System.out.println("打断标记=" + t1.isInterrupted());
        }
    }
    

    在这里插入图片描述

    而打断正常线程时,其实并没有打断,只是改变打断标记,线程还会继续运行,需要我们手动利用打断标记的布尔值去判断进行打断。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        log.debug("打断标记判断为真,退出循环");
                        break;
                    }
                }
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            log.debug("打断");
            t1.interrupt();
        }
    }
    

    在这里插入图片描述

    两阶段终止

    在了解完打断后,我们可以知道,打断正常和阻塞中的线程时,情况是不一样的,想要利用打断标记停止一个线程,要考虑两种情况,正常就不用说了,在睡眠时,被打断后,打断标记重置为false,所以在异常处理中,再打断一次,即可改变打断标记,随后的循环中结束线程。

    在这里插入图片描述

    package com.leran;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            TwoInt twoInt = new TwoInt();
            twoInt.start();
            Thread.sleep(3000);
            twoInt.stop();
        }
    }
    
    @Slf4j(topic = "c.TwoInt")
    class TwoInt {
        private Thread t1;
        public void start() {
            t1 = new Thread(() -> {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        log.debug("被打断");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                        log.debug("监控");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        Thread.currentThread().interrupt(); // 重新打断一次,在正常运行状态下
                    }
                }
            }, "t1");
            t1.start();
        }
    
        public void stop(){
            t1.interrupt();
        }
    }
    

    在这里插入图片描述

    注意

    isInterrupted() 判断是否打断不会清空打断标记。
    interrupted() 是静态方法,和上面功能一样,但是会清空打断标记。

    打断park线程

    park是LockSupport类中的静态方法,用于暂停当前线程,处于wait状态。

    只有打断标记为false的线程才能被park。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                log.debug("park");
                LockSupport.park();
                log.debug("unpark");
                log.debug("打断标记=" + Thread.currentThread().isInterrupted()); // true;
    
                LockSupport.park(); // true  无法park
                log.debug("unpark"); // 直接会运行这个
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            t1.interrupt(); // 打断
        }
    }
    

    在这里插入图片描述

    看起来和打断正常运行的线程差不多。

    第二个park未执行,因为打断标记为true,可以用Thread.interrupted()获取后重置标记。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                log.debug("park");
                LockSupport.park();
                log.debug("unpark");
                log.debug("打断标记=" + Thread.interrupted()); // true ,然后重置为false
    
                LockSupport.park(); // 可以park
                log.debug("unpark"); // 直接会运行这个
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            t1.interrupt(); // 打断
        }
    }
    

    在这里插入图片描述

    过时方法

    stop()
    suspend()
    resume()

    已过时,不建议使用,容易造成线程死锁。

    守护线程

    Java 进程需要等待所有线程都运行结束,才会结束。

    有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

    import java.util.concurrent.locks.LockSupport;
    
    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                while(true) {
                    log.debug("t1正在运行");
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t1");
    
            t1.setDaemon(true);
            t1.start();
            Thread.sleep(1);
            log.debug("主线程运行完成");
        }
    }
    

    在主线程运行完后,t1作为守护线程随之结束。
    在这里插入图片描述

    线程状态

    • 新建:线程实例创建
    • 就绪:调用start方法
    • 运行:被CPU选中运行
    • 阻塞:线程竞争锁失败进入阻塞
    • 等待:使用sleep或者wait方法
    • 有限等待:使用sleep方法并设置了时间
    • 销毁:线程执行完销毁

    在这里插入图片描述

    多线程的安全问题

    学过操作系统应该都知道,分时系统,线程切换造成的安全问题,这里就不再赘述,大家都知道。

    java例子:

    @Slf4j(topic = "c.Test3")
    public class st3 {
        static int count = 0;
        public static void main(String[] args) throws InterruptedException {
    
            Thread t1 = new Thread(() -> {
                    for (int i = 0; i < 15000; i++) {
                        count++;
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 15000; i++) {
                    count--;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug(String.valueOf(count));
        }
    }
    

    在这里插入图片描述

    正常情况下,一个++,一个–,应该答案为0,但是由于线程切换,造成了读写不一致问题。

    synchronized

    synchronized俗称对象锁,只有获取了锁的线程才能执行被锁住的代码。

    有且只有一个线程能获取对象锁。既可以让线程之间互斥访问被锁住的对象。

    即使线程切换,没有锁也运行不了临界区中的代码,这样就可以保证读写的原子性,那么就一定正确。

    代码示例

    @Slf4j(topic = "c.Test3")
    public class st3 {
        static int count = 0;
        static Object lock = new Object();
        
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock) {
                        count++;
                    }
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 5000; i++) {
                    synchronized (lock){
                        count--;
                    }
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}",count);
        }
    }
    

    在这里插入图片描述

    面向对象改进:

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
            Lock lock = new Lock();
            Thread t1 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    lock.incr();
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    lock.decr();
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            log.debug("count: {}" , lock.getCount());
        }
    
    }
    
    class Lock{
        private int count = 0;
    
        public void incr() {
            synchronized(this) {
                count++;
            }
        }
    
        public void decr() {
            synchronized (this) {
                count--;
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    

    在这里插入图片描述

    一样的只不过,在类方法里调用,锁的是当前对象this。

    方法上加synchronized

    加在普通方法上是对象锁,加载静态方法上是类锁。
    而不加锁的方法不受任何影响。

    class Test{
        public synchronized void test() {
    
        }
    }
    等价于
    class Test{
        public void test() {
            synchronized(this) {
    
            }
        }
    }
    
    class Test{
        public synchronized static void test() {
            
        }
    }
    等价于
    class Test{
        public static void test() {
            synchronized(Test.class) {
    
            }
        }
    }
    

    注意:不要有误区,不要认为类锁被锁后其类对应的所有对象锁也不能再执行,类锁锁住的是类对象,也是一个对象而已,也就是Class c = xx.getClass()这个对象。

    所以我认为没必要区分什么锁类锁对象啥的,都是锁一个对象。只不过一个锁的该类的对象,一个锁的该类的Class对象。不过我们通常将锁一个类叫做类锁而已。

    线程八锁

    "线程八锁"通常是指Java中关于多线程同步的八种情况,这些情况是基于对象锁的不同组合而产生的。这些情况包括对实例方法和静态方法的访问,以及对普通对象和Class对象的访问。
    对于非静态同步方法,锁的是当前实例对象。
    对于静态同步方法,锁的是当前类的Class对象。
    对于同步方法块,锁的是括号里配置的对象。

    下面我们就来全面解析吧。

    No.1

    class Number{
        public synchronized void a() {
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
    }
    

    a、b锁对象,互斥,执行顺序随机。
    答案:
    1、2 或
    2、1

    No.2

    class Number{
        public synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
    }
    

    a、b锁对象、互斥,执行顺序随机。
    答案:1s、1、2 或
    2、1s、1

    No.3

    class Number{
        public synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
        public void c() {
            log.debug("3");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
        new Thread(()->{ n1.c(); }).start();
    }
    

    a、b锁对象,c普通方法,同一对象的a、b互斥、普通方法不受影响,随时可以执行。
    答案:3、2、1s、1 或
    3、1s、1、2 或
    2、3、1s、1

    No.4

    class Number{
        public synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
    }
    

    a、b锁对象,但是调用对象不同,互不影响。
    答案:
    2、1s、1

    No.5

    class Number{
        public static synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
    }
    

    a锁对象、b锁类(Class对象),互不影响。
    答案:
    2、1s、1

    No.6

    class Number{
        public static synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public static synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n1.b(); }).start();
    }
    

    a、b锁类,互斥,执行顺序随机。
    答案:
    1s、1、2 或
    2、1s、1

    No.7

    class Number{
        public static synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
    }
    

    a锁类、b锁对象,但是调用对象和方法都不同,互不影响。
    答案:
    2、1s、1

    No.8

    class Number{
        public static synchronized void a() {
            sleep(1);
            log.debug("1");
        }
        public static synchronized void b() {
            log.debug("2");
        }
    }
    
    public static void main(String[] args) {
        Number n1 = new Number();
        Number n2 = new Number();
        new Thread(()->{ n1.a(); }).start();
        new Thread(()->{ n2.b(); }).start();
    }
    

    a、b都是锁类、虽然是不同对象调用,但是他俩锁的类,互斥,执行顺序随机。
    答案:
    1s、1、2
    2、1s、1

    线程安全问题

    分析

    局部变量是安全的,成员变量并不安全。

    切记,方法是拷贝执行的,方法内部的局部的变量并不共享,所以是安全的,但如果方法调用成员变量,成员变量在堆中,操作的是同一个,这就会有安全问题。

    也就是说,一个变量只被一个线程调用,是安全的。

    list不安全的(成员):

    class ThreadUnsafe {
        ArrayList<String> list = new ArrayList<>();
        public void method1(int loopNumber) {
            for (int i = 0; i < loopNumber; i++) {
                // { 临界区, 会产生竞态条件
                method2();
                method3();
                // } 临界区
            }
        }
        private void method2() {
            list.add("1");
        }
        private void method3() {
            list.remove(0);
        }
    }
    

    list安全的(局部):

    class ThreadSafe {
        public final void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
        private void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    

    不过不是绝对的,如果子类继承并重写的方法3,那么局部变量被暴露给了其他线程,就会出现安全问题。

    所以private和final的作用的体现出来了。

    class ThreadSafe {
        public final void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
        private void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    
    class ThreadSafeSubClass extends ThreadSafe{
        @Override
        public void method3(ArrayList<String> list) {
            new Thread(() -> {
                list.remove(0);
            }).start();
        }
    }
    

    常见的线程安全类

    • String
    • Integer
    • StringBufffer
    • Random
    • Vector
    • Hashtable

    多个线程调用同一实例下,这些类是安全的,因为其方法都是原子性的。
    不过多个方法组合并不安全。

    线程安全类方法组合

    并不安全
    例如:

    Hashtable table = new Hashtable();
    // 线程1,线程2
    if( table.get("key") == null) {
        table.put("key", value);
    }
    

    很明显,两个线程如果都读取完null切换线程,那么就会put。

    不可变类型

    像String,Integer是不可变的,肯定是安全的。
    他内部方法源码都是创建一个新对象返回,所以看起来像我们修改了一样。

    例如这个subString方法,如果不是截取全部,返回一个新对象。

    在这里插入图片描述

    所以那么加final的类一定安全么?

    显然是不一定的,final对应引用类型只是保证其指向不变,但其内部的属性还是可以改变的,所以不安全。

    Monitor

    java对象头

    普通对象
    在这里插入图片描述

    Mark Word 主要用来存储对象自身的运行时数据、klass word就是指向该对象的类型。

    数组对象
    在这里插入图片描述

    mark word
    在这里插入图片描述

    不同对象状态下结构和含义不同。

    Monitor(锁、管程)

    在这里插入图片描述

    • 当synchronized锁一个对象(重量级锁)时会关联一个操作系统的Monitor对象,对象头中markword中ptr_to_heavyweight_montior就会指向对应monitor对象。

    • 同时将Monitor中的owner给对应线程,表示该锁现在对应线程拥有。

    • 如果现在有其他线程尝试加锁,会发现对象指向的Monitor中的owner并不是该线程,就会让该线程进入等待队列,线程阻塞。

    • 如果该锁被释放,那么等待队列中的线程将被唤醒重写争取锁。

    锁的类型

    重量级锁、轻量级锁、偏向锁。
    加锁过程:偏向->轻量级->重量级

    轻量级锁

    轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

    轻量级锁对使用者是透明的,即语法仍然是 synchronized,锁的升级什么的不用我们考虑。

    static final Object obj = new Object();
    
    public static void method1() {
        synchronized( obj ) {
            // 同步块 A
            method2();
        }
    }
    
    public static void method2() {
        synchronized( obj ) {
            // 同步块 B
        }
    }
    

    每一个线程的栈帧中都会包含一个锁记录结构,记录对象头中的markword。

    在这里插入图片描述

    锁记录中Object reference指向锁对象,并尝试用 cas 交换 Object 的 Mark Word,将Mark Word的值存入锁记录中。

    在这里插入图片描述

    若cas成功,对象头中存储锁地址和锁状态00(轻量级),表示该线程给此对象加锁。

    在这里插入图片描述

    如果 cas 失败,有两种情况:

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

    在这里插入图片描述

    当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

    在这里插入图片描述

    当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

    • 成功,则解锁成功。
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

    重量级锁

    如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时会进行锁膨胀,将轻量级锁膨胀为重量级锁。

    static Object obj = new Object();
    public static void method1() {
        synchronized( obj ) {
            // 同步块
        }
    }
    

    当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。

    在这里插入图片描述

    这时Thread-1就会CAS失败,进入锁膨胀过程。

    • Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED
    • Object 对象头锁标记变为10(重量级)

    在这里插入图片描述

    当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。

    简单来说,我们可以简化一下:

    • 就是没竞争时,自己正常加锁运行解锁即可,同一线程有锁的重入,防止没必要的加锁解锁过程。
    • 如果有竞争时,新线程进入阻塞队列等待,当解锁时,唤醒阻塞队列中的线程。

    自旋优化

    重量级锁竞争的时候,可以使用自旋(不断的循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(当前持有锁的线程释放了锁),这时当前线程就可以避免阻塞。(阻塞再恢复,会进行上下文切换,耗费性能)

    但是自旋也是会消耗性能的,尤其是进程非常多的时候,一堆进程在不断循环等待锁被释放,优点就是及时,一释放锁就能感应到。

    偏向锁

    在锁重入时,会产生锁记录,每次都需要尝试CAS,虽然会失败,但是CAS这个操作是没必要的。

    优化:

    将对象头中记录持有该锁的线程id,如果发生锁的重入,检测对象头,若是自己的,那么就不用再CAS了。

    对比:

    在这里插入图片描述

    在这里插入图片描述

    偏向锁的细节

    在这里插入图片描述

    一个对象创建时:

    • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0。

    • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。

    • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。

    • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成。

    • 调用了 hashCode() 后会撤销该对象的偏向锁。

    从图中也可以看出,偏向锁的markword没有位置可以放hashcode了,除非转变为轻量级锁,所以调用对象的hashCode会撤销他的偏向锁。

    偏向锁的撤销

    除了上面说的调用hashCode,也可用 让其他线程使用偏向锁,这时偏向锁会升级为普通锁。

    不过两个线程需要错开加锁,不然会升级为重量级锁。

    批量重偏向

    可用发现,就算错开没用竞争,那么锁也会从偏向锁转变为普通锁,那其实锁的转变也是消耗性能的,所以就有了偏向锁的重偏向这个功能。

    如果对象被多个线程访问,但没有竞争(错开),这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 为 T2线程。

    当某对象(这一类的不同对象)撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会在给这些对象加锁的时候重新偏向至新加锁线程。

    批量撤销

    当相同类型不同对象撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向,这个锁竞争太激烈了。于是整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的(轻量级)。

    锁消除

    简单来说,当jvm调用某方法达到一定阈值,就会用记时编译器优化,发现锁加和不加没有影响时,就会去除掉锁。

    public class MyBenchmark {
        static int x = 0;
       
        public void a() throws Exception {
            x++;
        }
        
        public void b() throws Exception {
            Object o = new Object();
            synchronized (o) {
                x++;
            }
        }
    }
    

    比如Object为局部变量,根本不会逃离此方法作用范围,不能被共享,那加不加锁没啥意义,jvm就会去除掉锁去运行,从而优化性能。

    wait/notify

    这块比较简单,就不在把所有例子都写上了。

    在这里插入图片描述

    要注意区分waitSet和EntryList中的线程,一个获得了锁但是wait释放了锁进入等待notify唤醒状态,一个是正在等待获得锁。

    不过相同点就是他们都处于阻塞状态,一个是等待阻塞,一个是同步阻塞。 不占用时间片。

    • BLOCKED 线程会在 Owner 线程释放锁时唤醒
    • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

    所以只有获取了锁的线程才能wait,如果我们不手动notify,将永远沉睡下去。

    api

    • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
    • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
    • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

    wait可用传入参数,表示等待时间ms,如果到了时间没有被唤醒,那么主动唤醒自己。

    它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。

    wait vs sleep

    sleep(long n) 和 wait(long n) 的区别

    1. sleep 是 Thread 方法,而 wait 是 Object 的方法
    2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
    3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
    4. 它们状态 TIMED_WAITING

    手写消息队列

    操作系统的生产者-消费者模式而已。
    生产者:不断判断队列中是否有空余位置,若没有进入等待,若有放入一个产品到队列,然后唤醒消费者消费。

    消费者:不断判断队列中是否有产品,若没有进入等待,若有则拿走一个产品,然后唤醒生产者生产品。

    package com.消息队列;
    
    import java.util.LinkedList;
    
    public class Test {
    
    
    }
    
    class MessageQueue {
    
        private final int capacity; // 最大容量
        private final LinkedList<Message> dq = new LinkedList<>(); // 存放消息的队列
    
        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }
    
        /**
         * 队列中取出产品
         * @return
         */
        public Message take(){
            synchronized (dq) {
                while (dq.isEmpty()) {
                    try {
                        dq.wait(); // 若为空,一直等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Message first = dq.getFirst(); // 获取产品
                dq.notifyAll(); // 唤醒等待dq锁的线程
                return first;
            }
        }
    
        /**
         * 队列中放入产品
         * @param message
         */
        public void put(Message message){
            synchronized (dq) {
                while (dq.size() >= capacity) {
                    try {
                        dq.wait(); // 若无容量了,该线程进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                dq.addLast(message); // 放入产品
                dq.notifyAll(); // 唤醒等待dq锁的线程
            }
        }
    }
    
    final class Message {
        private final int id; // id
        private final Object object; // 任务
    
        public Message(int id, Object object) {
            this.id = id;
            this.object = object;
        }
    
        public int getId() {
            return id;
        }
    
        public Object getObject() {
            return object;
        }
    
        @Override
        public String toString() {
            return "Message{" +
                    "id=" + id +
                    ", object=" + object +
                    '}';
        }
    }
    

    sync已经保证了对于队列操作的互斥,所以其实不wait也可以,一直循环等到线程切换皆可,但是太浪费cpu了。

    生产者和消费者都是等待的dq的锁,所以notifyAll的时候,唤醒所有线程,被选中执行的不一定是哪一个,不过如果while条件不符,还是会再wait罢了。

    park / unpark

    用法

    // 暂停当前线程
    LockSupport.park(); 
    // 恢复某个线程的运行
    LockSupport.unpark(暂停线程对象)
    

    **先说结论:**无论unpark在park前还是后,都可以解除暂停状态。

    先park在unpark可以成功运行:

            Thread t1 = new Thread(() -> {
                log.debug("start...");
                sleep(1);
                log.debug("park...");
                LockSupport.park();
                log.debug("resume...");
            },"t1");
            t1.start();
    
            sleep(2);
            log.debug("unpark...");
            LockSupport.unpark(t1);
    

    在这里插入图片描述

    先unpark再park也可以成功运行:

            Thread t1 = new Thread(() -> {
                log.debug("start...");
                try {
                    sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("park...");
                LockSupport.park();
                log.debug("resume...");
            },"t1");
            t1.start();
    
            sleep(1);
            log.debug("unpark...");
            LockSupport.unpark(t1);
    

    在这里插入图片描述

    与wait/notify区别

    特点 与 Object 的 wait & notify 相比

    • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用(也就是先获取对象的锁),而 park,unpark 不必。
    • park & unpark 是以线程为单位来【阻塞】和【唤醒(指定)】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】。
    • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。

    在这里插入图片描述

    如果先unpark,在park之前,counter会先+1,把unpark存储,等park的时候再使用。当然只能存储一次。

    线程活跃性

    死锁

    两个线程相互等待对方已拥有的锁,就会相互一直等待,不会停止。

    t1拥有a锁,等待b锁。
    t2拥有b锁,等待a锁。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            Object A = new Object();
            Object B = new Object();
    
            Thread t1 = new Thread(() -> {
                synchronized (A) {
                    log.debug("lock A");
                    try {
                        sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        log.debug("lock B");
                        log.debug("操作...");
                    }
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                synchronized (B) {
                    log.debug("lock B");
                    try {
                        sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (A) {
                        log.debug("lock A");
                        log.debug("操作...");
                    }
                }
            }, "t2");
    
            t1.start();
            t2.start();
        }
    }
    

    活锁

    相互改变对方的结束条件,两个线程永远无法结束。

    count初始为10,
    t1希望count减到0结束。
    t2希望count加到20结束。
    两个一起同速度执行,一定不能结束。

    public class Test3{
        static volatile int count = 10;
        static final Object lock = new Object();
        
        public static void main(String[] args) {
            new Thread(() -> {
                // 期望减到 0 退出循环
                while (count > 0) {
                    sleep(0.2);
                    count--;
                    log.debug("count: {}", count);
                }
            }, "t1").start();
            
            new Thread(() -> {
                // 期望超过 20 退出循环
                while (count < 20) {
                    sleep(0.2);
                    count++;
                    log.debug("count: {}", count);
                }
            }, "t2").start();
            
        }
    }
    

    可以设置两个线程随机的睡眠时间解决。

    解饿

    线程一直无法获得cpu的调度,我么就称为线程饥饿。
    可能原因为线程优先级太低。

    ReentrantLock

    翻译:可重入锁

    特点

    • 可中断
    • 可设置超时时间(不会一直等待锁)
    • 可设置为公平锁(防止线程饥饿)
    • 支持多个条件变量
    • 与 synchronized 一样可重入

    基本语法

    // 获取锁
    reentrantLock.lock();
    try {
        // 临界区
    } finally {
        // 释放锁
        reentrantLock.unlock();
    }
    

    可重入

    同一个线程如果已经拥有了锁,可以再次获得这把锁。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        private static ReentrantLock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
    
            lock.lock(); // 加锁
            try {
                log.debug("enter m1");
                m1();
            }finally {
                lock.unlock(); // 解锁
            }
        }
    
        public static void m1 () {
            lock.lock();
            try {
                log.debug("enter m2");
                m2();
            } finally {
                lock.unlock();
            }
        }
    
        public static void m2 () {
            lock.lock();
            try {
                log.debug("m2获取了锁");
            } finally {
                lock.unlock();
            }
        }
    }
    

    在这里插入图片描述

    可打断(避免死等、被动)

    lockInterruptibly 可打断锁,在尝试获取锁进入阻塞队列中,被打断时,可以停止等待锁。

     public static void main(String[] args) throws InterruptedException {
    
            ReentrantLock lock = new ReentrantLock();
    
            Thread t1 = new Thread(() -> {
    
                try {
                    log.debug("t1线程尝试获取锁");
                    lock.lockInterruptibly(); // 尝试获取锁。若有竞争,进入阻塞队列,可被打断
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    log.debug("等待锁的时候被打断了,停止等待,返回");
                    return;
                }
    
                try {
                    log.debug("获得了锁");
                } finally {
                    lock.unlock();
                }
            }, "t1");
            lock.lock();
            log.debug("主线程获得了锁");
            t1.start();
    
            sleep(1);
            log.debug("打断 t1");
            t1.interrupt();
            lock.unlock();
        }
    

    锁超时(避免死等、主动)

    无参数:获取锁,若没获取到,立即失败,返回false。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            ReentrantLock lock = new ReentrantLock();
    
            Thread t1 = new Thread(() -> {
                log.debug("启动...");
                if (!lock.tryLock()) {
                    log.debug("t1获取锁立刻失败,返回");
                    return;
                }
                try {
                    log.debug("t1获得到了锁");
                } finally {
                    lock.unlock();
                }
            }, "t1");
            lock.lock();
            log.debug("主线程获得了锁");
            t1.start();
            sleep(2);
            lock.unlock();
        }
    }
    

    在这里插入图片描述

    可以主动设置等待时间。

    @Slf4j(topic = "c.Test3")
    public class st3 {
        public static void main(String[] args) throws InterruptedException {
    
            ReentrantLock lock = new ReentrantLock();
    
            Thread t1 = new Thread(() -> {
                log.debug("启动...");
    
                try {
                    if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                        log.debug("尝试获取锁1s,失败,返回");
                        return;
                    }
                } catch (InterruptedException e) { // 被打断,应该也return
                    e.printStackTrace();
                }
                try {
                    log.debug("t1获得了锁");
                } finally {
                    lock.unlock();
                }
            }, "t1");
    
            lock.lock();
            log.debug("主线程获得了锁");
            t1.start();
            sleep(1);
            lock.unlock();
        }
    }
    

    主线程1ms后释放了锁,t1线程还在1s的等待中,可以获得锁,若时等待超过1s那就不再尝试获取锁了。

    在这里插入图片描述

    若主线程沉睡1s的情况:

    sleep(1000);
    

    在这里插入图片描述

    公平锁

    ReentrantLock默认也是不公平锁,但是可以修改。

    公平: 先来先执行
    不公平: 随机有机会

    这里就给出如何修改

    ReentrantLock lock = new ReentrantLock(true);  // 公平
    ReentrantLock lock = new ReentrantLock(false); // 不公平  
    

    一般是不设置的,会降低并发度,所以就不给出例子了。

    多个条件变量

    • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
    • synchronized只能全部都唤醒,而ReentrantLock可以根据条件唤醒对应线程。
    Condition waitCigaretteQueue = lock.newCondition(); // 条件
    waitCigaretteQueue.await(); // 因为此原因进入等待  
    signal 唤醒  对应等待的线程  
    

    比如,在操作系统里,有一个很经典的问题,爸爸向盘子里放苹果,妈妈向盘子里放香蕉,儿子只吃苹果,女儿只吃香蕉,盘子可以放多个水果,但每种最多一个。

    如果我么只用sync锁,那么就只能有一个条件,如果只有爸爸向盘子里放了苹果,那么他会唤醒儿子和女儿,但其实只唤醒儿子即可。
    所以利用ReentrantLock多条件,因为沉睡的条件不同,我么就可以对应的唤醒我们想要的一些线程。
    这里例子是用的烟和早餐,一样的。

    @Slf4j(topic = "c.Test4")
    public class st4 {
        static ReentrantLock lock = new ReentrantLock();
    
        static Condition waitCigaretteQueue = lock.newCondition(); //因为等烟等烟,进入等待的阻塞线程队列
        static Condition waitTokeoutQueue = lock.newCondition(); //因为等早餐进入阻塞队列
    
        static boolean hasCigarette = false;
        static boolean hasTokeout = false;
    
        public static void main(String[] args) throws InterruptedException {
    
            new Thread(() -> {
                lock.lock();
                try {
                    while (!hasCigarette) {
                        try {
                            waitCigaretteQueue.await(); // 因为等烟,进入等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了烟");
                } finally {
                    lock.unlock(); // 解锁
                }
            }).start();
    
            new Thread(() -> {
                try {
                    lock.lock();
                    while (!hasTokeout) {
                        try {
                            waitTokeoutQueue.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了它的早餐");
                } finally {
                    lock.unlock();
                }
            }).start();
    
            sleep(1);
            sendBreakfast();
            sleep(1);
            sendCigarette();
        }
    
        private static void sendCigarette() {
            lock.lock();
            try {
                log.debug("送烟来的了");
                hasCigarette = true;
                waitCigaretteQueue.signal();
            } finally {
                lock.unlock();
            }
        }
    
        private static void sendBreakfast() {
            lock.lock();
            try {
                log.debug("送早餐的来了");
                hasTokeout = true;
                waitTokeoutQueue.signal();
            } finally {
                lock.unlock();
            }
        }
    }
    

    在这里插入图片描述

    java内存模型

    JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

    JMM 体现在以下几个方面 :

    • 原子性 - 保证指令不会受到线程上下文切换的影响
    • 可见性 - 保证指令不会受 cpu 缓存的影响
    • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

    可见性

    按照如下代码,一般我们都会想,run改成false,那么线程t1也就运行结束了,但是并没有运行结束,而是无线运行下去了。

    这就算因为可见性导致的。

    @Slf4j(topic = "c.Test5")
    public class st5 {
        static boolean run = true;
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread t = new Thread(()->{
                while(run){
    
                }
            });
            t.start();
    
            sleep(1);
            run = false; // 线程t不会如预想的停下来
        }
    }
    

    在一开始t1线程是在主内存中获取 run 值。
    在这里插入图片描述

    随着 while 次数一直获取次数增多,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    但是,会带来一个问题,就是主存和内存中的值不一致,当主存内容修改,t1 线程任然从内存中获取run值,就会读到旧的内容,从而一直停不下来。

    在这里插入图片描述

    解决方法

    加volatile, 它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的最新值,线程操作 volatile 变量都是直接操作主存。

    volatile static boolean run = true;
    

    当然我么之前用synchronized上锁,让共享变量在锁代码块中操作,也是可以保证可见性的,不然我么之前的的代码不就都不对了么。

    不过synchronized毕竟比较重还要创建monitor,而volatile是更加轻量级的。

    原子性

    volatile只能保证读取到最新值,而不能解决原子性问题。

    比如线程t1读取到了值x = 0,要进行++操作时,正好切换了,切换到了线程t2了,而t2也读取x = 0, 并对于x进行了–操作返回给内存进行更新,然后又切换回线程t2,t2该进行++操作,并且赋值x更新到内存。

    原本一加一减应该变回0,但经过这种线程切换的情况,x最后为1,显然不符合我们的预期,因为破坏了读写的原子性。

    所以在保证原子性时,还是利用synchronized和ReentrantLock编写代码时对共享变量读写保证其原子性。

    有序性

    指令重排

    JVM 会在不影响正确性的前提下,可以调整语句的执行顺序。

    比如i和j同时++,因为互不影响,谁先执行都一样,所以jvm可能的执行顺序就是i++,j++也可以是j++,i++。

    流水线技术

    流水线技术是一种并行计算的方式,它类似于生产流水线,将一个复杂的任务分解为一系列的子任务,并且这些子任务可以并行地执行,每个子任务的输出作为下一个子任务的输入,从而实现整体任务的并行处理。

    在计算机科学中,流水线技术通常用于优化处理速度,特别是对于那些可以被分解为多个相互独立阶段的任务。通过流水线技术,可以将一个任务分解为多个阶段,每个阶段由专门的处理单元来执行,这样就可以同时处理多个阶段,从而提高整体处理速度。

    流水线技术常见于处理器的设计中,例如现代CPU中的指令流水线。在指令流水线中,CPU将指令执行分解为多个阶段,比如取指阶段、解码阶段、执行阶段等等,每个阶段由专门的硬件单元来执行,这样就可以实现多条指令的并行执行,从而提高CPU的吞吐量。

    简单来说啊,在不改变结果的情况下,把一个任务,拆成多个,重排序,一起执行。

    在这里插入图片描述

    // 可以重排
    int a = 10; // 指令1
    int b = 20; // 指令2
    System.out.println( a + b );
    
    // 不能重排
    int a = 10; // 指令1
    int b = a - 5; // 指令2
    

    指令重排出现的问题

    int num = 0;
    boolean ready = false;
    
    // 线程1 执行此方法
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    
    // 线程2 执行此方法
    public void actor2(I_Result r) { 
        num = 2;
        ready = true; 
    }
    

    r.r1最后可能为0,但是概率非常非常非常小,我们自己测不出来的。

    原因线程2执行方法指令重排,先执行了ready = true,这时切换线程1,执行了r.r1 = num + num,然后线程2才执行了num = 2。那么最后r.r1就等于0.

    解决方法
    还是加上volatile即可,可以防止指令重排序。
    volatile会保证 顺序性 和 可见性。

    模式之Balking(犹豫)

    犹豫模式,一个线程在执行某个操作之前会检查某个条件,如果条件不满足,则放弃执行,直接返回。这个模式的核心思想就是避免重复执行某个操作,当发现已经有其他线程或本线程已经执行了相同的操作时,就直接结束当前线程的执行。
    Balking 模式通常适用于一些需要进行资源共享或状态同步的场景。当多个线程需要对共享资源进行访问或修改时,可以使用 Balking 模式来确保资源的正确使用,避免出现竞态条件或资源浪费。

    举个简单的例子,假设有一个线程池,多个线程需要从线程池中获取任务执行。在获取任务之前,每个线程会检查线程池中是否还有可用的任务,如果有,则取出任务执行,如果没有,则放弃执行,直接返回。这样就可以避免多个线程同时尝试获取任务导致的竞态条件,同时也可以避免线程在没有可执行任务时进行无效的等待,提高了线程池的效率。

    public class MonitorService {
        
        // 用来表示是否已经有线程已经在执行启动了
        private volatile boolean starting;
        
        public void start() {
            log.info("尝试启动监控线程...");
            synchronized (this) {
                if (starting) {
                    return;
                }
                starting = true;
            }
            
            // 真正启动监控线程...
        }
    }
    

    这里volatile是可以不加的,synchronized已经保证了可见性,不过,通常建议还是添加 volatile 关键字来明确地表达你的意图,即使这样做可能会带来一些微小的性能开销。

    synchronized锁代码块可以保证原子性、有序性、可见性。

    double-checked locking(DCL) 问题

    • 第一个if用于后续进入的线程,不用再获取锁来判断是否已经创建了对象。
    • 第二个if,为的是第一个进入的线程创建对象,以及防止卡在第一个if之后,获锁之前的线程在第一个线程已经创建对象的情况下,在获取锁后,判断不用创建对象,防止多次创建。

    单例模式:

    public final class Singleton {
        private Singleton() { }
        private static Singleton INSTANCE = null;
        
        public static Singleton getInstance() { 
            if(INSTANCE == null) { 
                // 首次访问同步,而之后的使用就不用 synchronized,所以在此行前加了判断  
                synchronized(Singleton.class) {
                    if (INSTANCE == null) { 
                        INSTANCE = new Singleton(); 
                    } 
                }
            }
            return INSTANCE;
        }
    }
    

    特点:

    • 懒汉式实例化
    • 首次使用加锁,之后不用加锁

    但是,看似完美的代码,其实并不对,因为第一个if判断并不在synchronized中,所以可能会发生指令重排问题。

    synchronized可以保证原子、有序、可见性,但是有序性需要变量完全synchronized被保护,这里第一个if并不在sync中,所以是可能发生下述情况的。

    也就是说synchronized只能保证临界区内的指令不和临界区外的指令发生重排序。

    比如先给INSTANCE赋值了,但是还没有调用构造方法,这时线程切换,他以为你创建好了对象,然后返回对象直接开始使用,是不是就出现问题了,因为我们对象还没构造呢。

    在这里插入图片描述

    解决方法

    给Singleton加volatile的原因。

    public final class Singleton {
        private Singleton() { }
        private static volatile Singleton INSTANCE = null;
        
        public static Singleton getInstance() { 
            if(INSTANCE == null) { 
                // 首次访问同步,而之后的使用就不用 synchronized,所以在此行前加了判断  
                synchronized(Singleton.class) {
                    if (INSTANCE == null) { 
                        INSTANCE = new Singleton(); 
                    } 
                }
            }
            return INSTANCE;
        }
    }
    

    这样就是一个正确的代码了。

    volatile作用

    volatile作用:
    可见性:

    • 写屏障(sfence)保证在该屏障之前对共享变量的改动,都同步到主存当中
    • 读屏障(lfence)保证在该屏障之后对共享变量的读取,加载的是主存中最新数据

    有序性:

    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

    只能保证读数据正确,不能保证原子性。

    happens-before 规则

    No.1
    线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可性。

    static int x;
    static Object m = new Object();
    
    new Thread(()->{
        synchronized(m) {
            x = 10;
        }
    },"t1").start();
    
    new Thread(()->{
        synchronized(m) {
            System.out.println(x);
        }
    },"t2").start();
    

    synchronized锁保证了可见性。

    No.2
    线程对 volatile 变量的写,对接下来其它线程对该变量的读可见。

    volatile static int x;
    
    new Thread(()->{
        x = 10;
    },"t1").start();
    
    new Thread(()->{
        System.out.println(x);
    },"t2").start();
    

    volatile保证读的正确性。

    No.3
    线程 start 前对变量的写,对该线程开始后对该变量的读可见。

    static int x; 
    x = 10;
    
    new Thread(()->{
        System.out.println(x);
    },"t2").start();
    

    线程开始前写的,开始后肯定读到了正确。

    No.4
    线程结束前对变量的写,对其它线程得知它结束后的读可见。

    static int x;
    
    Thread t1 = new Thread(()->{
        x = 10;
    },"t1");
    t1.start();
    
    t1.join();
    System.out.println(x);
    

    No.5
    线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见

    static int x;
    
    public static void main(String[] args) {
        Thread t2 = new Thread(()->{
            while(true) {
                if(Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        },"t2");
        t2.start();
        
        new Thread(()->{
            sleep(1);
            x = 10;
            t2.interrupt();
        },"t1").start();
        
        while(!t2.isInterrupted()) {
            Thread.yield();
        }
        System.out.println(x);
    }
    

    t1线程赋完值后,进行打断t2,t2感知到打断标记为true后,输出,同时主线程再判断,然后输出。

    No.6
    对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。

    No.7
    具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子。

    volatile static int x;
    static int y;
    
    new Thread(()->{ 
        y = 10;
        x = 20;
    },"t1").start();
    
    new Thread(()->{
        // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
        System.out.println(x); 
    },"t2").start();
    

    volatile会将写屏障之前的所有操作同步到主存,同步不会重排序,所以x赋值后,y也一定同步到主存了,虽然y没加volatile,但是对t2也是可见的。

    CAS

    compareAndSet 比较和换行。

    方法内部是原子的(指令级别)。

    修改失败会返回false,用原值和最新值进行比较,如果原值被他人修改了返回false,若原值没有被人修改,进行交换,返回true。

    当然这里会出现aba问题,加上版本号就可以很好的解决。

    while循环直到交换成功。

    public void withdraw(Integer amount) {
    
        while (true) {
    
            int prev = balance.get();
            int next = prev - amount;
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }     
    

    在这里插入图片描述

    • CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
    • 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

    与volatile关系

    cas需要volatile支持,因为我们比较时,需要利用最新值进行比较。

    来看看AtomicInteger源码

    在这里插入图片描述

    为什么 cas 比 加锁 效率高

    • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
    • 无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
    • 从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。
    • 所以需要多核cpu才能发挥cas的作用。

    cas的特点

    结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

    • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
    • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

    CAS 体现的是无锁并发、无阻塞并发。

    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。你想想,如果有一堆线程再不断的自旋重试,肯定会造成cpu的浪费。

    原子类型

    AtomicInteger 原子整数

    public class test6 {
        public static void main(String[] args) {
            AtomicInteger i = new AtomicInteger();
    
            System.out.println(i.getAndIncrement()); // i++ sout: 0
            System.out.println(i.incrementAndGet()); // ++i sout: 2
            System.out.println(i.decrementAndGet()); // --i sout: 1
            System.out.println(i.getAndDecrement()); // i-- sout: 1
            // i = 0
            System.out.println(i.getAndAdd(5)); // sout:0 i += 5
            System.out.println(i.addAndGet(-5)); // i -= 5 sout:0
            // i = 0
            System.out.println(i.getAndUpdate(p -> p - 2)); // sout:0 p -= 2
            System.out.println(i.updateAndGet(p -> p + 2)); // p += 2 sout: 0
        }
    }
    

    在这里插入图片描述

    AtomicReferenc 原子引用

    class DecimalAccountSafeCas{
        AtomicReference<BigDecimal> ref;
    
        public DecimalAccountSafeCas(BigDecimal balance) {
            ref = new AtomicReference<>(balance);
        }
    
        public BigDecimal getBalance() {
            return ref.get();
        }
    
        public void withdraw(BigDecimal amount) {
            while (true) {
                BigDecimal prev = ref.get();
                BigDecimal next = prev.subtract(amount); // 减小
                if (ref.compareAndSet(prev, next)) { // cas
                    break;
                }
            }
        }
    }
    

    AtomicStampedReference 带版本号的原子引用

    解决aba问题,aba问题之前介绍过了。
    如果版本号与之前获取的相同,那么就进行交换,同时更新版本号。
    可以知道更改次数。

    class DecimalAccountSafeCas{
        AtomicStampedReference<BigDecimal> ref;
    
        public DecimalAccountSafeCas(BigDecimal balance) {
            ref = new AtomicStampedReference<>(balance, 0); // 初始版本号0
        }
    
        public BigDecimal getBalance() {
            return ref.getReference();
        }
    
        public void withdraw(BigDecimal amount) {
            while (true) {
                BigDecimal prev = ref.getReference();
                int stamp = ref.getStamp();// 获取版本号
                // 如果这之中有其他线程又修改了ref,那么版本号改变与之前不同。就会cas失败    
                BigDecimal next = prev.subtract(amount); // 减小
                if (ref.compareAndSet(prev, next, stamp, stamp + 1)) { // cas。版本号相同才交换,并且更新版本号
                    break;
                }
            }
        }
    }
    

    AtomicMarkableReference 仅记录是否修改的原子引用

    我们一般不关心中间修改了多少次,我么只在乎是否修改过,那么就可以用这个类。

    所以一个boolean值就可以记录。

    class DecimalAccountSafeCas{
        AtomicMarkableReference<BigDecimal> ref;
    
        public DecimalAccountSafeCas(BigDecimal balance) {
            ref = new AtomicMarkableReference<>(balance, true);
        }
    
        public BigDecimal getBalance() {
            return ref.getReference();
        }
    
        public void withdraw(BigDecimal amount) {
            while (true) {
                BigDecimal prev = ref.getReference();
                BigDecimal next = prev.subtract(amount); // 减小
                // 如果这时有线程修改,把mark变为了false,那么就会cas就会失败
                // 因为只有true,和false,我们就直接就对比是否是true就行了,不用在获取
                if (ref.compareAndSet(prev, next, true, false)) { // cas。版本号相同才交换,并且更新版本号
                    break;
                }
            }
        }
    }
    

    AtomicXXXArray 原子数组

    我么一般都不是修改引用指向,而是引用里的内容,就比如数组。

    下面是线程安全的数组。

    • AtomicIntegerArray
    • AtomicLongArray
    • AtomicReferenceArray

    这里我么先用普通数组,来看看是否有线程安全问题:

    package com.leran;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.function.BiConsumer;
    import java.util.function.Consumer;
    import java.util.function.Function;
    import java.util.function.Supplier;
    
    public class test7 {
    
        public static void main(String[] args) {
    
            demo(
                    () -> new int[10],
                    (array) ->  array.length,
                    (array, idx) -> array[idx]++,
                    array -> System.out.println(Arrays.toString(array))
            );
        }
    
        private static <T> void demo(
            Supplier<T> arraySupplier, // 生产者,无中生有
            Function<T, Integer> lengthFun, // 一个参数一个结果 BiFunction 多个参数,多个结果
            BiConsumer<T, Integer> putConsumer, // 消费者多个参数,无结果
            Consumer<T> printConsumer ) { // 一个参数,无结果
    
            List<Thread> ts = new ArrayList<>();
            T array = arraySupplier.get(); // 获取传入的数组
            int length = lengthFun.apply(array); // 获取长array长度
            for (int i = 0; i < length; i++) {
                // 每个线程对数组作 10000 次操作
                ts.add(new Thread(() -> {
                    for (int j = 0; j < 10000; j++) {
                        putConsumer.accept(array, j%length);
                    }
                }));
            }
            ts.forEach(t -> t.start()); // 启动所有线程
    
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }); // 等所有线程执行完,输出
            printConsumer.accept(array);
        }
    }
    
    

    在这里插入图片描述

    显然是不安全的,下面介绍安全的AtomicIntegerArray。

    demo方法不变,改变传入的数组。

        demo(
                () -> new AtomicIntegerArray(10),
                (array) ->  array.length(),
                (array, idx) -> array.getAndIncrement(idx),
                array -> System.out.println(array)
        );
    

    在这里插入图片描述

    其他用法相似。

    AtomicXXXFieldUpdater 字段更新器

    用于更新某一类中的属性。

    • AtomicReferenceFieldUpdater // 域字段
    • AtomicIntegerFieldUpdater
    • AtomicLongFieldUpdater

    必须配合volatile使用。

    如:AtomicIntegerFieldUpdater

    public class test8 {
        public static void main(String[] args) {
            AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
            Student student = new Student();
            fieldUpdater.compareAndSet(student, 0, 5);
            System.out.println(student.age); // 5
            fieldUpdater.compareAndSet(student,5, 10);
            System.out.println(student.age); // 10
            fieldUpdater.compareAndSet(student, 5, 20); // 修改失败
            System.out.println(student.age); // 10
        }
    }
    
    class Student{
        volatile int age;
    
        @Override
        public String toString() {
            return "student{" +
                    "age=" + age +
                    '}';
        }
    }
    

    在这里插入图片描述

    其他用法相似。

    LongAdder 累加器

    java中专门用于累加的,所以性能肯定比我么AtomicLong好。
    下面是对比。

    public class test9 {
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
            }
            for (int i = 0; i < 5; i++) {
                demo(() -> new LongAdder(), adder -> adder.increment());
            }
        }
        private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
            T adder = adderSupplier.get();
    
            long start = System.nanoTime();
    
            List<Thread> ts = new ArrayList<>();
            for (int i = 0; i < 40; i++) {
                ts.add(new Thread(() -> {
                    for (int j = 0; j < 500000; j++) {
                        action.accept(adder);
                    }
                }));
            }
            ts.forEach(t -> t.start());
    
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.nanoTime();
            System.out.println(adder + " cost:" + (end - start)/1000_000 + "ns");
        }
    }
    

    前五次是AtomicLong累加
    后五次是LongAdder累加

    20000000 cost:414ns
    20000000 cost:390ns
    20000000 cost:366ns
    20000000 cost:377ns
    20000000 cost:414ns
    20000000 cost:33ns
    20000000 cost:26ns
    20000000 cost:26ns
    20000000 cost:27ns
    20000000 cost:37ns
    
    Process finished with exit code 0
    

    显然是快了10倍左右。

    性能提升的原因,就是在有竞争时,设置多个累加单元Cell,最后将结果汇总。
    这样在累加时操作的是不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

    不可变类

    SimpleDateFormat 不安全

        public static void main(String[] args) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    try {
                        System.out.println(simpleDateFormat.parse("1951-04-21"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    

    SimpleDateFormat是一个线程不安全的,多线程下大概率出现报错:
    在这里插入图片描述

    为什么不安全?

    来看看这个方法:

        @Override
        public Date parse(String text, ParsePosition pos)
        {
            checkNegativeNumberExpression();
    
            int start = pos.index;
            int oldStart = start;
            int textLength = text.length();
    
            boolean[] ambiguousYear = {false};
    
            CalendarBuilder calb = new CalendarBuilder();
    
            for (int i = 0; i < compiledPattern.length; ) {
                int tag = compiledPattern[i] >>> 8;
                int count = compiledPattern[i++] & 0xff;
                if (count == 255) {
                    count = compiledPattern[i++] << 16;
                    count |= compiledPattern[i++];
                }
    
                switch (tag) {
                case TAG_QUOTE_ASCII_CHAR:
                    if (start >= textLength || text.charAt(start) != (char)count) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                    break;
    
                case TAG_QUOTE_CHARS:
                    while (count-- > 0) {
                        if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                            pos.index = oldStart;
                            pos.errorIndex = start;
                            return null;
                        }
                        start++;
                    }
                    break;
    
                default:
                    // Peek the next pattern to determine if we need to
                    // obey the number of pattern letters for
                    // parsing. It's required when parsing contiguous
                    // digit text (e.g., "20010704") with a pattern which
                    // has no delimiters between fields, like "yyyyMMdd".
                    boolean obeyCount = false;
    
                    // In Arabic, a minus sign for a negative number is put after
                    // the number. Even in another locale, a minus sign can be
                    // put after a number using DateFormat.setNumberFormat().
                    // If both the minus sign and the field-delimiter are '-',
                    // subParse() needs to determine whether a '-' after a number
                    // in the given text is a delimiter or is a minus sign for the
                    // preceding number. We give subParse() a clue based on the
                    // information in compiledPattern.
                    boolean useFollowingMinusSignAsDelimiter = false;
    
                    if (i < compiledPattern.length) {
                        int nextTag = compiledPattern[i] >>> 8;
                        if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                              nextTag == TAG_QUOTE_CHARS)) {
                            obeyCount = true;
                        }
    
                        if (hasFollowingMinusSign &&
                            (nextTag == TAG_QUOTE_ASCII_CHAR ||
                             nextTag == TAG_QUOTE_CHARS)) {
                            int c;
                            if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                                c = compiledPattern[i] & 0xff;
                            } else {
                                c = compiledPattern[i+1];
                            }
    
                            if (c == minusSign) {
                                useFollowingMinusSignAsDelimiter = true;
                            }
                        }
                    }
                    start = subParse(text, start, tag, count, obeyCount,
                                     ambiguousYear, pos,
                                     useFollowingMinusSignAsDelimiter, calb);
                    if (start < 0) {
                        pos.index = oldStart;
                        return null;
                    }
                }
            }
    
            // At this point the fields of Calendar have been set.  Calendar
            // will fill in default values for missing fields when the time
            // is computed.
    
            pos.index = start;
    
            Date parsedDate;
            try {
                parsedDate = calb.establish(calendar).getTime();
                // If the year value is ambiguous,
                // then the two-digit year == the default start year
                if (ambiguousYear[0]) {
                    if (parsedDate.before(defaultCenturyStart)) {
                        parsedDate = calb.addYear(100).establish(calendar).getTime();
                    }
                }
            }
            // An IllegalArgumentException will be thrown by Calendar.getTime()
            // if any fields are out of range, e.g., MONTH == 17.
            catch (IllegalArgumentException e) {
                pos.errorIndex = start;
                pos.index = oldStart;
                return null;
            }
    
            return parsedDate;
        }
    

    在这里插入图片描述

    主要是这里调用了establish()

      Calendar establish(Calendar cal) {
            boolean weekDate = isSet(WEEK_YEAR)
                                && field[WEEK_YEAR] > field[YEAR];
            if (weekDate && !cal.isWeekDateSupported()) {
                // Use YEAR instead
                if (!isSet(YEAR)) {
                    set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
                }
                weekDate = false;
            }
    
            cal.clear();
            // Set the fields from the min stamp to the max stamp so that
            // the field resolution works in the Calendar.
            for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
                for (int index = 0; index <= maxFieldIndex; index++) {
                    if (field[index] == stamp) {
                        cal.set(index, field[MAX_FIELD + index]);
                        break;
                    }
                }
            }
    
            if (weekDate) {
                int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
                int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                    field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
                if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                    if (dayOfWeek >= 8) {
                        dayOfWeek--;
                        weekOfYear += dayOfWeek / 7;
                        dayOfWeek = (dayOfWeek % 7) + 1;
                    } else {
                        while (dayOfWeek <= 0) {
                            dayOfWeek += 7;
                            weekOfYear--;
                        }
                    }
                    dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
                }
                cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
            }
            return cal;
        }
    

    在这里插入图片描述

    参数为calender,那么这个canlender出自哪里呢?
    在这里插入图片描述

    实际上是继承DateFormat里的。

    establish中先进行了cal.clear(),又进行了cal.set()。 先清除cal对象中是值,再设置新的值。

    这样多线程下,调用同一个cal,而cal又没有设置线程安全,比如线程t1刚set完,t2进行了clear,那t1的输出肯定是不对的。

    下面介绍几种解决方法。

    解决 DateTimeFormatter

    方法一
    首先想到的肯定是加锁

    public class test10 {
        public static void main(String[] args) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    synchronized (simpleDateFormat) {
                        try {
                            System.out.println(simpleDateFormat.parse("1951-04-21"));
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        }
    }
    

    在这里插入图片描述

    这样可以解决,但是效率肯定是不高的。

    方法二

    利用不可变类型DateTimeFormatter
    源码介绍是一个不可变的、线程安全的类
    在这里插入图片描述

    public class test10 {
        public static void main(String[] args) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    TemporalAccessor parse = dateTimeFormatter.parse("1951-04-21");
                    System.out.println(parse);
                }).start();
            }
        }
    }
    

    在这里插入图片描述

    不可变类保证线程安全的实现

    不可变类的类,属性都是final修饰的。
    例如String:

    在这里插入图片描述

    保证只读和防止修改。

    同时其方法都是操作一个新对象,并不在原对象上操作,保证了安全。

    手写数据库连接池

    package com.数据库连接池;
    
    import java.sql.*;
    import java.util.Map;
    import java.util.Properties;
    import java.util.Random;
    import java.util.concurrent.Executor;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicIntegerArray;
    
    public class test {
        public static void main(String[] args) {
    
            Pool pool = new Pool(3);
    
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    Connection conn = pool.borrow();
                    try {
                        Thread.sleep(new Random().nextInt(1000)); // 模拟使用随机的时间后,进行归还
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pool.free(conn);
                }).start();
            }
        }
    }
    
    class Pool {
    
        // 连接池大小
        private final int poolSize;
    
        // 连接对象数组
        private Connection[] connections;
    
        // 连接状态,线程安全的
        private AtomicIntegerArray states;
    
        public Pool(int poolSize) {
            this.poolSize = poolSize;
            this.connections = new Connection[poolSize];
            this.states = new AtomicIntegerArray(new int[poolSize]);
            for (int i = 0; i < poolSize; i++) {
                connections[i] = new MockConnection();
            }
        }
    
        // 借连接
        public Connection borrow() {
            while (true) {
                for (int i = 0; i < poolSize; i++) {
                    if (states.get(i) == 0) {
                        if (states.compareAndSet(i, 0, 1)) { // cas成功才return,不要直接set,防止多线程读写分离
                            System.out.println(Thread.currentThread().getName() + "借到了连接");
                            return connections[i];
                        }
                    }
                }
                // 没有空闲连接,进入等待
                System.out.println(Thread.currentThread().getName() + "没有空闲连接,进入等待");
                synchronized (this) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        // 归还连接
        public void free(Connection connection) {
    
            for (int i = 0; i < poolSize; i++) {
                if (connections[i] == connection) {
                    states.set(i, 0); // 这里不用cas,归还线程一定是唯一拥有它的线程
                    System.out.println(Thread.currentThread().getName() + "释放了连接");
                    // 通知如果有再wait中的线程
                    synchronized (this) {
                        this.notifyAll();
                    }
                    break;
                }
            }
    
        }
    }
    // 不用真的去连接,随便实现一下Connection用来测试即可
    class MockConnection implements Connection{
    
        // 这里省略了,太长了
    }
    

    在这里插入图片描述

    手写线程池

    在这里插入图片描述

    public class MyThreadPool {
        public static void main(String[] args) {
            ThreadPool threadPool = new ThreadPool(2, 1000,TimeUnit.MICROSECONDS, 10, (queue, task) -> {
                // 1.死等
                queue.put(task);
                // 2.带超时时间等待加入等待队列
                // queue.offer(task, 500, TimeUnit.MICROSECONDS);
                // 3.放弃任务
                // 队列满了,没做人任何事情
                // 4.抛出异常
                // throw new RuntimeException("任务执行失败" + task);
                // 5.让调用者自己执行
                // task.run();
            });
            for (int i = 0; i < 15; i++) {
                int j = i;
                threadPool.execute(() -> {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(j);
                });
            }
        }
    }
    
    // 拒接策略
    @FunctionalInterface
    interface RejectPolicy<T> {
        void reject(BlockQueue queue, T task) ;
    }
    class ThreadPool {
        // 任务队列
        private BlockQueue<Runnable> taskQueue;
    
        // 线程集合
        private HashSet<Worker> workers = new HashSet();
    
        // 线程数
        private int coreSize;
    
        private long timeout;
    
        private TimeUnit timeUnit;
    
        private RejectPolicy<Runnable> rejectPolicy;
        // 构造方法
        public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueSize, RejectPolicy<Runnable> rejectPolicy) {
            this.coreSize = coreSize;
            this.timeout = timeout;
            this.timeUnit = timeUnit;
            this.taskQueue = new BlockQueue<>(queueSize);
            this.rejectPolicy = rejectPolicy;
        }
    
        public void execute(Runnable task) {
            // 当任务数没有超过核心数时,直接交给woker对象执行
            // 如果超过,放入任务队列中存起来
            synchronized (workers) { // workers不安全,把他锁起来
                if (workers.size() < coreSize) {
                    Worker worker = new Worker(task);
                    System.out.println("新增worker");
                    workers.add(worker); // 加入线程集合
                    worker.start();
                } else {
                    // taskQueue.put(task); // 任务添加进入
                    // 1.死等
                    // 2.带超时时间等待
                    // 3.放弃任务
                    // 4.抛出异常
                    // 5.让调用者自己执行
                    taskQueue.tryPut(rejectPolicy, task);
                }
            }
        }
        class Worker extends Thread{
            private Runnable task;
    
            public Worker(Runnable task) {
                this.task = task;
            }
    
            @Override
            public void run() {
                // 当task任务不为空,执行
                // 当任务为空,去任务队列中去取
                //  while (task != null || (task = taskQueue.take()) != null) 一直等待获取
                while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                    try {
                        System.out.println("正在执行" + task);
                        task.run();
                    } catch (Exception e) {
    
                    } finally {
                        task = null;
                    }
                }
                synchronized (workers) {
                    System.out.println("worker被移除" + this);
                    workers.remove(this); // 移除当前集合对象
                }
            }
        }
    }
    
    // 阻塞队列
    class BlockQueue<T> {
        // 任务队列
        private Deque<T> queue = new ArrayDeque<>();
    
        // 锁
        private ReentrantLock lock = new ReentrantLock();
    
        // 满了等待,生产者
        private Condition fullWaitSet = lock.newCondition();
    
        // 空的等待,消费者
        private Condition emptyWaitSet = lock.newCondition();
    
        // 容量
        private int capacity;
    
        public BlockQueue(int capacity) {
            this.capacity = capacity;
        }
    
        // 阻塞队列中获取任务
        public T take() {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    try {
                        emptyWaitSet.await(); // 进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal(); // 唤醒
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 阻塞队列中添加任务
        public void put(T t) {
             lock.lock();
             try {
                 while (queue.size() == capacity) { // 如果满了,进入等待
                     try {
                         System.out.println("等待加入任务队列" +  t);
                         fullWaitSet.await();
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
                 System.out.println("加入任务队列" + t);
                 queue.addLast(t);
                 emptyWaitSet.signal(); // 唤醒
             }finally {
                 lock.unlock();
             }
        }
    
        public int size() {
            lock.lock();
            try {
                return queue.size();
            }finally {
                lock.unlock(); // 就算return也会执行
            }
        }
    
        // 带超时时间的获取,无需永久的等待了
        public T poll (long timeout, TimeUnit unit) {
            lock.lock();
            try {
                long nanos = unit.toNanos(timeout); // 时间转换为ns
                while (queue.isEmpty()) {
                    try {
                        if (nanos <= 0) return null; // 超时了,直接返回吧
                        nanos = emptyWaitSet.awaitNanos(nanos);// 进入等待,超时不再等待,返回结果为剩余等待时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal(); // 唤醒
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 带超时时间的添加, return 添加成功 or 失败
        public boolean offer(T task, long timeout, TimeUnit timeUnit) {
            lock.lock();
            try {
                long nanos = timeUnit.toNanos(timeout);
                while (queue.size() == capacity) { // 如果满了,进入等待
                    try {
                        System.out.println("等待加入任务队列" +  task);
                        if (nanos <= 0) return false;
                        nanos = fullWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("加入任务队列" + task);
                queue.addLast(task);
                emptyWaitSet.signal(); // 唤醒
                return true;
            }finally {
                lock.unlock();
            }
        }
    
        public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
            lock.lock();
            try {
                // 判断队列是否已满
                if (queue.size() == capacity) { // 有空闲
                    rejectPolicy.reject(this, task); // 拒绝策略
                } else { // 有空闲
                    queue.addLast(task);
                    emptyWaitSet.signal();
                }
            }finally {
                lock.unlock();
            }
        }
    }
    

    ThreadPoolExecutor

    线程池状态

    线程池用高三位表示状态,第一位为符号位。

    在这里插入图片描述

    TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

    构造方法

        public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    
    • corePoolSize 核心线程数目
    • maximumPoolSize 最大线程数目
    • keepAliveTime 生存时间 - 针对救急线程
    • unit 时间单位 - 针对救急线程
    • workQueue 阻塞队列
    • threadFactory 线程工厂
    • handler 拒绝策略

    在这里插入图片描述

    任务会先创建核心线程执行,若核心线程到达核心数,任务进入阻塞队列(任务队列),若阻塞队列也满了,则启用急救线程执行任务,若达到最大线程数,那么才会启用拒绝策略,急救线程在一段时间没有执行任务会自行关闭。

    Executors 工厂方法

    帮助我们快速构建想要的线程池,不用配置那么多参数。

    newFixedThreadPool

    固定线程的线程池

        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    

    特点:

    • 核心线程数 = 最大线程数(救急线程)。
    • 无界队列,可以放入任意多任务。
    • 阻塞队列是无界的,可以放任意数量的任务。

    适用于任务量已知,相对耗时的任务。

    newCachedThreadPool

    带缓冲的线程池

        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    

    特点:

    • 核心线程数是 0,最大线程数是 Integer.MAX_VALUE。
    • 创建的全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建。
    • 队列采用了SynchronousQueue(同步队列),没有容量,有线程去取才能放入,所以叫同步的队列。

    适合任务数比较密集,但每个任务执行时间较短的情况。

    newSingleThreadExecutor

    单线程的线程池

        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    

    特点:
    线程数固定为 1,任务数多于 1 时,会放入无界队列排队。
    任务执行完毕,这唯一的线程也不会被释放。

    和自己创建一个线程来工作的区别:

    • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。

    和Executors.newFixedThreadPool(1)的区别:

    • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
    • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改。
      对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

    提交任务方法

    // 执行任务
    void execute(Runnable command);
    
    // 用Future 获得任务执行结果,保护性暂停,主线程输出结果时,阻塞,直到返回结果
    <T> Future<T> submit(Callable<T> task);
    
    // 提交集合中所有任务
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    
    // 提交集合中所有任务,带超时时间
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
    
    // 交集合中所有任务, 只要有一个任务完成,返回结果,其他的任务取消
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
    
    // 交集合中所有任务, 只要有一个任务完成,返回结果,其他的任务取消, 带超时时间  
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
     throws InterruptedException, ExecutionException, TimeoutException;
    

    关闭任务方法

    void shutdown();
    

    线程池状态变为 SHUTDOWN

    • 不会接收新任务
    • 但已提交任务会执行完
    • 此方法不会阻塞调用线程的执行
    List<Runnable> shutdownNow();
    

    线程池状态变为 STOP

    • 不会接收新任务
    • 会将队列中的任务返回
    • 并用 interrupt 的方式中断正在执行的任务
    // 不在RUNNING状态的线程池,就返回 true
    boolean isShutdown();
    
    // 线程池状态是否是TERMINATED
    boolean isTerminated();
    
    // 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    

    线程个数

    . 创建多少线程池合适

    • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
    • 过大会导致更多的线程上下文切换,占用更多内存

    CPU 密集型运算

    通常采用 cpu 核数 + 1 和cpu核心数 - 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因
    导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费。

    核心数2n、3n,防止cpu核心空闲,及时的补上取利用cpu。

    I/O 密集型运算

    CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程
    RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

    经验公式如下:
    线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

    例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
    4 * 100% * 100% / 50% = 8

    例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
    4 * 100% * 100% / 10% = 40

    ScheduledThreadPoolExecutor

    timer(不建议用)

    timer也可以进行延迟运行,但是会有很多问题。

    比如task1运行时间超过task2延迟时间。

    @Slf4j(topic = "c.Main")
    public class Main {
        public static void main(String[] args) {
            Timer timer = new Timer();
    
            TimerTask task1 = new TimerTask() {
                @Override
                public void run() {
                    log.debug("task 1");
                    try {
                        sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
    
            TimerTask task2 = new TimerTask() {
                @Override
                public void run() {
                    log.debug("task 2");
                }
            };
    
            log.debug("start...");
            timer.schedule(task1, 1000);
            timer.schedule(task2, 1000);
        }
    }
    

    在这里插入图片描述

    当task1出异常,task2不会运行。

    ScheduledThreadPoolExecutor

    Executors就可以创建。

    延迟执行:

    @Slf4j(topic = "c.Main")
    public class Main {
        public static void main(String[] args) {
    
    
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
            scheduledExecutorService.schedule(() -> {
                log.debug("task1");
                int i = 1 / 0; // 模拟异常
            }, 1, TimeUnit.SECONDS);
    
            scheduledExecutorService.schedule(() -> {
                log.debug("task2");
            }, 1, TimeUnit.SECONDS);
    
        }
    }
    

    在这里插入图片描述

    可以看出通过控制线程数可以解决timer的缺点。
    同时出现异常也不会影响其他任务运行。

    定时运行:

    @Slf4j(topic = "c.Main")
    public class Main {
        public static void main(String[] args) {
    
    
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
            log.debug("start...");
            scheduledExecutorService.scheduleAtFixedRate(() -> {
                log.debug("running...");
            }, 1, 1, TimeUnit.SECONDS); // 每隔1s反复执行
    
        }
    }
    

    在这里插入图片描述

    处理异常

    try catch

    ExecutorService pool = Executors.newFixedThreadPool(1);
    pool.submit(() -> {
        try {
            log.debug("task1");
            int i = 1 / 0;
        } catch (Exception e) {
            log.error("error:", e);
        }
    });
    

    通过返回值判断

    ExecutorService pool = Executors.newFixedThreadPool(1);
    
    Future<Boolean> f = pool.submit(() -> {
        log.debug("task1");
        int i = 1 / 0; // 模拟异常
        return true;
    });
    log.debug("result:{}", f.get());
    

    若正常执行返回true,
    若出现异常get会返回异常信息。
    在这里插入图片描述

    应用

    每周四定时运行

    public class Main {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            // 获得当前时间
            LocalDateTime now = LocalDateTime.now();
            // 获取本周四 18点时间
            LocalDateTime startTime =
                    now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
            // 如果当前时间已经超过 本周四 那么找下周四
            if(now.compareTo(startTime) >= 0) {
                startTime = startTime.plusWeeks(1);
            }
    
            // 计算时间差,延时执行时间
            long initialDelay = Duration.between(now, startTime).toMillis();
    
            // 执行间隔 1周
            long period = 1000 * 60 * 60 * 24 * 7;
    
            ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    
            executor.scheduleAtFixedRate(() -> {
                System.out.println("running..");
            }, initialDelay, period, TimeUnit.MILLISECONDS);
    
        }
    }
    

    readwritelock

  • 相关阅读:
    机器学习中的数学基础(一)
    【OpenCV】图形绘制与填充
    行业品牌监测报告|《中国汽车产业舆情报告2022(上半年)》
    VR数字化线上展馆降低企业投入成本和周期
    【STM32】CRC(循环冗余校验)
    L958. 二叉树的完全性检验 java
    《HelloGitHub》第 79 期
    Web前端开发——新年倒计实时刷新
    Effective C++条款18:让接口容易被正确使用,不容易被误用
    【UNI-APP】阿里NLS一句话听写typescript模块
  • 原文地址:https://blog.csdn.net/Cosmoshhhyyy/article/details/139484763