menu 冷 の Codeworld
search self_improvement
目录

JUC并发编程工具包

冷环渊
冷环渊 2022年09月30日  ·  阅读 498

多线程进阶 JUC并发编程

什么是JUC

问:如何学习JUC?

答: 源码 + Java帮助文档 面试高频,

juc 其实就是 Java.util 包下的线程分类的工具

image-20220301194131214

以往学习的线程只是:

​ 我们使用的普通的线程代码 Thread,==Runnable== 等其实回顾JavaSE的线程知识我们可以发现,

其实我们学习线程基础的时候,也是有用到concurrent包下的东西

比如 RunnableCallable ,Callable 就是我们concurrent 包下的

还有就是 Lock

image-20220301194541263

线程与进程

线程、进程、如何来解释

进程 : 一个程序 如 QQ.exe Music.exe 程序的集合

一个进程可以包含多个线程,至少包含一个线程

==Java 默认是开启两个线程 main GC==

线程: 开了一个进程 比如: typora 写字,自动保存(线程负责的)

对于Java 而言创建线程我们学习到的方法有三种 : Thread , Runnable , Callable

PS :Java本身是无法开启线程的!!!

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

	// 本地方法 调用底层的 C++ Java本身不能直接操作硬件
    private native void start0();

并发,并行

我们要学习并发编程,首先要了解 : 并发和并行是什么

并发(多线程操作同一资源)

  • CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替。

并行(多个程序一起走)

  • CPU 多核,多个线程同时执行;如果需要提高性能 : 线程池

查看一下自己的cpu核数

image-20220301201453940

并发编程的本质:充分利用CPU的资源

所有的公司都很看重,就是效率,比如裁员:一个厉害的可以大于两个不再那么厉害的甚至更多

比如: 做事情的速度 高手:1s,一般人: 10s 肯定是 1s 的更加的之前也更加的有效率;

做到人员(减少),但是技术力却提高了

线程有几个状态

通过Thread 源码里的枚举类:State 中的属性可以看出来

线程有六个状态

    public e
num State {
        //新的线程
        NEW,
        //运行中
        RUNNABLE,
        // 阻塞
        BLOCKED,
        //等待,一直等
        WAITING,
        //等待一段时间就不等了
        TIMED_WAITING,
        // 终止线程
        TERMINATED;
    }

wait/sleep的区别

1、他们来自不同的类

wait => object

sleep => Thread

2、关于锁的释放

wait会释放锁,sleep不会释放锁

抽象理解 : sleep 睡着了没办法释放, wait 是等待,有人需要的释放

wait必须在同步代码块中使用,得有需要等的人

sleep可以在任何地方使用

3、是否需要捕获异常

wait 不需要捕获异常

sleep 需要捕获异常

Lock锁

传统 Synchronized

传统火车票案例

/**
 * @projectName: JUC
 * @package: Thread
 * @className: SaleTicketDemo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 20:28
 * @version: 1.0
 */
public class SaleTicketDemo {
    /**
     * 真正的多线程开发 公司中的需要降低耦合性
     * 线程就是一个单独的资源类,没有任何附属的操作
     * 1、 属性。方法
     * */
    public static void main(String[] args) {
        //并发: 多线程操作同一个资源类,把资源丢入线程
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "C").start();
    }
}

//资源类 OOP
class Ticket {
    //属性 方法
    private int number = 50;

    //    买票的方式
    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了" + (number--) + "票,剩余" + number);
        }
    }
}

Lock接口

image-20220301204107904

常用的一些实现类

image-20220301204151242

image-20220301204430613

公平锁 : 十分公平,可以先来后到 比如 前一个线程要执行30s 后面的 需要3s 后者必须等待前者执行完

非公平锁 : 十分不公平,可以根据条件来插队

如何用Lock来编写火车票Demo

  1. new ReentrantLock();
  2. Lock.lock(); 解锁
  3. finally => lock.unlock(); 解锁

Synchronized和Lock区别

  1. Synchronized 内置的java关键字,Lock是一个java类
  2. Synchronized 无法获取锁的状态 Lock 可以判断是否获取到锁
  3. Synchronized 会自动释放锁, Lock 必须手动解锁,如果不释放锁 就会死锁
  4. Synchronized 线程1(获得锁,阻塞),线程2(等待,死等),Lock锁不一定会等待
  5. Synchronized 可重入锁,不可以中断,非公平,Lock 可重入锁,可以判断锁,手动调整
  6. Synchronized 适合锁少量代码同步问题,Lock 适合锁大量的同步代码

锁是什么,如何判断锁的是谁

生产者和消费者问题

生产者和消费者问题:Synchronized 版

面试高频 : 单例模式,排序算法,生产者和消费者 死锁

package ProduceAndconsum;

/**
 * @projectName: JUC
 * @package: ProduceAndconsum
 * @className: A
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 21:08
 * @version: 1.0
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

//数字 资源类
// 1、 判断等待,通知
class Data {
    private int number = 0;

    //    +1
    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            //    等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //    通知其他线程 +1 完毕了
        this.notifyAll();
    }

    //    -1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {
            //    等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //    通知其他线程 -1 完毕了
        this.notifyAll();
    }
}

但是,写出来这个简单的模型 面试官还是会挑的出毛病来,问题出在那?

问题,现在我们是两个线程 ,我们加到四个线程 A,B,C,D 现在还是安全的吗?答案是肯定的不是

虚假唤醒问题:我们增加到四个线程输出的时候就会发现一些问题来,输出不再是0101了

image-20220301212425229

image-20220301212149214

这里如何解决呢? 将 if 换成 while循环

修改之后根据官方文档的解释之后,将if改编成while

image-20220301212605922

输出就又回到了正常

package ProduceAndconsum;

/**
 * @projectName: JUC
 * @package: ProduceAndconsum
 * @className: A
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 21:08
 * @version: 1.0
 */
public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

//数字 资源类
// 1、 判断等待,通知
class Data {
    private int number = 0;

    //    +1
    public synchronized void increment() throws InterruptedException {
        while (number != 0) {
            //    等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //    通知其他线程 +1 完毕了
        this.notifyAll();
    }

    //    -1
    public synchronized void decrement() throws InterruptedException {
        while (number == 0) {
            //    等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "=>" + number);
        //    通知其他线程 -1 完毕了
        this.notifyAll();
    }
}

生产者和消费者问题: JUC版

在新的学习中 synchronized 有 Lock 替换

那么 wait 和 notify谁来替换呢?

image-20220301212829262

通过 Lock来找到 Condition

image-20220301213020385

image-20220301213059259

package ProduceAndconsum;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @projectName: JUC
 * @package: ProduceAndconsum
 * @className: B
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 21:31
 * @version: 1.0
 */


public class B {
    public static void main(String[] args) {
        Data2 data = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

//数字 资源类
// 1、 判断等待,通知
class Data2 {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    //等待
    //    condition.await();
    //唤醒全部
    //    condition.signalAll();

    //    +1
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                //    等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            //    通知其他线程 +1 完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //    -1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                //    等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            //    通知其他线程 -1 完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
package ProduceAndconsum;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @projectName: JUC
 * @package: ProduceAndconsum
 * @className: B
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 21:31
 * @version: 1.0
 */


public class B {
    public static void main(String[] args) {
        Data2 data = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

//数字 资源类
// 1、 判断等待,通知
class Data2 {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    //等待
    //    condition.await();
    //唤醒全部
    //    condition.signalAll();

    //    +1
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                //    等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            //    通知其他线程 +1 完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //    -1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                //    等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            //    通知其他线程 -1 完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

任何的一个新技术,都不可能是单纯的去替代老的技术,一定有优化和补充

Condition

image-20220301214146773

我们更换完代码之后,可以正常输出但是还是混乱的,我们想要达到 A->B->C->D 这样输出,这个时候就引出了新的知识点

Conddition精准唤醒

package ProduceAndconsum;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @projectName: JUC
 * @package: ProduceAndconsum
 * @className: B
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/1 21:31
 * @version: 1.0
 */


public class C {
    public static void main(String[] args) {
        Data3 data = new Data3();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        }, "C").start();
    }
}

//数字 资源类
// 1、 判断等待,通知
class Data3 {
    private int number = 1;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();

    public void printA() {
        lock.lock();
        try {
            //    业务,判断 执行 通知
            while (number != 1) {
                //等待
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAA");
            number = 2;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            //    业务,判断 执行 通知
            while (number != 2) {
                //等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBB");
            number = 3;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            //    业务,判断 执行 通知
            while (number != 3) {
                //等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBB");
            number = 1;
            condition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

输出的结果就达到我们的预期了

image-20220301215538935

8锁现象

锁 ----> 刚new 出来的对象、class

8锁就是关于的锁的八个问题,下面也有四个demo类来阐述各种场景下锁的不同状态

demo1

两个问题:

正常模式下 在两个同步线程方法调用的时候 中途延时1s 会不会改变输出结果

答:不会影响输出顺序

正常模式下 同步方法内延时4s 会不会印象输出

答:不会影响输出顺序

  • 标准情况下 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话
  • 发短信延时四秒 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话

demo代码

package lock8;

import java.util.concurrent.TimeUnit;

/**
 * @projectName: JUC
 * @package: Lock8
 * @className: lock8Demo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 *  Lock8 就是关于锁的八个问题
 * 1. 标准情况下 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话
 * 2. 发短信延时四秒 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话
 * @date: 2022/3/2 1:13
 * @version: 1.0
 */
public class lock8Demo {
    public static void main(String[] args) {
        phone phone = new phone();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();
        //    捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.call();
        }, "B").start();
    }
}

class phone {
    //synchronized 锁的对象是方法调用者,
    //两个方法用的都是 phone 对象的锁,两个方法谁先拿到锁 谁执行
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }
}

demo2

同步方法执行和普通方法执行顺序

答 :普通方法没有锁,所以普通方法先

两个对象执行会不会影响顺序

答:会 两个不同的对象锁也是不同的 ,对象1 还在等待,对象2调用的call方法不用等待所以先输出

  • 现在新增一个普通方法 问 先发短信还是先发hello 先输出hello
  • 新增两个对象 是先打电话还是先发短信
package lock8;

import java.util.concurrent.TimeUnit;

/**
 * @projectName: JUC
 * @package: Lock8
 * @className: lock8Demo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 *  Lock8 就是关于锁的八个问题
 * 3、 现在新增一个普通方法  问 先发短信还是先发hello 先输出hello
 * 4、 新增两个对象 是先打电话还是先发短信
 * @date: 2022/3/2 1:13
 * @version: 1.0
 */
public class lock8Demo2 {
    public static void main(String[] args) {
        //两个对象,现在是两个调用者所以是两个锁
        phone2 phone = new phone2();
        phone2 phone1 = new phone2();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();
        //    捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone1.call();
        }, "B").start();
    }
}

class phone2 {
    //synchronized 锁的对象是方法调用者,
    //两个方法用的都是 phone 对象的锁,两个方法谁先拿到锁 谁执行
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }


    //因为这里没有锁,不受锁的影响
    public void hello() {
        System.out.println("hello");
    }
}

demo3

一个对象调用静态同步方法会不会改动执行顺序

答: 不会 ,static是锁的类模版全局唯一,不会改变锁的执行交换顺序

两个对象调用静态同步方法会不会改变执行顺序

答:不会 static修饰的是类模版,锁的也是类模板而不是类对象,只要是这个类生成的对象,不管多少个都不会改变顺序

  • 一个对象 添加两个静态的同步方法,只有一个对象 先打印 发短信 还是打电话?
  • 添加两个对象,分别调用静态同步方法 先打印 发短信 还是打电话?
package lock8;

import java.util.concurrent.TimeUnit;

/**
 * @projectName: JUC
 * @package: Lock8
 * @className: lock8Demo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 *  Lock8 就是关于锁的八个问题
 *  5、添加两个静态的同步方法,只有一个对象 先打印 发短信 还是打电话?
 *  6、 添加两个对象,增加两个同步方法 先打印  发短信 还是打电话?
 * @date: 2022/3/2 1:13
 * @version: 1.0
 */
public class lock8Demo3 {
    public static void main(String[] args) {

        //不管多少对象, 使用的都是底层的唯一class所以不管怎么怎么改变结果都不会变,
        // 因为synchroized锁的是class对象,static修饰的方法 类一开始就加载了,
        new Thread(() -> {
            Phone3.sendSms();
        }, "A").start();
        //    捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            Phone3.call();
        }, "B").start();
    }
}

//phone3是唯一的一个class
class Phone3 {
    //synchronized 锁的对象是方法调用者,
    //两个方法用的都是 phone 对象的锁,两个方法谁先拿到锁 谁执行
    // 这里 static 是静态方法 ,类一加载就有了,这个用的锁不再是 phone锁 而是class锁
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call() {
        System.out.println("打电话");
    }

}

demo4

  • 一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,因为锁的不是一个东西,sync锁的是类,static 是锁class
  • 两个对象 一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,

答: 不是同一个锁,谁执行快就输出谁,

package lock8;

import java.util.concurrent.TimeUnit;

/**
 * @projectName: JUC
 * @package: Lock8
 * @className: lock8Demo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 *  Lock8 就是关于锁的八个问题
 *  7、一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,因为锁的不是一个东西,sync锁的是类,static 是锁class
 *  8、两个对象 一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,
 * @date: 2022/3/2 1:13
 * @version: 1.0
 */
public class lock8Demo4 {
    public static void main(String[] args) {
        Phone4 phone = new Phone4();
        //不管多少对象, 使用的都是底层的唯一class所以不管怎么怎么改变结果都不会变,
        // 因为synchroized锁的是class对象,static修饰的方法 类一开始就加载了,
        new Thread(() -> {
            Phone4.sendSms();
        }, "A").start();
        //    捕获
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.call();
        }, "B").start();
    }
}

//phone3是唯一的一个class
class Phone4 {
    //静态同步方法是锁的 class
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    //这里普通同步方法锁的是 phone
    public synchronized void call() {
        System.out.println("打电话");
    }

}

小结

我们通过八种不同的情况来理解锁在不同情况下的执行情况:

  • 标准情况下 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话
  • 发短信延时四秒 两个线程打印 发短信还是打电话 1/ 发短信 2/打电话
  • 现在新增一个普通方法 问 先发短信还是先发hello 先输出hello
  • 新增两个对象 是先打电话还是先发短信
  • 一个对象 添加两个静态的同步方法,只有一个对象 先打印 发短信 还是打电话?
  • 添加两个对象,分别调用静态同步方法 先打印 发短信 还是打电话?
  • 一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,因为锁的不是一个东西,sync锁的是类,static 是锁class
  • 两个对象 一个静态方法和一个普通加锁方法, 先打印 发短信 还是打电话? 先出打电话,

大家可以自己写一下代码看一些这些问题的结果,实践出效果,实践出理解

锁的东西,无非就是对象和 类模版

  1. new 出来的对象 就是锁具体的对象,比如普通同步方法
  2. 带有static 修饰的静态同步方法 锁的是类模版是全局唯一的对象class如 : class

集合类不安全

List不安全

并发 arrayList是不安全的
解决方案:

1. vector 线程安全,因为新增方法前 带了 sync关键字,这个方法并不是最优解
2. 集合工具类 Collections.synchronizedList(new ArrayList<>());
3. JUC包下 CopyOnWriteArrayList<>()

CopyOnWrite 写入时复制,COW 计算机程序设计领域的一种优化策略

如 : 比如多个调用者调用同一个list,读取的时候,固定的,写入(覆盖)

在写入的时候 避免覆盖,造成数据问题; 读写分离

问题: 为什么不用vector 为什么呢?

答 : 因为只要使用 sync关键字,效率都会低一些,而CopyOnWriteArrayList,底层的方法是用的lock锁

//java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
    public static void main(String[] args) {
        //并发 arrayList是不安全的
        /*  解决方案
         * 1. vector 线程安全,因为新增方法前 带了 sync关键字,这个方法并不是最优解
         * 2. 集合工具类 Collections.synchronizedList(new ArrayList<>());
         * 3. JUC包下 CopyOnWriteArrayList<>()
         *  CopyOnWrite 写入时复制,COW 计算机程序设计领域的一种优化策略
         * 如 : 比如多个调用者调用同一个list,读取的时候,固定的,写入(覆盖)
         * 在写入的时候 避免覆盖,造成数据问题;
         * 读写分离
         *
         * 问题: 为什么不用vector 为什么呢? 因为只要使用 sync关键字,效率都会低一些,而CopyOnWriteArrayList,底层的方法是用的lock锁
         * */
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 600; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

写入时复制的add方法的原理

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock(); //上锁,只允许一个线程进入
        try {
            Object[] elements = getArray(); // 获得当前数组对象
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝到一个新的数组中
            newElements[len] = e;//插入数据元素
            setArray(newElements);//将新的数组对象设置回去
            return true;
        } finally {
            lock.unlock();//释放锁
        }
    }

Set不安全

解决方案 :

  • 首先就是 Collections工具类 Collections.synchronizedSet(new HashSet());
  • 就是和集合再同一个解决方案里的 Set set = new CopyOnWriteArraySet();
public class SetTest {
    //java.util.ConcurrentModificationException 同理和集合没有什么区别
    public static void main(String[] args) {
        /* 如何解决hashset 线程不安全?
         * 1. 首先就是 Collections工具类 Collections.synchronizedSet(new HashSet());
         * 2. 就是和集合再同一个解决方案里的  Set set = new CopyOnWriteArraySet();
         * */
        Set set = new CopyOnWriteArraySet();
        for (int i = 1; i <= 600; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

HashSet的底层 是 hashmap

public HashSet(){
    map = new HashMap<>();
}

//add set 的本质就是map key 是无法重复的
public boolean add(E e){
    return map.put(e,PRESENT)==null;
}

private static final Object PRESENT = new Object //不变的值

Map类不安全

HashMap原理简单回顾

image-20220302112806560

解决方案:

  • Map<String, String> map = new ConcurrentHashMap<>();
public class mapTest {
    //java.util.ConcurrentModificationException
    public static void main(String[] args) {
        //    map是这样用的吗 不是 工作中很少使用 Hashmap
        //    默认等价于什么 new HashMap<>(16,0.75);
        /*解决方案
         *
         * */
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 1; i <= 500; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }

    }
}

ConcurrentHashMap原理

​ ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentHashMap的内部结构

image-20220309163044870

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

Callable

image-20220302112839751

callable是什么?

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不容 run() /call()

入门案例

查看源码 我们发现 Thread 类所有的方法参数都是 Runnable 那我们怎么样可以使用callble呢?

image-20220302113326707

思路图:

image-20220302113344057

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //new Thread(new Runnable()).start();
        //new Thread(new FutureTask<V>()).start();
        //new Thread(new FutureTask<V>(callable)).start();
        //怎么启动callable
        new Thread().start();
        MyThread thread = new MyThread();
        //适配类
        FutureTask futureTask = new FutureTask(thread);
        new Thread(futureTask, "A").start();

        Integer o = (Integer) futureTask.get();
        System.out.println(o);
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("call()");
        return 123;
    }
}

细节

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //new Thread(new Runnable()).start();
        //new Thread(new FutureTask<V>()).start();
        //new Thread(new FutureTask<V>(callable)).start();
        //怎么启动callable
        new Thread().start();
        MyThread thread = new MyThread();
        //适配类
        FutureTask futureTask = new FutureTask(thread);
        new Thread(futureTask, "A").start();
        new Thread(futureTask, "B").start();

        // get 接口 可能会产生阻塞
        Integer o = (Integer) futureTask.get();
        System.out.println(o);
    }

这里我们用两个对象,他会输出几次?

image-20220302114220524

一次,为什么呢? 源码会告诉我们答案

 public void run() {
  //这里的判断 如果state 不是new 那么就会return   
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

很明显,我们两次开启的线程但是futureTask只会有一个线程run完,新开的话就会检测到状态不是new 就会return

常用辅助类

CountDownLatch

​ 线程减法计数器 递减

image-20220302135542300

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

常用方法

public void countDown()

递减锁存器的计数,如果计数到达零,则释放所有等待的线程。如果当前计数大于零,则将计数减少.

public boolean await(long timeout,TimeUnit unit) throws InterruptedException

使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。如果当前计数为零,则此方法立刻返回true值。

  如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下三种情况之一前,该线程将一直出于休眠状态, 由于调用countDown()方法,计数到达零;或者其他某个线程中断当前线程;或者已超出指定的等待时间。

  • 如果计数到达零,则该方法返回true值。
  • 如果当前线程,在进入此方法时已经设置了该线程的中断状态;或者在等待时被中断,则抛出InterruptedException,并且清除当前线程的已中断状态。
  • 如果超出了指定的等待时间,则返回值为false。如果该时间小于等于零,则该方法根本不会等待。

简单入门案例

//计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        //    总数是六
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "go out");
                //数量-1
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        //等待计数器归零
        countDownLatch.await();
        System.out.println("Close door");
    }
}

countDownLatch.countDown(); // 数量-1

countDownLatch.await(); //等待计数器归零

每次有线程调用countDown 数量 -1 假设计数器变为0,countDownLatch.await()就会被唤醒;继续执行

CyclicBarrier

线程 计数器 递增,是函数式接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vUiHo2n0-1664477615639)(https://gitee.com/cold-abyss_admin/my-image-host/raw/master/ img /image-20220302140300307.png)]

构造器

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

parties 是参与线程的个数

第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务

重要方法

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

解析:

  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

代码案例

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        /*集齐七龙珠 召唤神龙
         * 召唤龙珠线程
         * Cyclicbarrier 是函数式接口 ,所以我们可以直接用 lamba
         * */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功");
        });

        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
                //等待
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

SemaPhore

Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

举例理解

可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

image-20220302142104753

使用场景

通常用于那些资源有明确访问数量限制的场景,常用于限流 。

比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。

比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。

常用方法

acquire()  
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

acquire(int permits)  
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
    
acquireUninterruptibly() 
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
    
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。

tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。

release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

hasQueuedThreads()
等待队列里是否还存在等待线程。

getQueueLength()
获取等待队列里阻塞的线程数。

drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。

availablePermits()
返回可用的令牌数量。

代码案例

public class SemaPhoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                //得到
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放车位
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

读写锁

Synchronized存在一个性能问题就是不同读取之间互斥,我们想要实现的最好效果是可以做到读和读互不影响,写的时候只有一个线程能写

解决方案 : ReadWriteLock。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fFW9ShSx-1664477615640)(C:/Users/doomwstcher.000/AppData/Roaming/Typora/typora-user-images/image-20220302162455748.png)]

案例代码

package rwLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @projectName: JUC
 * @package: rwLock
 * @className: rwLockDemo
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/2 16:29
 * @version: 1.0
 */
public class rwLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");

            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

/**
 * 自定义缓存类
 * 加锁
 * */
class MyCache {
    //存放数据的集合
    private volatile Map<String, Object> map = new HashMap<>();

    //    存 写
    public void put(String key, Object value) {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕");
    }
    //    取 读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取" + "");
    }
}

image-20220302170621703

可以看到这并不是我们想要的效果,这个时候我们需要加锁

ReadWriteLock读写锁 分别有

readLock()读锁

writeLock()写锁

使用方式除了相比lock细化的一些其他没有变化

读写锁代码实例

思路理解 :

独占锁(写锁)

共享锁(读锁)

public class rwLockDemo {
    public static void main(String[] args) {
        //MyCache myCache = new MyCache();
        MyCacheLock myCache = new MyCacheLock();
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}
class MyCacheLock {
    //存放数据的集合
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁的区别, 更加细粒度的控制
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //    存 写
    public void put(String key, Object value) {
        //加入写锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            readWriteLock.writeLock().unlock();
        }
    }

    //    取 读
    public void get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取" + "");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

输出效果就达到了,先写且只有一个写,之后随意读

image-20220302182538999

阻塞队列

阻塞队列简介

什么是阻塞队列,我们要分开来理解

阻塞: 等待前面的走了才能加入新的

队列: 先进来的,先出去

image-20220302183347089

阻塞队列 在jdk文档中的 解释

image-20220302183907308

队列接口

我们学习的BlockingQueue也是实现类之一

什么时候我们会使用 阻塞队列

多线程 , 线程池 用的相对的多一点

image-20220302184326096

队列的类关系图

image-20220302184516163

阻塞队列相对的四组api

  1. 抛出异常api

        /** 会抛出异常的
         * java.lang.IllegalStateException: Queue full 会抛出队列已经满了的异常
         * java.util.NoSuchElementException  过多移除异常
         * */
        public static void test1() {
            ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
            System.out.println("===============多过加入================");
            System.out.println(blockingQueue.add("a"));
            System.out.println(blockingQueue.add("b"));
            System.out.println(blockingQueue.add("c"));
    
            ////    此时的队列长度为 3 如果我们此时加入 第四个会怎么样,抛出队列已经满了的异常
            //System.out.println(blockingQueue.add("b"));
            System.out.println("===============过多移除================");
            System.out.println(blockingQueue.remove());
            System.out.println(blockingQueue.remove());
            System.out.println(blockingQueue.remove());
            System.out.println(blockingQueue.remove());
        }
    
  2. 不会抛出异常api

    public static void test2() {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println("===============多过加入================");
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        //返回false
        System.out.println(blockingQueue.offer("d"));
        System.out.println("===============过多移除================");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //返回null
        System.out.println(blockingQueue.poll());
    
    }
    
  3. 阻塞等待 api

    /* 一直等待 阻塞
     * */
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        //blockingQueue.put 队列没有位置了 一支阻塞
        //blockingQueue.put("d");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        //m没有这个元素一直等待
        System.out.println(blockingQueue.take());
    }
    
  4. 超时等待 api

    /*等待
    等待超时*/
    public static void test4() throws InterruptedException {
        //队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        //等待,如果设置时间还没有空位置。否则结束
        blockingQueue.offer("d", 2, TimeUnit.SECONDS);
        System.out.println("======================");
        blockingQueue.poll();
        blockingQueue.poll();
        blockingQueue.poll();
        //等待,如果设置时间还没有找到。否则结束
        blockingQueue.poll(2, TimeUnit.SECONDS);
    
    }
    
方式抛出异常有返回值阻塞等待超时等待
添加操作addofferput()offer()
移除操作removepolltake()poll()
判断队列首元素elementpeek

同步队列

特性

同步队列,SynchronusQueue 同步队列 和其他的 BlockingQueue不一样 SynchronusQueue不存储元素

put了 一个元素 必须先从里面拿出来,否则是不能再put进去值

代码实例

public class synchronusQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {

            try {
                System.out.println(Thread.currentThread().getName() + "put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + "put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + "put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {

            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "=>" + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}

线程池

池化技术、

程序的运行 本质: 占用系统的资源 ! 优化资源的使用 =>池化技术

线程池,连接池,内存吃,对象池, 频繁的创建销毁 十分的浪费资源

线程池的好处:

  1. 降低资源的消耗
  2. 提高响应的速度
  3. 方面管理

==线程的复用 可以控制最大并发数量,管理线程==

三大方法

下图来自 阿里巴巴开发规约手册

image-20220302213232478

代码实例

public class poolDemo {
    public static void main(String[] args) {
        //单个线程
        ExecutorService Threadpool = Executors.newSingleThreadExecutor();
        // 创建一个固定的线程池大小
        //ExecutorService Threadpool = Executors.newFixedThreadPool(5);
        //可以伸缩的 遇强则强
        // ExecutorService Threadpool = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 10; i++) {
                Threadpool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "=> ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池使用完毕 一定要关闭
            Threadpool.shutdown();
        }
    }
}

三大方法的创建代码

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

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

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

七大参数

七大参数

public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
                          int maximumPoolSize,// 最大核心线程数大小
                          long keepAliveTime,// 超时了没有人调用就会释放
                          TimeUnit unit,// 超时单位
                          BlockingQueue<Runnable> workQueue,// 阻塞队列
                          ThreadFactory threadFactory, // 线程工厂,创建线程的
                          RejectedExecutionHandler handler// 拒绝策略
                         ) 
   

这七个参数分别有什么作用呢,思路图

image-20220303182515651

手动创建线程池,不用封装好的方法,使用原生的线程池方法

ThreadPoolExecutor threadpool = new ThreadPoolExecutor(
        2,
        5,
        3,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(3),
        Executors.defaultThreadFactory(),
        // 这个时候 举例子,银行的人满了 这种方式就是 不处理 抛出异常
        new ThreadPoolExecutor.AbortPolicy()
);

线程池四种拒绝策略

  • new ThreadPoolExecutor.AbortPolicy() 银行的人满了 这种方式就是 不处理 抛出异常

    执行效果

    image-20220303183829048

  • new ThreadPoolExecutor.CallerRunsPolicy() 银行人满了,哪里来的去哪里,回到调用线程输出,不会异常

    执行结果

    image-20220303183923829

  • new ThreadPoolExecutor.DiscardPolicy() 队列满了 就抛出全部任务,

    执行结果

    image-20220303184035431

  • new ThreadPoolExecutor.DiscardOldestPolicy() 尝试和最早的线程竞争 查看是否有位置,没有就抛出任务

    执行结果

    image-20220303184127445

代码实例

public class poolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadpool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );


        //单个线程
        //ExecutorService Threadpool = Executors.newSingleThreadExecutor();
        // 创建一个固定的线程池大小
        //ExecutorService Threadpool = Executors.newFixedThreadPool(5);
        //可以伸缩的 遇强则强
        // ExecutorService Threadpool = Executors.newCachedThreadPool();

        try {
            //最大承载如何计算 : 阻塞队列+max数量
            //超过的话就会 爆出异常 :RejectedExecutionException
            for (int i = 1; i <= 15; i++) {
                threadpool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "=> ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池使用完毕 一定要关闭
            threadpool.shutdown();
        }
    }
}

最大负载

  1. CPU密集型 有几个核心就定义几个,可以保证效率最高

        Runtime.getRuntime().availableProcessors() //获取cpu 核心数
    
  2. IO 密集型 判断程序中 十分消耗IO资源的线程,如: 程序 有 15个大型任务,io 十分占中资源,那么设定的比任务数量大 就可以保证一定性能、

ForkJion

什么是ForkJoin

ForkJoin 下 JDK 1.7 并行执行任务的,数量越大,效率越高

比如 :大数据 Map Reduce(把大任务拆分成小任务)

image-20220304004113183

ForkJoin 特点: 工作窃取

举例子:

PS: 维护的是双端队列 Deuue

A线程执行任务到 第二个

B线程执行完毕,那么B线程回去讲A线程的东西拿来执行,从而提高效率

image-20220304004235249

认识forkjion

ForkJoin 使用两个类来完成以上两件事情:

  • ForkJoinTask:我们要使用 ForkJoin 框架,必须首先创建一个 ForkJoin 任务。它提供在任务中执行 fork() 和 join() 操作的机制,通常情况下我们不需要直接继承 ForkJoinTask 类,而只需要继承它的子类,Fork/Join 框架提供了以下两个子类:
    • RecursiveAction:用于没有返回结果的任务。
    • RecursiveTask :用于有返回结果的任务。
  • ForkJoinPool :ForkJoinTask 需要通过 ForkJoinPool 来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

image-20220304005022113

代码实例

task 类 里面编写的是我们继承了 递归任务继承的实现方法

public class forkjoinDemo extends RecursiveTask<Long> {
    /* 解决方案 也是有三六九等的,比如案例 求和
     * 最低等 就是直接for循环求和
     * 中等 使用forkjion
     * 高等 stream 并行流
     * */
//开始
    private long start;
    //结束
    private long end;
    //到多少值,才开始分开任务
    private long threshold = 10000L;

    public forkjoinDemo(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        //判断超过阈值的时候 开始使用 fork join
        if (end - start > threshold) {
            long sum = 0L;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            //    求出中间值
            long mid = (start - end) / 2;
            forkjoinDemo task1 = new forkjoinDemo(start, mid);

            //拆分任务,把任务压入线程队列
            task1.fork();
            forkjoinDemo task2 = new forkjoinDemo(mid + 1, end);
            task2.fork();
            return task1.join() + task2.join();
        }
    }
}

测试类

三种方法的速度

public class test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //test1(); 7042;
        //test2(); 969
        //test3(); 179;
    }

    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (long i = 1L; i <= 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum" + sum + "=> 执行时间" + (end - start));
    }


    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new forkjoinDemo(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("sum" + sum + "=> 执行时间" + (end - start));
    }

    public static void test3() {

        long start = System.currentTimeMillis();
        //并行流
        long reduce = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum" + reduce + "=> 执行时间" + (end - start));
    }
}

异步回调

什么是future

常见的两种创建线程的方式。一种是直接继承Thread,另外一种就是实现Runnable接口。

这两种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

Future模式的核心思想是能够让主线程将原来需要同步等待的这段时间用来做其他的事情。(因为可以异步获得执行结果,所以不用一直同步等待去获得执行结果)

image-20220304014056949

上图简单描述了不使用Future和使用Future的区别,不使用Future模式,主线程在invoke完一些耗时逻辑之后需要等待,这个耗时逻辑在实际应用中可能是一次RPC调用,可能是一个本地IO操作等。B图表达的是使用Future模式之后,我们主线程在invoke之后可以立即返回,去做其他的事情,回头再来看看刚才提交的invoke有没有结果。

Future接口的局限性

当我们得到包含结果的Future时,我们可以使用get方法等待线程完成并获取返回值,注意我加粗的地方,Future的get() 方法会阻塞主线程。即使我们使用isDone()方法轮询去查看线程执行状态,但是这样也非常浪费cpu资源。

image-20220304014316398

我们需要新的,更强大的拓展,CompletableFuture

在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,结合了Future的优点,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

CompletableFuture被设计在Java中进行异步编程。异步编程意味着在主线程之外创建一个独立的线程,与主线程分隔开,并在上面运行一个非阻塞的任务,然后通知主线程进展,成功或者失败。

通过这种方式,你的主线程不用为了任务的完成而阻塞/等待,你可以用主线程去并行执行其他的任务。 使用这种并行方式,极大地提升了程序的表现。

实例化:

有两种格式,一种是supply开头的方法,一种是run开头的方法

  • supply开头:这种方法,可以返回异步线程执行之后的结果
  • run开头:这种不会返回结果,就只是执行线程任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor);

public static CompletableFuture<Void> runAsync(Runnable runnable);
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor);

获取结果

同步获取结果

public T    get()
public T    get(long timeout, TimeUnit unit)
public T    getNow(T valueIfAbsent)
public T    join()

简单的例子

CompletableFuture<Integer> future = new CompletableFuture<>();
Integer integer = future.get();

get() 方法同样会阻塞直到任务完成,上面的代码,主线程会一直阻塞,因为这种方式创建的future从未完成。有兴趣的小伙伴可以打个断点看看,状态会一直是not completed

代码使用案例

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        ////没有返回值的异步回调, runAsync
        //CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
        //    System.out.println(Thread.currentThread().getName() + "runAsync=> Void");
        //});
        //System.out.println("1111");
        ////获取执行结果
        //completableFuture.get();
     
     
        //    有返回值的
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "runAsync=>integer");
            int i = 10 / 0;
            return 1024;
        });
        completableFuture.whenComplete((t, u) -> {
            //t是正常的返回结果
            //u是返回报错信息
            System.out.println("t=>" + t);
            System.out.println("u=>" + u);
        }).exceptionally((e) -> {
            System.out.println(e.getMessage());
            return 233;
        }).get();
    }

JMM

谈谈队 Volatile的理解

Volatile 是 java 虚拟机的轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是Jmm

JMM就是Java内存模型(java memory model) Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

jmm的一些同步约定

  1. 线程解锁前 必须把共享变量 立刻刷新回主缓存
  2. 线程加锁 前,必须读取主内存中的最新值到工作中
  3. 加锁和解锁必须是同一把锁

线程 : 工作内存和 主内存

Jmm中的八种操作:

image-20220304163433104

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

Volatile

验证 这个关键字的特性

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1 可见性

    //volatile 之后 就会循环停止了,验证了特性 1 可见性
    private volatile static int num = 0;

    public static void main(String[] args) {
        //此时循环 没有结束 为什么? 因为线程队主内存的变化并不知道
        new Thread(() -> {
            while (num == 0) {

            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
    }

2 不保证原子性

使用 synchronized 关键字 保证原子性 结果为 20000

   private static int num = 0;

    public synchronized static void add() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

使用 Volatile 不保证原子性 17824 每次出来的都不一样,可能是两万 概率不大

    private volatile static int num = 0;

    public static void add() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

问题 如果不上锁 也不用 关键字 ,我们怎么保证原子性

image-20220304165005009

使用JUC 包中的 原子类 来解决 原子性问题

比如 int : 替换成 原子类 AtomicInteger

 private volatile static AtomicInteger num = new AtomicInteger(0);

    public static void add() {
        //num++;
        // incr 自增
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

保证了原子操作 输出20000

image-20220304165350036

这些原子类的底层直接和操作系统挂钩,在内存中修改值,Unsafe 类是一个中很特殊的存在

指令重排

什么是 指令重排: 我们写的程序 计算机不一定会按照我们写的顺序来执行

源代码 -> 编译器优化重排 -> 指令并行可能会重排->内存系统也会重排-->执行

==处理器再进行指令重排的时候 考虑数据之间的依赖性==

int x = 1;// 1
int y = 2;// 2
x = x + 5;// 3
y = x * x;// 4

我们期待的是 1234, 可能执行的时候变成了 2134 1324
但是不可能是 4123

可能 造成影响最后的执行结果 a b x y 默认值都是 0

image-20220304170008300

正常的结果 : x=0;y=0; 可能由于指令重排

image-20220304170120677

指令重排 导致的奇怪结构 = x=2 y=1

volatile 可以避免指令重排

内存屏障 CPU 指令 作用:

  1. 保证特定操作的执行顺序
  2. 可以保证某些变量的内存可见性

image-20220304170643130

单例模式

饿汉式

public class Hungryemo {
    private Hungryemo() {
    }

    private final static Hungryemo HUNGRYEMO = new Hungryemo();

    public static Hungryemo getIstance() {
        return HUNGRYEMO;
    }
}

DCL懒汉式

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @projectName: JUC
 * @package: single
 * @className: LazyMan
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/4 17:09
 * @version: 1.0
 */
public class LazyMan {
    private static boolean flag = true;

    private LazyMan() {
        synchronized (LazyMan.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
            if (lazyMan != null) {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }

    //必须 加上 volatile 避免 指令重排
    private volatile static LazyMan lazyMan;

    // 双重检测锁模式 的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                    //    会有问题 不是一个原子性操作
                    /* 1. 分配内存空间
                     * 2 。 执行构造对象 初始化对象
                     * 3. 把这个对象指向空间
                     *
                     * 比如 我们期望执行的是 123
                     * 但是 指令重排 执行是 132
                     * a 线程可能没有问题,
                     * b 就会以为已经完成构造,指向就是null 可能会空指针
                     * */
                }
            }
        }
        return lazyMan;
    }

    //但是会有并发问题
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSngle> field = EnumSngle.class.getDeclaredConstructor(String.class, int.class);
        field.setAccessible(true);
        //LazyMan lazyMan = field.newInstance();
        //LazyMan lazyMan1 = field.newInstance();
        EnumSngle lazyMan = field.newInstance();
        EnumSngle lazyMan1 = field.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}

静态内部类

public class Holder {
    private Holder() {
    }

    public static Holder getInstance() {
        return innerClass.holder;
    }

    public static class innerClass {
        private final static Holder holder = new Holder();
    }


}

枚举

//enum 是什么? 本身也是一个 Class 类
public enum EnumSngle {
    INSTANCE;

    private EnumSngle() {
    }

    public EnumSngle getInstance() {
        return INSTANCE;
    }
}
class test{
        //但是会有并发问题
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSngle> field = EnumSngle.class.getDeclaredConstructor(String.class, int.class);
        field.setAccessible(true);
        //LazyMan lazyMan = field.newInstance();
        //LazyMan lazyMan1 = field.newInstance();
        EnumSngle lazyMan = field.newInstance();
        EnumSngle lazyMan1 = field.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}

深入理解CAS

什么是 CAS

CAS compareAndSet 比较并交换

研究底层,才会有所突破

实例代码

    //CAS compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        // 两个参数 : 期望 更新
        //   public final boolean compareAndSet(int expectedValue, int newValue)
        // 如果我们的期望值达到了 那么就更新,否则 就不更新 CAS 是 CPU 并发原语
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
    }

加一 方法底层原理 为什么效率高

  1. 调用了 unsafe 操作内存的方法
  2. 查看这个getAndAddInt这个方法的参数, var 1 就是原本数字 var 2 就是valueoffset ,var 4 就是要增加多少
  3. var 是获取内存值,之后调用方法 如果 var1 和 var2 的结果是我们想要的 也就是 var5 那么就讲 var5+var4 也就是原本的结果 +1

image-20220304173908384

这个方法是一个典型的自旋锁

CAS:比较当前工作内存中的值,如果这个值是期望的,那么执行操作,如果不是就一直循环

缺点:

  1. 循环会耗时
  2. 一次性只能保证一个共享变量
  3. ABA问题

unsafe类

image-20220304173602223

CAS ABA问题

A:期望是 1 交换成2 ,但是在还没有交换的时候 另一个线程 把 当前的a 改变成了 3 又改回 1 此时 当前A线程依旧可以正常的交换,但是期间的值已经被别人用过了。

image-20220304224113617

    //CAS compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //对于 我们平时都 sql 是如何解决的,乐观锁
        // 两个参数 : 期望 更新
        //   public final boolean compareAndSet(int expectedValue, int newValue)
        // 如果我们的期望值达到了 那么就更新,否则 就不更新 CAS 是 CPU 并发原语
        //=============捣乱的线程====================
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
        atomicInteger.compareAndSet(2021, 2020);
        System.out.println(atomicInteger.get());

        //=================期望的线程=============
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
    }

原子引用(解决aba问题)

解决ada问题

原子引用 AtomicStampedReference

可以理解为 乐观锁

==PS: integer 使用了 对象缓存机制,默认范围是 -128-127 ,推荐使用静态工厂方法 valueof 获取对象实例,而不是new ,因为 value of 使用缓存,而new 一定会创建心的对象分配新的内存空间==

image-20220305021114344

代码实例

  //CAS compareAndSet 比较并交换
    public static void main(String[] args) {
        //AtomicInteger atomicStampedReference = new AtomicInteger(2020);
        // 注意 如果 泛型是一个包装类,注意对象的引用问题,
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("a1 =>" + stamp);
            System.out.println("a2 =>" + atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2 =>" + atomicStampedReference.getStamp());
            System.out.println("a3 =>" + atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a3 =>" + atomicStampedReference.getStamp());
        }, "a").start();
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println("b1 =>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("b2 =>" + atomicStampedReference.compareAndSet(1, 6, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("b2 =>" + atomicStampedReference.getStamp());
        }, "b").start();
    }

对于锁的理解

1、公平锁和非公平锁的区别

公平锁 :不能够插队,必须先来后到

非公平锁: 可以插队 锁 默认的都是非公平的

    public ReentrantLock() {
        sync = new NonfairSync();
    }

也可以修改成 公平锁

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

2、可重入锁

image-20220305021641302

sync关键字

这里 是一把锁,每次执行的时候直到方法里逐层向外解锁

public class lockdemo {
    public static void main(String[] args) {
        phone phone = new phone();
        new Thread(() -> {
            phone.sms();
        }, "a").start();
        new Thread(() -> {
            phone.sms();
        }, "b").start();
    }
}

class phone {
    public synchronized void sms() {
        System.out.println(Thread.currentThread().getName() + "=>发短信");
        //这里也有锁
        call();
    }

    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "=>打电话");
    }
}

Lock

  1. lock 锁每个方法是配对一个锁,像下面的例子就是开了两个锁,锁必须配对
public class lockdemo2 {
    public static void main(String[] args) {
        phone2 phone = new phone2();
        new Thread(() -> {
            phone.sms();
        }, "a").start();
        new Thread(() -> {
            phone.sms();
        }, "b").start();
    }
}

class phone2 {
    //这里就有区别 sync关键字是一个锁,这里使用lock 是两个锁,锁必须配对,否则就会死锁
    Lock lock = new ReentrantLock();

    public synchronized void sms() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "=>发短信");
            //这里也有锁
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public synchronized void call() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "=>打电话");
            //这里也有锁
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3、自旋锁

这里我们之前查看CAS 的时候 有一个调用自增的方法就是自旋锁

image-20220305022601565

自己的简易自旋锁

public class spinlocks {
    //    int =0
//    thread = null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //    加锁操作
    public void mylock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "=> mylock");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    //    解锁
    public void myunlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "=> myUnlock");
        atomicReference.compareAndSet(thread, null);
    }
}

class test {
    public static void main(String[] args) throws InterruptedException {
        //底层使用 CAS 自旋锁
        spinlocks lock = new spinlocks();
        new Thread(() -> {
            lock.mylock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myunlock();
            }
        }, "t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            lock.mylock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.myunlock();
            }
        }, "t2").start();
    }
}

4、死锁

什么是死锁: 互相争抢锁的过程

image-20220305023700803

死锁测试,如何排查死锁

public class DeadLockDemo {
    public static void main(String[] args) {
        String a = "lockA";
        String b = "lockB";
        new Thread(new mythread(a, b), "t1").start();
        new Thread(new mythread(b, a), "t2").start();
    }
}

class mythread implements Runnable {
    private String a;
    private String b;

    public mythread(String a, String b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public void run() {
        synchronized (a) {
            System.out.println(Thread.currentThread().getName() + "lock=>" + a + "lock=>" + b);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (b) {
                System.out.println(Thread.currentThread().getName() + "lock=>" + b + "lock=>" + a);
            }
        }
    }
}

解决问题

1、使用 ==jps -l== 定位 进程

image-20220305025155109

2、 使用==jstack==+进程号 2916

image-20220305025252482

面试或者工作中,排查锁的问题:

  1. 日志 百分之九十
  2. 堆栈 百分之十