Loading

juc并发编程

准备工作

创建项目

新建一个名为juc的普通maven项目

image-20240109183530715

导入依赖包

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.8</version>
    </dependency>
</dependencies>

环境检查

工程需要使用jdk8版本的新特性,所以要使用jdk1.8

image-20240109183730554

image-20240109183856018

什么是juc

JUC指的是:Java里的三个包

  • java.util.concurrent 并发包
  • java.util.concurrent.atomic:原子性
  • java.util.concurrent.locks:lock锁

下载官方文档

image-20240109184749123

在本地的位置

image-20240109185133382

rt.jar包下

image-20240109185226864

image-20240109185313993

线程和进程

进程、线程

进程

  • 进程是操作系统中的应用程序,是资源分配的基本单位

线程

  • 线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位。

关系

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

image-20240109190731996

java默认有两个线程:main线程、GC垃圾回收线程

对于Java而言:Thread、Runable、Callable进行开启线程的。

java真的可以开启线程吗?

不可以

Thread.start()

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        // 查看start0方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

// start0方法
// native 本地方法,超过java的控制范围,所以java不能开启线程
private native void start0();

并发、并行

并发编程:并发、并行;

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

  • CPU一核,模拟出来多条线程,快速交替。

并行(多个人一起行走)

  • CPU多核,多个线程可以同时执行;线程池。

查看电脑支持多少个线程

任务管理器(4个)

image-20240109191727690

设备管理器(4个)

image-20240109191841139

使用代码查看

public static void main(String[] args) {
    System.out.println(Runtime.getRuntime().availableProcessors());
}
// 打印输出
4

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

回顾多线程

线程有几个状态

public enum State {
	// 新生
    NEW,
    // 运行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待(死死的等)
    WAITING,
    // 超时等待(超多指定的时间就不再等)
    TIMED_WAITING,
    // 终止
    TERMINATED;
}

wait方法和sleep方法的区别

  • 来自不同的类

    • wait=》Object

    • sleep=》Thread

  • wait释放锁,sleep不释放锁(sleep抱着锁睡觉)

  • wait必须在同步代码块中,sleep可以在任何地方(sleep可以在任何地方睡觉)

  • wait不需要捕获异常,sleep需要捕获异常(可能发生超时等待)

Lock锁(重点)

传统的Synchronized锁

/**
 * 不加Synchronized
 * 真正的多线程开发
 * 线程就是一个资源类,没有任何附属的操作
 */
public class SaleTicketDemo01 {

    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 = 30;
    //买票的方式
    public void sale(){
        if (number > 0){
            System.out.println(Thread.currentThread()
                               .getName()+"卖出了第"+(number--)+"票,剩余:"+number);
        }
    }
}

结果是乱的:

image-20240109195832538

加上Synchronized

//synchronized  本质:队列,锁
public synchronized void sale(){
    if (number > 0){
        System.out.println(Thread.currentThread()
                           .getName()+"卖出了第"+(number--)+"票,剩余:"+number);
    }
}

结果:没有乱

image-20240109200029652

Lock锁(接口)

jdk官方文档

image-20240109200630105

公平锁、非公平锁

image-20240109201417657

查看ReenTrantLock源码

image-20240109201544049

公平锁:十分公平,必须先来后到;

非公平锁:十分不公平,可以插队; (默认为非公平锁)

用lock锁实现买票实例

public class SaleTicketDemo01 {

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

// lock三部曲
// 1、Lock lock=new ReentrantLock();
// 2、lock.lock() 加锁
// 3、finally=> 解锁:lock.unlock();
class Ticket{
    //属性,方法
    private int number = 30;
    //创建锁
    private Lock lock=new ReentrantLock();

    //买票的方式
    public void sale(){
        // 加锁
        lock.lock();
        try{
            if (number > 0){
                System.out.println(Thread.currentThread()
                .getName()+"卖出了第"+(number--)+"票,剩余:"+number);
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

Synchronized和Lock区别

  • Synchronized 是内置的java关键字,Lock 是一个接口;

  • Synchronized 无法判断获取锁的状态,Lock 可以判断;

  • Synchronized 会自动释放锁,Lock必须要手动加锁和手动释放锁!(若不释放锁,可能会造成死锁);

  • 阻塞、等待的区别

    • Synchronized
      • 线程1获得锁,线程2会等待。如果线程1阻塞了,线程2因为没有获得锁会一直在等待
    • Lock
      • Lock 不会一直等待下去,lock有一个trylock()方法尝试获取锁,不会造成长久的等待;
  • 锁类型

    • Synchronized 是可重入锁,不可以中断的,非公平锁。

    • Lock是可重入锁,可以中断锁,可以自己设置公平锁和非公平锁;

  • Synchronized 适合锁少量的代码同步问题,Lock适合锁大量同步代码问题;

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

生产者和消费者的关系

Synchronized版本

A B两个线程

/**
 * 线程间的通信问题:生产者和消费者的问题!  等待唤醒:wait() 通知唤醒:notifyAll();
 * 线程交替执行  A B同时操作一个变量
 * A num+1
 * B num-1
 */
public class A {
    public static void main(String[] args) {
        // 线程A +1
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // 线程B -1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}


// 生产者、消费者设计模式口诀
//  判断等待
//  业务
//  通知
class Data{
    private int num = 0;

    //生产者  +1
    public synchronized void increment() throws InterruptedException {
        //判断等待
        if (num != 0){
            // 等待,会释放锁
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

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

结果:

image-20240109210605913

问题存在,上述场景只有A、B两个线程,如果现在再加C、D线程,是否还安全

A B C D 四个线程

代码测试

A B +1操作,C D -1操作

/**
 * 线程间的通信问题:生产者和消费者的问题!  等待唤醒:wait() 通知唤醒:notifyAll();
 * 线程交替执行  A B C D同时操作一个变量
 * A num+1
 * B num+1
 * C num-1
 * D num-1
 */
public class A {
    public static void main(String[] args) {

        // 线程A +1
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // 线程B +1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        // 线程C -1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        // 线程D -1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }
}

// 生产者、消费者设计模式口诀
//  判断等待
//  业务
//  通知
class Data{
    private int num = 0;

    //生产者  +1
    public synchronized void increment() throws InterruptedException {
        //判断等待
        if (num != 0){
            // 等待,会释放锁
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

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

image-20240109211437440

存在虚假唤醒问题

虚假唤醒

参考文章:https://blog.csdn.net/weixin_45668482/article/details/117373700

image-20200810224826214

解决方式: if改成while即可,防止虚假唤醒

// 生产者、消费者设计模式口诀
//  判断等待
//  业务
//  通知
class Data{
    private int num = 0;

    //生产者  +1
    public synchronized void increment() throws InterruptedException {
        //判断等待
        while (num != 0){
            // 等待,会释放锁
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + "=>" + num);
        //通知其他线程 我+1完毕了
        this.notifyAll();
    }

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

image-20240109213026594

结论

synchronized:

  • 保证拥有这个方法的对象锁的线程,才能执行这个方法。其他线程因为没有这个对象锁,所以阻塞
  • 这样就保证了synchronized修饰的同步方法只能有一个线程执行

wait():

  • 能够让当前线程等待,并释放拥有的对象锁。
  • 当被其他线程唤醒后,会进入就绪状态,重新和其他线程抢夺cpu资源

notifyAll():

  • 会唤醒拥有调用这个方法的对象的对象锁,并处于等待状态的其他线程
  • 被唤醒的线程处于就绪状态

为什么使用if会造成虚假唤醒

//生产者  +1
public synchronized void increment() throws InterruptedException {
    //判断等待
    if (num != 0){
        // 等待,会释放锁
        this.wait();
    }
    num++;
    System.out.println(Thread.currentThread().getName() + "=>" + num);
    //通知其他线程 我+1完毕了
    this.notifyAll();
}

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

想象一下这样的一个场景:

num的初始值是0,首先A线程先获得cpu资源,执行+1方法后,四个线程都在就绪状态。此时如果A线程再次获得cpu资源,num的值此时为1,所以A线程会进入等待,并释放对象锁。B C D线程在就绪状态,此时B线程获取cpu资源,获得对象锁,但是num值为1,所以线程B进入等待状态。当某一个时机,C或者D线程消费完毕,num值为0,A B线程被唤醒,进入就绪状态,如果A线程再次执行+1方法,会直接执行上次等待之后的代码,直接进行+1,num值为1,执行完毕进入就绪状态。如果此时B线程执行+1方法,也会从上次的等待代码之后执行+1操作,此时num值为2,造成数据错误

为什么使用while不会造成虚假唤醒

//生产者  +1
public synchronized void increment() throws InterruptedException {
    //判断等待
    while (num != 0){
        // 等待,会释放锁
        this.wait();
    }
    num++;
    System.out.println(Thread.currentThread().getName() + "=>" + num);
    //通知其他线程 我+1完毕了
    this.notifyAll();
}

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

还是同样的场景:

A线程首先会+1,num值为1。此时A线程再次执行会进入等待状态。B线程执行也会进入等待状态。当某个时机,C、D线程消费之后,num值为0。唤醒A B线程,A线程继续执行等待之后的代码,进入while循环判断,此时因为num的值为0,跳出循环,进行+1操作,num值为1。B线程执行等待之后的代码,进入while循环判断,此时因为num的值为1,进入循环,继续等待。保证了数据的安全。

Lock版本

Synchronized和Lock两个版本实现比较

image-20200811094721678

Condition

image-20240110113916674

代码实现

public class B {
    public static void main(String[] args) {

        // 线程A +1
        Data2 data = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        // 线程B +1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        // 线程C -1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

        // 线程D -1
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();

    }
}

// 生产者、消费者设计模式口诀
//  判断等待
//  业务
//  通知
class Data2{
    private int num = 0;

    // 创建锁
    Lock lock = new ReentrantLock();
    // 对象监视器
    Condition condition = lock.newCondition();

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

    }

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

运行结果:

image-20240110120146379

从运行结果,我们可以看到Lock版本和Synchronized版本没有什么区别。

但是,任何一个新的技术,绝对不仅仅只是覆盖了原来的技术,而是有了新的优势和补充

Condition

从上面的实现可以看到,线程是随机的状态

image-20240110120639738

现在我们想让线程有序的执行(A->B->C->D),该怎么做

精确的通知和唤醒线程

public class C {
    public static void main(String[] args) {
        Data3 data = new Data3();
        // 三个线程分别执行data中的三个方法
        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();
    }
}

// 资源类
class Data3 {
    // num=1的时候 线程A执行;2:B执行;3:C执行
    private int num = 1;
    // 创建锁
    private Lock lock = new ReentrantLock();
    // 定义多个监视器
    // A线程使用condition1
    private Condition condition1 = lock.newCondition();
    // B线程使用condition1        
    private Condition condition2 = lock.newCondition();
    // C线程使用condition1    
    private Condition condition3 = lock.newCondition();

    public void printA() {
        // 加锁
        lock.lock();
        try {
            // 业务代码:判断等待->执行业务->通知
            while (num != 1) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "==>AAAAAAAAA");
            num++;
            // 唤醒指定的线程,这里唤醒B线程
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    
    public void printB() {
        // 加锁
        lock.lock();
        try {
            // 业务代码:判断等待->执行业务->通知
            // num=2,B线程执行
            while(num!=2){
                // 线程B等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "==>BBBBBBBBB");
            num++;
            // 唤醒指定的线程,这里唤醒C线程
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    
    public void printC() {
        // 加锁
        lock.lock();
        try {
            // 业务代码:判断等待->执行业务->通知
            while(num!=3){
                // C线程等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "==>CCCCCCCCC");
            num=1;
            // 唤醒A线程
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

8锁现象

如何判断锁的是谁!永远的知道什么是锁,锁到底锁的是谁

深刻理解锁

8锁,就是关于锁的8个问题,下面进行案例分析

案例分析

问题1

标准测试代码

Test1

/*
 * 定义两个线程A、B
 * 两个线程操作同一个资源 Phone
 * 	A线程指定发短信的方法
 * 	B线程执行打电话的方法
 */
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone.call();},"A").start();
    }
}
// 资源类
class Phone{
    public synchronized void sendSms(){
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

标准测试代码情况下,两个线程先打印 发短信 还是 打电话?结果:1/发短信 2/打电话

原因:

两个方法用的是同一个锁,synchronized 锁的是方法的调用者(也就是phone对象),所以哪个线程先拿到这个对象锁,谁就先执行。这里是A线程先拿到这个对象锁,所以先执行发短信

问题2

在标准测试代码的情况下,让A线程延迟4秒

/*
* 两个线程 一个对象 两个同步方法 线程睡眠
*/
public synchronized void sendSms(){
    try {
        // A线程睡4秒
        TimeUnit.SECONDS.sleep(4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("发短信");
}

两个线程先打印 发短信 还是 打电话?结果:1/发短信 2/打电话

原因:

两个方法用的是同一个锁,锁的还是方法的调用者(也就是phone对象)。首先,还是A线程先获取了phone这个对象锁,先执行。在执行时,虽然睡了4秒,但是sleep方法不会释放线程占用的这个对象锁。即使B线程此时在就绪状态,因为没有获得对象锁,所以不会执行。显然,还是A线程先执行,执行完毕释放对象锁,B线程再执行。

问题3

在问题2的基础上,给资源类新添加一个hello的普通方法,让线程B调用这个方法

Test2

/*
* 两个线程 一个对象 一个同步方法 一个普通方法
*/
public class Test2 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone.hello();},"B").start();
    }
}
// 资源类
class Phone2{
    public synchronized void sendSms(){
        try {
            // A线程睡4秒
            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");
    }
}

image-20240110133433188

两个线程先打印 发短信 还是 hello?结果:1/hello 2/发短信

原因:

hello这个方法没有锁,所以不受锁的影响。在本测试代码中,还是A线程先获得这个对象锁,先执行发短信的方法,但是在执行的时候睡了4秒,在睡的过程中,B线程早已经创建并且在就绪的状态,因为不用获得phone对象锁就能执行hello方法,所以先打印的 hello,在打印的 发短信

问题4

在问题3的代码基础上,创建一个phone2对象,让A线程执行phone1的发短信方法,让B线程执行phone2的打电话方法

/**
 * 两个线程,两个对象,执行不同对象的同步方法
 */
public class Test2 {
    public static void main(String[] args) {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone1.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone2.call();},"B").start();
    }
}

image-20240110135149425

两个线程先打印 发短信 还是 打电话?结果:1/打电话 2/发短信

原因:

两个方法使用的是不同的锁,一个锁锁的对象是phone1,另一个锁锁的对象是phone2,两个线程拿到的锁不一样,不会受到对方的影响。A线程先获取phone1对象锁,先执行发短信的方法,执行过程中睡眠。此时B线程获取phone2的对象锁,执行打电话的方法,在A线程还在睡眠的过程中,B线程就将方法执行完毕了。所以先 打电话,再 发短息

问题5

在问题2测试代码的基础上,将资源类的发短信方法和打电话方法改成static方法

Test3

/*
* 两个线程,一个对象,两个静态方法
*/
public class Test3 {
    public static void main(String[] args) {
        Phone3 phone = new Phone3();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone.call();},"A").start();
    }
}
// 资源类
class Phone3{
    public static synchronized void sendSms(){
        try {
            // A线程睡4秒
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

image-20240110144338767

image-20240110142852552

两个线程先打印 发短信 还是 打电话?结果:1/发短信 2/打电话

原因:

两个方法使用的还是同一个锁,因为方法用static修饰,类一加载静态方法就存在了。所以锁的是Phone的class对象,全局只有一个。所以A线程先拿到锁,即使睡眠也不会释放锁,所以A线程执行完毕,B线程在执行。

问题6

在问题5的测试代码的基础上,新创建一个资源类对象。A线程执行phone1的发短信静态方法,B线程执行phone2的打电话静态方法。

/*
* 两个线程 两个对象 两个静态方法
*/
public class Test3 {
    public static void main(String[] args) {
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone1.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone2.call();},"A").start();
    }
}

image-20240110144838080

两个线程先打印 发短信 还是 打电话?结果:1/发短信 2/打电话

原因:

和问题5的原因一样,因为静态方法和对象没有关系,用的还是同一个锁,锁的是Phone的class对象

问题7

在问题5的代码基础上,将资源类的发短信方法改为静态同步方法,打电话方法改为普通方法

Test4

/*
* 两个线程 一个对象 一个静态同步方法,一个普通方法
*/
public class Test4 {
    public static void main(String[] args) {
        Phone4 phone = new Phone4();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone.call();},"A").start();
    }
}
// 资源类
class Phone4{
    public static synchronized void sendSms(){
        try {
            // A线程睡4秒
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public void call(){
        System.out.println("打电话");
    }
}

image-20240110150014353

image-20240110150052074

两个线程先打印 发短信 还是 打电话?结果:1/打电话 2/发短信

原因:

发短信的方法因为是同步方法,所以要使用锁。又因为是静态方法,所以使用的锁锁的对象是Phone的class对象。打电话方法因为是普通方法,所以用不到锁,在A线程执行发短信方法的睡眠的过程中,B线程因为不用获得锁就能执行打电话方法,所以打电话方法先执行完。

问题8

在问题7的代码基础上,新加一个对象phone,A线程执行phone1的发短信方法,B线程执行phone2的打电话方法

/*
* 两个线程 两个对象 一个静态同步方法 一个普通方法
*/
public class Test4 {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        // 创建两个线程 A线程:发短信;B线程:打电话
        // A线程
        new Thread(()->{phone1.sendSms();},"A").start();
        // main线程睡一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // B线程
        new Thread(()->{phone2.call();},"A").start();
    }
}

两个线程先打印 发短信 还是 打电话?结果:1/打电话 2/发短信

原因:

和问题7的原因一样,B线程不用去获得锁就能够执行打电话的方法

总结

非静态方法如果使用锁,那这个锁锁的对象就是new出来的对象

静态方法如果使用锁,那这个锁锁的对象就是这个类的class对象,全局唯一。

集合类不安全

List不安全

案例

测试代码

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 循环10次,每次循环创建一个新线程,每个线程向list中添加一个数据
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

执行结果出现异常:ConcurrentModificationException 并发修改异常,多个线程同时操作一个对象导致

image-20240110192747101

解决方案

1.使用Vector集合,这个集合是安全的

List<String> list = new Vector<>();

2.使用集合工具类将不安全的list集合转换成安全的

List<String> list = Collections.synchronizedList(new ArrayList<>());

3.使用juc的解决方案CopyOnWriteArrayList,获得安全的ArrayList集合

List<String> list = new CopyOnWriteArrayList<>();

image-20240110194018425

CopyOnWriteArrayList

写入时复制! COW 计算机程序设计领域的一种优化策略 ,ArrayList由于多个线程同时并发操作,对同一份内存资源会进行覆盖,最终导致数据不安全。

CopyOnWriteArrayList核心思想:在进行读取操作的时候,多个线程会共同获取相同的指针指向相同的资源。但是在修改操作(add、remove等)时,不会在原有的内存中进行修改操作,而是将原有内存拷贝一份,在新的内存中进行修改操作。修改操作完毕后,将原来内存的指针执行新的内存地址,原来的内存就可以被回收掉。所以可以知道,读操作和写操作并不是在同一个内存中进行的。现在就有一个问题,如果多个线程同时进行写操作,是否会创建多个副本,如果每个线程都在自己新开辟的内存空间中进行写操作,那最后指针指向的是哪个内存空间呢??? 带着这个问题,我们来看看CopyOnWriteArrayList的源码

add方法

image-20240110203248353

remove方法

image-20240110203328668

发现,写操作都进行了加锁的同步控制,所以就保证了每次只有一个线程能够进行写的操作,所以就不能同时又多个副本出现。第一个线程先获取锁,进行写操作之后,指针指向这个修改进行写操作的内存地址。然后其他线程进行写操作的时候,将这个新的内存再次拷贝,进行写操作,以此类推...

CopyOnWriteArrayListVector厉害在哪里?

Vector底层是使用synchronized 关键字来实现的:效率特别低下。

image-20240110204441452

CopyOnWriteArrayList 使用的是Lock锁,效率会更加高效!

image-20240110204511203

Set不安全

案例

public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        // 循环10次,每次循环创建一个新线程,每个线程向set中添加一个数据
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

执行结果:ConcurrentModificationException异常

image-20240110205335801

解决方案

依据List同理可证,解决方案

1.集合工具类成为安全的set集合

Set<String> set = Collections.synchronizedSet(new HashSet<>());

2.使用juc的解决方案CopyOnWriteArraySet

Set<String> set = new CopyOnWriteArraySet();

CopyOnWriteArraySet

原理和CopyOnWriteArrayList是一样的

下面看下源码即可

add

image-20240110210229713

image-20240110210249990

remove

image-20240110210338680

image-20240110210400420

Map不安全

先回顾下HashMap的知识点

// 默认等价什么? 答:加载因子为0.75,初始化容量为16
Map<String,String> map = new HashMap<>();
// 等价为
Map<String,String> map1 = new HashMap<>(16,0.75f);

案例

public class MapTest {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), 
                        UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

执行结果:

image-20240110212024262

解决方案

1.集合工具类成为安全的Map集合

Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

2.使用juc的解决方案ConcurrentHashMap

Map<String, String> map = new ConcurrentHashMap<>();

image-20240110212312400

ConcurrentHashMap

有时间再了解其原理

Callable(简单)

介绍

image-20240110214126027

image-20240110214149533

callable和runnable的区别

  • callable可以有返回值
  • callable可以抛出异常
  • callable方法不同,run()/call()

源码分析

先看Callable接口,只有一个call方法

image-20240110220324788

我们知道Thread类的构造方法只能接收Runnable类型的参数

image-20240110220559659

那么如果是Callable类型,如果去放到线程中去执行呢???

从类的关系中,发现FutureTask是RunnableFuture接口的一个实现类

image-20240110220727911

RunnableFuture接口是Runnable接口的一个子接口,所以FutureTask类和Runnable接口挂上了关系

image-20240110220843946

看FutureTask类的构造方法,发现他接收的是Callable类型的参数,到这里FutureTask和Callable挂上了关系

image-20240110221000290

类的关系结构图

image-20240110222110142

代码实现

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Callable实现类对象
        Callable myThread = new MyThread();
        // 创建Runnable接口的FutureTask实现类对象
        // 将myThread放到了FutureTask中
        FutureTask stringFutureTask = new FutureTask<>(myThread);
        new Thread(stringFutureTask).start();
        // 获取call方法的返回值
        Object o = stringFutureTask.get();
        System.out.println(o);
    }
}

class MyThread implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "123456";
    }
}

执行成功,完美获得返回值

image-20240110223607909

为什么能够执行call()方法,call()方法的返回值是怎么通过get()方法获得的,带着这两个问题深入源码

查看FutureTask的构造方法,知道将传入的callable类型参数给了FutureTask对象的callable属性

image-20240110224425027

我们知道new Thread(stringFutureTask).start()通过静态代理模式调用Runnable实现类的run方法

image-20240110223959153

这里的Runnable实现类是FutureTask,查看FutureTask的run方法

image-20240110224618625

最终将callable属性给了c变量,最终c执行了call()方法,将返回值给了result变量。
并调用了set(result)方法,查看set(result)方法

image-20240110224802467

将call()方法的返回值result参数给了FutureTask对象的outcome属性,查看FutureTask的get()方法,发现调用了
report(s),

image-20240110225135145

继续深入,发现call()方法的返回值outcome给了变量x,并进行了返回,所以我们能够通过get()方法获得返回值

image-20240110225245321

注意点

FutureTask的get()方法可能会产生阻塞

因为只有Callable的call方法执行完才会有返回值,如果call方法是一个比较耗时的工作,get()方法会等待call方法执行完毕,所以可能产生阻塞。

一般就是将get方法放到最后或者使用异步通信

call方法的运结果会被缓存,提高效率,这点没有深究,知道即可

常用的辅助类

image-20240111125245008

CountDownLatch

减法计数器

image-20240111131439702

// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 指定线程的数量是6
        CountDownLatch countDownLatch = new CountDownLatch(6);
        // 每循环一次创建一个线程
        for (int i = 1; i < 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"线程 go out");
                // 每个线程的任务执行完毕,countDownLatch里面线程的数量-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        // 主线程等待6个线程都执行完毕,计数器为0时,再执行主线程的任务
        countDownLatch.await();

        // 6个线程执行完,执行主线程
        System.out.println("Close Door");
    }
}

执行结果:6个线程执行完毕再执行主线程

image-20240111131353008

原理:

countDownLatch.countDown():数量-1

countDownLatch.await():等待计数器归0之后,继续向下执行

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

CyclickBarrier

加法计数器

image-20240111132243106

简单理解:极其7颗龙珠才能召唤神龙

public class CyclickBarrierDemo {
    public static void main(String[] args) {
        /**
         * 收集7颗龙珠才能召唤神龙
         */
        // 7表示线程的数量达到7;
        // ()->{System.out.println("召唤神龙成功");}
        // 是Runnable接口的实现类,线程数量达到7后,新开辟一个线程执行召唤神龙的任务
        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里面的计数器到达7后,再唤醒
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

执行结果

image-20240111134720800

如果将CyclicBarrier的计数器改成8,表示需要线程的数量到达8才能召唤神龙,但是循环的次数是7,永远也到不了8,线程永远都是阻塞的状态,永远没有办法召唤神龙

image-20240111135009408

执行结果

image-20240111135100266

Semaphore

image-20240111135413324

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 3代表线程数量,可以想象成停车位有3个
        Semaphore semaphore = new Semaphore(3);

        // 每次循环创建一个线程,可想象为创建一辆车
        // 一共有6辆车
        for (int i = 1; i < 7 ; i++) {
            new Thread(()->{
                try {
                    // 当前这辆车获得停车位
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"车获得了停车位");
                    // 在停车位上呆上2s
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    
                    // 呆上2s后释放停车位
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

执行结果

image-20240111140808141

原理:

semaphore.acquire():如果停车位已经满了就等待,直到停车位被释放为止

semaphore.release():会将当前的信号量释放,然后唤醒等待的线程

作用:多个共享资源互斥的使用,并发限流,控制最大的线程数

读写锁ReadWriteLock

实现类:ReetrantReadWritelock

读可以被多个线程同时读,写的时候只能有一个线程去写。

image-20240111142742525

案例

不加锁控制

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCatch myCatch = new MyCatch();
        // 创建5个线程进行写入操作
        for (int i = 1; i < 6; i++) {
            final int temp=i;
            new Thread(()->{
                myCatch.set(temp,temp);
            },String.valueOf(i)).start();
        }

        // 创建5个线程进行读取操作
        for (int i = 1; i < 6; i++) {
            final int temp=i;
            new Thread(()->{
                myCatch.get(temp);
            },String.valueOf(i)).start();
        }
    }
}

/**
 * 自定义缓存,资源类
 */
class MyCatch{
    // 存储数据的容器
    private Map<Integer,Object> map=new HashMap<>();

    // 存数据,写操作
    public void set(Integer key,Object value){
        System.out.println(Thread.currentThread().getName()+"写入"+key);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+"写入成功");
    }

    // 得数据,读操作
    public void get(Integer key){
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取成功");
    }
}

执行结果

image-20240111151231147

结论:不加锁的情况下,多线程的写会造成数据不可靠的问题。

加锁控制

我们可以采用之前学习过的synchronized这种重量级锁和轻量级锁lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCatchLock myCatch = new MyCatchLock();
        // 创建5个线程进行写入操作
        for (int i = 1; i < 6; i++) {
            final int temp=i;
            new Thread(()->{
                myCatch.set(temp,temp);
            },String.valueOf(i)).start();
        }

        // 创建5个线程进行读取操作
        for (int i = 1; i < 6; i++) {
            final int temp=i;
            new Thread(()->{
                myCatch.get(temp);
            },String.valueOf(i)).start();
        }
    }
}

class MyCatchLock{
    // 存储数据的容器
    private Map<Integer,Object> map=new HashMap<>();

    // 创建读写锁
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 存数据,写操作
    public void set(Integer key,Object value){
        try {
            // 使用读写锁的写锁,只能有一个线程进行写操作
            // 加锁
            readWriteLock.writeLock().lock();
            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(Integer key){
        try {
            // 使用读写锁的读锁,可以有多个线程进行读操作
            // 加锁
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放读锁
            readWriteLock.readLock().unlock();
        }
    }
}

执行结果:

image-20240111203206008

独占锁和共享锁

理解两个新的概念,虽然概念是新的,但是一直在使用

独占锁:就是写锁,一次只能被一个线程占有

共享锁:就是读锁,多个线程可以同时占有

阻塞队列

image-20240111204853984

BlockingQueue

简介

阻塞队列,是Collection的一个子类

image-20240111205337221

类的结构示意图

image-20200812093254651

什么情况下我们会用到阻塞队列

多线程并发处理、线程池

四组API

方式 抛出异常 不会抛出异常,有返回值 阻塞,等待 超时等待
添加 add() offer() put() offer(timenum,timeUnit)
移出 remove() poll() take() poll(timenum,timeUnit)
检测队首元素 element() peek()

抛出异常

public class Test {
    public static void main(String[] args) {
        test1();
    }
    /**
     * 抛出异常
     */
    public static void test1(){
        // 设置队列初始化容量的大小是3
        BlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
        // 向队列中添加3个数据,输出返回值
        System.out.println(blockingQueue.add("a")); // true
        System.out.println(blockingQueue.add("b")); // true
        System.out.println(blockingQueue.add("c")); // true

        // 因为队列的容量大小是3,现在向队列中添加第四个数据
        // java.lang.IllegalStateException: Queue full
        // 队列满了,第4个数据添加不进去,出现异常
        System.out.println(blockingQueue.add("d"));

        // 一次获取队列中的数据,输出返回值
        System.out.println(blockingQueue.remove()); // a
        System.out.println(blockingQueue.remove()); // b
        System.out.println(blockingQueue.remove()); // c

        // 现在队列中已经没有数据了,在获取数据
        // java.util.NoSuchElementException
        // 出现这个异常,表示没有元素异常
        System.out.println(blockingQueue.remove());
    }
}

不会抛出异常,有返回值

/**
 * 不抛出异常,有返回值
 */
public static void test2(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    System.out.println(blockingQueue.offer("a"));
    System.out.println(blockingQueue.offer("b"));
    System.out.println(blockingQueue.offer("c"));
    //添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常
    System.out.println(blockingQueue.offer("d"));

    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    //弹出 如果没有元素 只会返回null 不会抛出异常
    System.out.println(blockingQueue.poll());
}

阻塞,等待(一直阻塞)

/**
 * 等待 一直阻塞
 */
public static void test3() throws InterruptedException {
    ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

    //
    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    // 如果队列已经满了, 再进去一个元素  这种情况会一直等待这个队列 什么时候有了位置再进去,程序不会停止
    // blockingQueue.put("d");

    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    System.out.println(blockingQueue.take());
    //如果我们再来一个  这种情况也会等待,程序会一直运行 阻塞
    System.out.println(blockingQueue.take());
}

超时等待(超过指定的时间就不在阻塞了)

/**
 * 等待 超时阻塞
 * 这种情况也会等待队列有位置 或者有产品 但是会超时结束
 */
public static void test4() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);

    blockingQueue.offer("a");
    blockingQueue.offer("b");
    blockingQueue.offer("c");

    //超时时间2s 等待如果超过2s就结束等待
    blockingQueue.offer("d", 2, TimeUnit.SECONDS);

    blockingQueue.poll();
    blockingQueue.poll();
    blockingQueue.poll();
    //超过两秒 我们就就结束等待了
    blockingQueue.poll(2, TimeUnit.SECONDS);
}

SynchronousQueue

image-20240112111418789

同步队列

特点:

  • 同步队列没有容量,也可以视为容量为1的队列
  • 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法

  • SynchronousQueue和 其他的BlockingQueue 不一样 它不存储元素;
  • put了一个元素,就必须从里面先take出来,否则不能再put进去值!
  • 并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

代码实现

public static void main(String[] args) {
    BlockingQueue<Integer> blockingQueue = new SynchronousQueue<>();

    // T1线程向同步队列中放数据 blockingQueue.put();
    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();
    // T2线程从同步队列中取数据 blockingQueue.take()
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(3);
            System.out.println(Thread.currentThread()
                               .getName()+" get "+blockingQueue.take());
            System.out.println(Thread.currentThread()
                               .getName()+" get "+blockingQueue.take());
            System.out.println(Thread.currentThread()
                               .getName()+" get "+blockingQueue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"T2").start();
}

执行结果:
    T1 put 1
	T2 get 1
    T1 put 2
	T2 get 2 
    T1 put 3
	T2 get 3    

线程池(重点)

线程池:三大创建方式、七大参数、四种拒绝策略

池化技术

程序的运行,本质:占用系统的资源! 我们需要去优化资源的使用 ====> 池化技术

例如:线程池、JDBC的连接池、内存池、对象池等等…,资源的创建、销毁十分消耗资源

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。

线程池的好处

降低资源的消耗;提高响应的速度;方便管理;线程复用、可以控制最大并发、管理线程;

三大方法

阿里巴巴规范:创建线程时,不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

image-20200812114142750

//单个线程

ExecutorService threadPool = Executors.newSingleThreadExecutor();

public class Demo01 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        try {
            for (int i = 1; i < 11; i++) {
                int finalI = i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread()
                                       .getName()+"->ok->"+ finalI);
                });
                TimeUnit.SECONDS.sleep(1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdownNow();
        }
    }
}

image-20240112122350976

//创建一个固定的线程池的大小,这里创建的线程数是5个

ExecutorService threadPool2 = Executors.newFixedThreadPool(5);

public class Demo01 {
    public static void main(String[] args) {
        // 创建5个线程去执行任务
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 1; i < 11; i++) {
                int finalI = i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread()
                                       .getName()+"->ok->"+ finalI);
                });
            }
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdownNow();
        }
    }
}

image-20240112123103484

//可伸缩的,遇强则强,遇弱则弱

ExecutorService threadPool3 = Executors.newCachedThreadPool();

public class Demo01 {
    public static void main(String[] args) {
        // 可伸缩线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 1; i < 11; i++) {
                int finalI = i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread()
                                       .getName()+"->ok->"+ finalI);
                });
            }
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdownNow();
        }
    }
}

image-20240112123413655

现在我们循环100次,就有45个线程,遇强则强

image-20240112123734064

七大参数

源码分析

// 单个线程
Executors.newSingleThreadExecutor();
// 源码
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
// 固定线程
Executors.newFixedThreadPool(5);
// 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
// 可伸缩线程
Executors.newCachedThreadPool();
// 源码
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

创建线程池的三大方法底层都是通过了ThreadPoolExecutor来创建的

ThreadPoolExecutor有七大参数,我们来看一下。

image-20240112124632099

通过this()调用了另外一个构造方法,查看另外一个构造方法

image-20240112124754392

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

通过银行办理业务的情景辅助理解线程池7大参数

image-20240112130929306

手动实现

public class Demo01 {
    public static void main(String[] args) {
        // 使用ThreadPoolExecutor手动创建一个线程池,自定义7大参数
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心线程数
                5, // 最大线程数
                3, // 线程等待时间,超过这个时间就会释放
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingDeque<>(3), // 阻塞队列
                Executors.defaultThreadFactory(), // 线程创建工厂
                // 默认的拒绝策略,银行满了(已经到达最大线程数,阻塞队列也满了),还有人进来
                // 不办理这个人的业务,抛出异常
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 1; i <=2; i++) {
                int finalI = i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread()
                            .getName()+"->ok->"+ finalI);
                });
            }

            TimeUnit.SECONDS.sleep(5);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdownNow();
        }
    }
}

循环2次,表示有两个任务需要线程池处理。达到核心线程数

image-20240112134730203

循环5次,有5个任务,其中2个任务交给核心线程处理,剩下的三个任务放在阻塞队列等待

image-20240112134931780

循环6次,6个任务,2个处理,3个等待,最后一个任务因为没有位置,在开启一个非核心线程处理最后一个任务

image-20240112135134365

循环8次,8个任务,2个核心线程处理,3个等待,最后三个任务,开启最大线程数,非核心线程3个处理任务

image-20240112135317794

循环9次,9个任务,因为已经达到最大线程数,阻塞队列也满了,所以第九个任务不被处理,拒绝策略

image-20240112135528966

四种拒绝策略

image-20240112140012498

超出最大承载:阻塞队列容量大小+maxPoolSize。

AbortPolicy:该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常

CallerRunsPolicy:哪来的去哪里 main线程进行处理。

image-20240112140451996

执行结果

image-20240112140519088

DiscardPolicy(): 放弃这个任务,不会抛出异常。

DiscardOldestPolicy():尝试和最早的线程(第一个)竞争,竞争成功,第一个线程执行,失败不会抛出异常。

指定最大线程数规则

手动创建线程池时,如何指定线程池的最大线程数呢

CPU密集型

电脑的核数是几核就选择几;指定maximunPoolSiz 的大小

// 获取cpu 的核数
int max = Runtime.getRuntime().availableProcessors();
ExecutorService service =new ThreadPoolExecutor(
    2,
    max,
    3,
    TimeUnit.SECONDS,
    new LinkedBlockingDeque<>(3),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()
);

I/O密集型

I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约就是最大I/O线程的一倍到两倍之间。因为I/O十分消耗资源

四大函数式接口

新时代的程序员: lambda表达式、链式编程、函数式接口、Stream流式计算

函数式接口:只有一个抽象方法的接口

@FunctionalInterface
public interface Consumer<T> {
	
	// 抽象方法
    void accept(T t);
	
	// 默认方法
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

// java中,函数式接口使用的非常多

image-20200812143713392

Function<T, R>

Function函数型接口,有一个输入参数,有一个输出参数,只要是函数型接口可以用lambda表达式简化。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
public class Demo01 {
    public static void main(String[] args) {
        // 匿名内部类
        Function<String, String> function = new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        };
        // 指定了泛型类型,直接转换为指定的类型
        String asd = function.apply("asd");
        System.out.println(asd);

        // lambda表达式将匿名内部类简化
        Function function1 = (str)->{return str;};
        // 没有指定泛型类型,泛型类型默认就是Object类型
        Object asd1 = function1.apply("asd");
        System.out.println(asd1);
        
        // lambda表达式再次简化
		Function function2 = str->{return str;};
    }
}

Predicate

断定型接口,只有一个参数,返回值时布尔值

@FunctionalInterface
public interface Predicate<T> {
    // 抽象方法
    boolean test(T t);
}
// lambda表达式简化
Predicate predicate = (str)->{return str==null?true:false;};
System.out.println(predicate.test(null));

Consumer

消费型接口,只有参数,没有返回值

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
// lambda表达式简化
Consumer consumer = (str)->{System.out.println(str);};
consumer.accept("asd");

Supplier

供给型接口,没有参数,只有返回值

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
// lambda表达式简化
Supplier suppier = ()->{return "asd";};
System.out.println(suppier.get());

Stream流式计算

大数据:存储 + 计算

集合、Mysql本质都是存储东西的;

计算都应该交给流来操作!

image-20240112154719749

计算都应该交给流来操作!

/**
 * Description:
 * 题目要求: 用一行代码实现
 * 1. Id 必须是偶数
 * 2.年龄必须大于23
 * 3. 用户名转为大写
 * 4. 用户名倒序
 * 5. 只能输出一个用户
 **/
public class Demo01 {
    public static void main(String[] args) {
        User u1 = new User(1, "a", 23);
        User u2 = new User(2, "b", 23);
        User u3 = new User(3, "c", 23);
        User u4 = new User(6, "d", 24);
        User u5 = new User(4, "e", 25);
        // 集合就是存储
        List<User> userList = Arrays.asList(u1, u2, u3, u4, u5);
        // 计算交给Stream流
        userList.stream()
                .filter((user)->{return user.getId()%2==0;})
                .filter((user)->{return user.getAge()>23;})
                // 获取每一个user的name,并将name变为大写
                .map((user)->{return user.getName().toUpperCase();})
                // 将user的name倒叙排列,如果是正序userName1.compareTo(userName2)
                .sorted((userName1,userName2)->{return userName2.compareTo(userName1);})
                // 取第一个name
                .limit(1)
                .forEach((userName)->{System.out.println(userName);});
    }
}

@NoArgsConstructor
@AllArgsConstructor
@Data
class User{
    private Integer id;
    private String name;
    private Integer age;
}

执行结果:

image-20240112171901678

ForkJoin

ForkJoin 在JDK1.7中出现,并行执行任务!提高效率。在大数据量速率会更快

大数据中: MapReduce 核心思想–>把大任务拆分为小任务!

image-20200812163638389

工作窃取

ForkJoin特点:工作窃取

实现原理: 双端队列 从上面和下面都可以去拿到任务进行执行!

image-20200812163701588

使用ForkJoin

1、通过ForkJoinPool来执行

2、计算任务 execute(ForkJoinTask<?> task)

3、计算类要去继承 ForkJoinTask

ForkJoin的计算案例

ForkJoin的计算类

public class ForkJoinDemo extends RecursiveTask<Long> {
    // 起始值
    private long start;
    // 结束值
    private long end;
    // 临界值
    private long temp;

    public ForkJoinDemo() {
    }

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

    @Override
    protected Long compute() {
        long sum=0;
        if((end-start)<temp){
            for (long i = 0; i <end ; i++) {
                sum=sum+i;
            }
            return sum;
        } else{
            // 1、计算平均值
            long middle=(start+end)/2;
            // 使用ForkJoin 分而治之  计算
            // 拆分任务,把线程压入线程队列
            ForkJoinDemo task1 = new ForkJoinDemo(start, middle,temp);
            ForkJoinDemo task2 = new ForkJoinDemo(middle, end,temp);
            task1.fork();
            task2.fork();
            return task1.join()+task2.join();
        }
    }
}

测试类

public class ForkJoinTest {

    private static long count=10_0000_0000;

    public static void main(String[] args) {
        // 耗费时间+结果:513->499999999500000000
        // test1();
        // 耗费时间+结果:95920->4896050999239035648
        // test2();
        // 571->499999999500000000
        test3();
    }

    public static void test1(){
        long start=System.currentTimeMillis();
        long sum=0L;
        for (long i = 0L; i < count; i++) {
            sum+=i;
        }
        long end=System.currentTimeMillis();
        System.out.println("耗费时间+结果:"+(end-start)+"->"+sum);
    }

    public static void test2(){
        long start=System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0, count, 1000000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinDemo);
        long sum=0;
        try {
            sum = submit.get();
            long end=System.currentTimeMillis();
            System.out.println("耗费时间+结果:"+(end-start)+"->"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static void test3(){
        long start=System.currentTimeMillis();
        long sum = LongStream.range(0, count).parallel().reduce(0, Long::sum);
        long end=System.currentTimeMillis();
        System.out.println("耗费时间+结果:"+(end-start)+"->"+sum);

    }
}

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

image-20200812164023833

异步回调

Future 设计的初衷:对将来的某个事件结果进行建模!

image-20240119144319774

我们平时都使用CompletableFuture

没有返回值的runAsync异步回调

public class Demo01 {
	public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        Future<Void> future = CompletableFuture.runAsync(()->{
            // 发起一个异步任务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()
                               .getName()+"============================");
        });
        System.out.println(System.currentTimeMillis());
        // 获取异步任务的执行结果
        // get方法会阻塞,当异步任务执行完毕才能获得返回值
        System.out.println(future.get()); // 异步任务没有返回值,所以返回的结果是null值
    }
}

执行结果

image-20240119145225070

有返回值的supplyAsync异步回调

程序正常的返回结果

public class Demo02 {
    public static void main(String[] args) 
        throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1024;
        });
        System.out.println(future.whenComplete((t, u) -> {
            // success 回调
            System.out.println("t=>" + t); //正常的返回结果
            System.out.println("u=>" + u); //抛出异常的 错误信息
        }).exceptionally((e) -> {
            // error回调
            System.out.println(e.getMessage());
            return 143;
        }).get());
    }
}

image-20240119150445391

程序异常的返回结果

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
        System.out.println(Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(2);
            // 程序异常
            int i=1/0;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 1024;
    });
    System.out.println(future.whenComplete((t, u) -> {
        // success 回调
        System.out.println("t=>" + t); //正常的返回结果
        System.out.println("u=>" + u); //抛出异常的 错误信息
    }).exceptionally((e) -> {
        // error回调
        System.out.println(e.getMessage());
        return 143;
    }).get());
}

image-20240119150601543

whenComplete: 有两个参数,一个是t 一个是u

  • T:是代表的 正常返回的结果
  • U:是代表的 抛出异常的错误信息
  • 如果发生了异常,get可以获取到exceptionally返回的值;

JMM

什么是JMM

JAVA内存模型,不存在的东西,是一个概念也是一个约定!

关于JMM的同步的约定

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

线程中分为 工作内存、主内存

8种操作

  • Read(读取):
    • 作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • Load(载入):
    • 作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
  • Use(使用):
    • 作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
  • Assign(赋值):
    • 作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
  • Store(存储):
    • 作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
  • Write(写入):
    • 作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
  • Lock(锁定):
    • 作用于主内存的变量,把一个变量标识为线程独占状态;
  • Unlock(解锁):
    • 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

image-20200812215247240

JMM对这8种操作给了相应的规定:

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

遇到问题

这时会出现一个问题,如线程A和线程B同时使用了主存的一个数据,线程B修改了值,但是线程A不能及时可见。

image-20200812215606080

private static int num=0;
public static void main(String[] args) throws InterruptedException {
    // 线程 对主内存的变化不知道,直接使用工作内存num=0的值,所以可能会进入死循环
    new Thread(()->{
        while(num ==0){
            
        }
    }).start();
    TimeUnit.SECONDS.sleep(2);
    num=1;
    System.out.println("改变num的值"+num);
}

执行结果:程序进入死循环,一直在运行

image-20240119191711631

遇到问题:程序不知道主存中的值已经被修改过了! 下面解答

解决

img

对Volatile的理解

Volatile 是Java虚拟机提供 轻量级的同步机制

提到Volatile我们就会想到它的三个特点!

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

volatile

保证可见性

image-20240119191811681

// volatile,保证可见性
private volatile static int num=0;
public static void main(String[] args) throws InterruptedException {
    // 线程 对主内存的变化不知道,直接使用工作内存num=0的值,所以可能会进入死循环
    new Thread(()->{
        while(num ==0){

        }
    }).start();
    TimeUnit.SECONDS.sleep(1);
    num=1;
    System.out.println("改变num的值"+num);
}

执行结果

image-20240119191842073

如果修饰一个对象,这个对象里面的属性也是可见的。

不保证原子性

原子性:不可分割;

线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败。

public class Demo02 {
    private volatile static int num = 0;

    public static void main(String[] args) {
        // 理论上num的结果应该是20000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
		//当线程数小于2的时候就停止,因为有两个默认线程:mian、GC
        while(Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(num);

    }

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

执行结果:因为add中的num++不是原子性的操作,所以最后累加的结果不到2万

image-20240119193240325

如果add方法不加lock或synchronized ,怎么样保证原子性?

image-20200812215844788

使用原子类保证原子性

image-20200812215909271

修改代码

image-20240119194048715

public class Demo02 {
    // num成为了AtomicInteger原子类的Integer
    private volatile static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) {
        // 理论上num的结果应该是20000
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        //当线程数小于2的时候就停止,因为有两个默认线程:mian、GC
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(num);
    }
    public static void add() {
        // AtomicInteger原子类的+1方法,底层使用的是CAS,效率极高
        num.getAndIncrement();
    }
}

执行结果:

image-20240119194128993

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

image-20240119194425226

禁止指令重排

什么是指令重排

我们写程序时,计算机并不是按照我们自己写的那样去执行的。

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

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

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

//我们期望的执行顺序是 1_2_3_4  可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的
1234567

可能造成的影响结果:前提:a b x y这四个值 默认都是0

线程A 线程B
x=a y=b
b=1 a=2

正常的结果: x = 0; y =0;

但是经过指令重排之后,会编程这样

线程A 线程B
b=1 a=2
x=a y=b

可能在线程A中会出现,先执行b=1,然后再执行x=a;

在B线程中可能会出现,先执行a=2,然后执行y=b;

那么就有可能结果如下:x=2; y=1.

Volatile可以避免指令重排

Volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令,作用

  • 保证特定的操作的执行顺序;
  • 可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

Volatile会在上面和下面该加上一层内存屏障

image-20200812220019582

总结

  • volatile可以保证可见性;
  • 不能保证原子性
  • 由于内存屏障,可以保证避免指令重排的现象发生

面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

彻底玩转单例模式

饿汉式 、懒汉式(DCL懒汉式)

饿汉式

// 饿汉式
public class Hungry {

    /**
     * 可能会浪费空间
     */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];

    private final static Hungry hungry=new Hungry();
    // 构造方法私有化
    private Hungry() {
    }
    // 提供对外接口
    public Hungry getInstance(){
        return hungry;
    }
}

DCL懒汉式

public class LazyMan {
    // 构造方法私有化
    private LazyMan() {
        System.out.println(Thread.currentThread().getName());
    }
    // 懒汉式,先不提供对象,等需要使用的时候在提供
    private static LazyMan lazyMan;
    // 提供对外接口
    public static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
}

上述代码如果在单线程的情况下没有问题,但是在多线程的情况下会出现问题

public class LazyMan {
    // 构造方法私有化
    private LazyMan() {
        System.out.println(Thread.currentThread().getName());
    }
    // 懒汉式,先不提供对象,等需要使用的时候在提供
    private static LazyMan lazyMan;
    // 提供对外接口
    public static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }

    // 多线程并发测试
    public static void main(String[] args) {
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                getInstance();
            }).start();
        }
    }
}

执行结果

image-20240119201807783

优化

要解决上述问题,加锁控制(双重检测锁模式)

image-20240119202122282

public class LazyMan {
    // 构造方法私有化
    private LazyMan() {
        System.out.println(Thread.currentThread().getName());
    }
    // 懒汉式,先不提供对象,等需要使用的时候在提供
    private static LazyMan lazyMan;
    // 提供对外接口
    public static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例(DCL懒汉式)
        if(lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan=new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    // 多线程并发测试
    public static void main(String[] args) {
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                getInstance();
            }).start();
        }
    }
}

执行结果

image-20240119202214640

但是想一下这个代码

if(lazyMan==null){
    synchronized (LazyMan.class){
        if (lazyMan==null){
            lazyMan=new LazyMan();
        }
    }
}

lazyMan=new LazyMan()其实执行的是3步操作

  1. 在堆内存中分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象lazyman指向这个内存空间

我们希望的步骤是1->2->3,但是会产生指令重排,可能真正的执行步骤是1->3->2,如果是1->3->2,就会产生问题。想象一下,比如线程A拿到LazyMan.class对象锁,按照1->3->2执行完3之后,将要执行2之前,线程B进入getInstance方法,此时因为对象lazyMan有了指向,所以不为null,不用获得对象锁就能执行下面的代码,而此时拿到的lazyMan对象指向的堆内存只是开辟了内存空间,并没有进行对象的初始化操作,所以我们要使用volatile禁止指令重排

再优化

使用volatile禁止指令重排

image-20240119203931699

public class LazyMan {
    // 构造方法私有化
    private LazyMan() {
        System.out.println(Thread.currentThread().getName());
    }
    // 懒汉式,先不提供对象,等需要使用的时候在提供
    // volatile 禁止指令重排
    private static volatile LazyMan lazyMan;
    // 提供对外接口
    public static LazyMan getInstance(){
        // 双重检测锁模式的懒汉式单例(DCL懒汉式)
        if(lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan=new LazyMan();
                }
            }
        }
        return lazyMan;
    }

    // 多线程并发测试
    public static void main(String[] args) {
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                getInstance();
            }).start();
        }
    }
}

即使是这样,如果通过反射,还是能破坏掉我们的单例模式

// 多线程并发测试
public static void main(String[] args) 
    throws NoSuchMethodException, InvocationTargetException, 
	InstantiationException, IllegalAccessException {
    // 通过对外接口获取对象
    LazyMan instance = LazyMan.getInstance();
    Class<LazyMan> lazyManClass = LazyMan.class;
    // 获取私有的无参构造
    Constructor<LazyMan> declaredConstructor = lazyManClass
            .getDeclaredConstructor(null);
    // 无视私有的构造器
    declaredConstructor.setAccessible(true);
    // 使用反射调用无参构造方法创建对象
    LazyMan lazyMan = declaredConstructor.newInstance();
    System.out.println(instance);
    System.out.println(lazyMan);
}

执行结果,成功破坏单例模式,还是不安全

image-20240119205703254

如何才能安全呢,我们看反射中的newInstance()方法

image-20240119205820776

结论:懒汉式无论怎么检测都是不安全的,使用枚举才是安全的

静态内部类

package com.guocl.Singleton;

/**
 * 静态内部类
 */
public class Holder {

    private Holder(){

    }

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

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

枚举

public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

使用反射测试枚举是否是安全的

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    EnumSingle instance1 = EnumSingle.INSTANCE;
    Constructor<EnumSingle> declaredConstructor =	    					   		  		EnumSingle.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    EnumSingle instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

执行结果,出现java.lang.NoSuchMethodException: com.jjh.single.EnumSingle.()异常

image-20240119211552829

这个异常表示没有无参构造方法异常(因为我们只调用了无参构造),和我们深入源码看到的异常不一样

image-20240119211730634

但是这个枚举类中真的没有构造方法嘛,我们看一下class文件,发现有无参构造方法

image-20240119212134272

使用反编译命令再次验证是否存在构造方法:还是存在无参构造方法

image-20240119212412754

使用专业的jad工具,查看最终源码

public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/jjh/single/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
                INSTANCE
        });
    }
}

发现只有一个有参构造方法private EnumSingle(String s, int i)

好的,现在为止,我们使用反射的方式调用有参构造

image-20240119213453530

执行结果:达到了我们想要的异常

image-20240119213521462

深入理解CAS

什么是CAS

大厂必须深入研究底层!!!!修内功!操作系统、计算机网络、组成原理、数据结构

public class Demo01 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        // boolean compareAndSet(int expect, int update)
        // 期望值、更新值
        // 如果实际值 和 期望值相同,那么就会更新
        // 如果实际值 和 期望值不相同,那么就不会更新,CAS是cpu的并发原语
        atomicInteger.compareAndSet(2020,2021);
        // 实际值是2020,期望值是2020,相同所以修改成功
        System.out.println(atomicInteger.get()); // 2021

        // 实际值是2021,期望值是2020,不同,所以修改失败
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get()); // 2021

        // ++操作  现在实际值为2021
        atomicInteger.getAndIncrement();
    }
}

我们看atomicInteger.getAndIncrement();的源码

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

用到了unsafe,我们看unsafe是怎么来的

image-20240120173347900

Unsafe类

image-20200812220347822

java可以通过Unsafe 类来操作内存

getAndIncrement()

接着看getAndIncrement()加1操作的源码

image-20200812220411463

方法解释

//var1 是this指针
//var2 是地址偏移量
//var4 是自增的数值,是自增1还是自增N
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //获取内存值,这是内存值已经是旧的,假设我们称作期望值E
        var5 = this.getIntVolatile(var1, var2);
        //compareAndSwapInt方法是重点,
        //var5是期望值,var5 + var4是要更新的值
        //这个操作就是调用CAS的JNI
        //每个线程将自己内存里的内存值M与var5期望值E作比较(其实就是var1, var2和var5作比较)
        //如果相同将内存值M更新为var5 + var4,否则做自旋操作
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

总结

CAS:比较当前工作内存中的值(期望值) 和 主内存中的值(实际值),如果这个值是期望的,则执行操作!如果不是就一直循环,使用的就是自旋锁。

缺点:

  • 循环会耗时;
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

CAS:ABA问题?(狸猫换太子)

image-20200812220441615

线程1:期望值是1,要变成2;

线程2:两个操作:

  • 期望值是1,变成3
  • 期望是3,变成1

所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1

package com.guocl.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class AbaDemo {

    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //捣乱线程
        System.out.println("a1:" + atomicInteger.compareAndSet(2020, 2021));
        System.out.println("a2:" + atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        System.out.println("b1" + atomicInteger.compareAndSet(2020, 2022));
        System.out.println(atomicInteger.get());
    }
}

执行结果:

a1:true
a2:true
2020
b1true
// 捣乱的线程改变了主存的值,虽然还是2020,但是骗过了线程1
2022

Process finished with exit code 0

解决CAS的ABA问题需要 原子引用,也就是乐观锁的思想

原子引用

解决ABA问题,对应的思想:就是使用乐观锁!!!

带版本号的原子操作!

当我们使用原子操作泛型为Integer时,注意一个大坑:

image-20200812220608094

public class Demo02 {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);

        new Thread(()->{
            // 获得版本号
            int stamp = reference.getStamp();
            System.out.println("A1->"+stamp);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(reference.compareAndSet(1, 2,
                    reference.getStamp(), reference.getStamp() + 1));

            System.out.println("A2->"+reference.getStamp());

            System.out.println(reference.compareAndSet(2, 1,
                    reference.getStamp(), reference.getStamp() + 1));

            System.out.println("A3->"+reference.getStamp());

        }).start();

        new Thread(()->{
            // 获得版本号
            int stamp = reference.getStamp();
            System.out.println("B1->"+stamp);

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(reference.compareAndSet(1, 6,
                    stamp, reference.getStamp() + 1));

            System.out.println("B2->"+reference.getStamp());

        }).start();
    }
}

执行结果:

image-20240120183252079

深入理解AQS

AQS是抽象队列同步器,简单来说就是一个抽象类AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

继承关系

继承了AbstractOwnableSynchronizer抽象类

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
    
}

下面我们看看这个AbstractQueuedSynchronizer抽象类当中都有哪些主要的东西

Node(静态内部类)

主要的字段,AbstractQueuedSynchronizer就是通过Node实现的双向队列

static final class Node {
	// 排他锁的标识
	static final Node EXCLUSIVE = null;
    // 具有这个标识,说明下一个节点就会被唤醒
    static final int SIGNAL = -1;
    // node对象存储标识的地方
    volatile int waitStatus;
    // 上一个节点
    volatile Node prev;
    // 下一个节点
    volatile Node next;
}

主要字段

// 头节点,指向双向队列的第一个节点
private transient volatile Node head;
// 尾节点,指向双向队列的最后一个节点
private transient volatile Node tail;
// 标志符号,CAS通过操作它实现加锁和解锁
private volatile int state;

构造方法源码分析

无参构造

// 调用无参构造方法创建reentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();

// 查看无参构造方法
public ReentrantLock() {
    // 创建了一个NonfairSync对象,引用给了reentrantLock的sync属性
    sync = new NonfairSync();
}

// 查看NonfairSync是什么东西
// NonfairSync继承了Sync
static final class NonfairSync extends Sync   

// 查看sync是什么东西
// 发现Sync继承了咱们的AQS    
abstract static class Sync extends AbstractQueuedSynchronizer    

有参构造

// 调用了有参构造
ReentrantLock reentrantLock = new ReentrantLock(true);

// 发现如果是如果fair是true,就使用公平锁
// 如果是false,就使用非公平锁,而无参构造默认使用的就是非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

非公平锁源码分析

加锁

// 创建非公平锁
ReentrantLock reentrantLock = new ReentrantLock();

lock()

// 加锁
reentrantLock.lock();

// 进入加锁方法
public void lock() {
	// 非公平锁NonfairSync的lock()方法
    sync.lock();
}

// 查看
final void lock() {
    // state的值在主存中默认的初始值是0
    // 通过cas进行获取锁的操作,如果修改成功state的值为1,返回true 失败state的值不变还是0,返回false
    if (compareAndSetState(0, 1))
        // 如果获得了锁,将sync对象的exclusiveOwnerThread属性的值引用当前的线程对象
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果获取锁失败,调用acquire()方法
        acquire(1);
}

acquire()

public final void acquire(int arg) {
    // 尝试再一次获取锁,arg值为1
    if (!tryAcquire(arg) &&
        // 如果获取锁失败的话,就将新的node节点添加到双向队列中去
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg)

// 尝试再一次获取锁,acquires值为1
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// 查看nonfairTryAcquire方法
// acquires值为1
final boolean nonfairTryAcquire(int acquires) {
    // 得到当前的线程对象
    final Thread current = Thread.currentThread();
    // 从主存中获得state的值
    int c = getState();
    // 代表当前没有线程获得锁
    if (c == 0) {
        // 通过cas方式再次获得锁
        if (compareAndSetState(0, acquires)) {
            // 如果获得锁成功,将sync对象的exclusiveOwnerThread属性的值引用当前的线程对象
            setExclusiveOwnerThread(current);
            // 表示当前线程拿到了锁
            return true;
        }
    }
    // 如果c不等于0的话,有两种情况
    // 一个就是其他线程获得了锁
    // 另外一种情况就是当前线程获得了锁(锁了多次),此时做的就是可重入的判断
    // 如果sync对象的exclusiveOwnerThread值等于当前线程的话,就代表是当前线程拿到了锁
    else if (current == getExclusiveOwnerThread()) {
        // 当前线程拿到了锁,主存中state的值+1给变量nextc
        int nextc = c + acquires;
        // 判断是否<0是因为nextc的值是int类型,如果超过的int类型的最大数会返回负数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 将state+1操作之后的值刷新到主存
        setState(nextc);
        // 表示还是当前线程获取到了锁,并进行了重入锁的操作
        return true;
    }
    // 如果再次获得锁失败,返回false
    return false;
}

addWaiter

通过这个方法,让当前新建的节点一定能添加到双向队列的队尾

public final void acquire(int arg) {
    // 尝试再一次获取锁,arg值为1
    if (!tryAcquire(arg) &&
        // 如果获取锁失败的话,就将新的node节点添加到双向队列中去
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// mode的值是null(Node.EXCLUSIVE的值就是null)
private Node addWaiter(Node mode) {
    // 创建一个新的节点,并将当前线程对象放入到节点当中
    Node node = new Node(Thread.currentThread(), mode);
    // 将上一个节点,也就是当前节点没有创建之前的双向队列当中的最后一个节点给变量pred
    Node pred = tail;
    // 如果pred不是null,代表双端队列当中有节点
    // 如果是null值,代表双端队列中没有节点
    if (pred != null) {
        // 如果有节点,将当前节点的上一个节点指向之前队列的最后一个节点
        node.prev = pred;
        // 设置队列中的尾节点是当前节点
        if (compareAndSetTail(pred, node)) {
            // 将之前的最后一个节点的下一个节点指向当前节点,完成了双向的一个指向(双向链表)
            pred.next = node;
            // 返回当前节点
            return node;
        }
    }
    // 如果队列当中没有节点或者cas操作compareAndSetTail设置队列的尾节点失败的话,调用enq(node)
    enq(node);
    return node;
}

// node就是新建的节点(当前节点)
private Node enq(final Node node) {
    // 死循环
    for (;;) {
        // 队列尾节点给变量t
        Node t = tail;
        // t为null,代表队列中没有节点
        if (t == null) { 
            // 此时创建一个新的节点设置成为队列的头节点(说白了,就是创建了一个没有任何意义的节点)
            if (compareAndSetHead(new Node()))
                // 尾节点也指向头节点,说明此时这个新创建的节点既是头节点又是尾节点
                tail = head;
        // 如果队列中有节点的话
        } else {
            // 将当前节点的上一个节点指向之前队列的尾节点
            node.prev = t;
            // 如果设置尾节点失败,就再次循环直到将当前的节点设置成为队列中的尾节点成功为止,
            // 不然就一在在循环
            if (compareAndSetTail(t, node)) {
                // 之前队列中的尾节点的下一个节点指向当前节点,形成双向的指向(链表)
                t.next = node;
                // 返回之前队列中的尾节点(而非当前节点)
                return t;
            }
        }
    }
}

acquireQueued

public final void acquire(int arg) {
    // 尝试再一次获取锁,arg值为1
    if (!tryAcquire(arg) &&
        // 如果获取锁失败的话,就将新的node节点添加到双向队列中去	
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// node就是新的节点,arg的值是1
final boolean acquireQueued(final Node node, int arg) {
    // 标识符
    boolean failed = true;
    try {
        // 标识符
        boolean interrupted = false;
        // 死循环
        for (;;) {
            // 获取当前节点的上一个节点
            final Node p = node.predecessor();
            // 如果上一个节点是头节点并且再次获得锁成功的话,进入if结构体
            // 再次获得锁
            // 	1.state的在主存的值是0了,然后修改为1,证明当前线程拿到了锁
            // 	2.当前线程拿到的是可重入锁
            if (p == head && tryAcquire(arg)) {
                // 将队列中的头节点设置成为当前节点,因为拿到锁资源了,排队对我来说没有任何意义
                setHead(node);
                // 之前的头节点设置为null,让gc清除垃圾
                p.next = null; // help GC
                // 将标识符failed设置成为false
                failed = false;
                // 返回false
                return interrupted;
            }
            // p表示上一个节点,node表示当前节点
            // shouldParkAfterFailedAcquire(p, node)一定保证的是当前节点的上一个节点是-1,
            // 才会返回true,只有返回true,才会将线程,等待唤醒获取锁资源
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 基于unsafe的pack方法,将线程挂起
                // 针对failed属性,这里是唯一可能会出现抛异常的地方,代表jvm内部出现问题
                // 可以这么理解,finally代码块中的内容,执行几率大概率为0
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果线程挂起失败,调用这个方法
        if (failed)
            cancelAcquire(node);
    }
}
// pred表示队列当中的上一个几点,node表示当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取上一个节点的状态
    int ws = pred.waitStatus;
    // 如果上一个节点的状态为SINGNAL(-1),表示一切正常
    if (ws == Node.SIGNAL)
        return true;
    // 如果ws的状态大于0   
    if (ws > 0) {
        do {
        	// 将当前节点的指针指向上一个的上一个
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0); // 一定找到小于等于0
        // 将重新标识号的最近的有效的节点的下一个节点的指针指向当前节点
        pred.next = node;
    } else {
    	// 如果上一个节点的状态小于等于0,不等于-1,将上一个有效节点的状态改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

cancelAcquire

如果线程挂起失败,调用这个方法

// node就是当前的这个节点
private void cancelAcquire(Node node) {
    // 做健壮性判断
    if (node == null)
        return;
	// node不为null才的前提下执行
    // 将node的线程置为null,竞争锁资源跟我就没有关系了
    node.thread = null;
	// 获取当前节点的上一个节点
    Node pred = node.prev;
    // 前驱节点的状态>0
    while (pred.waitStatus > 0)
        // 找到前驱节点最近的一个有效节点
        node.prev = pred = pred.prev;
	// 将最近的第一个有效节点的后继节点声明出来
    Node predNext = pred.next;
	// 将当前节点的状态置为失效节点,给别的node看的
    node.waitStatus = Node.CANCELLED;
	// 如果当前节点是尾节点,就会将尾节点设置成为最近的有效节点(如果当前节点是尾节点的操作)
    if (node == tail && compareAndSetTail(node, pred)) {
        // 用cas的当时将尾节点的next属性设置为null
        compareAndSetNext(pred, predNext, null);
    } else {
        // 如果当前节点是中间节点的操作
        int ws;
        // 如果上一个节点不是头节点
        if (pred != head &&
            // 判断上一个节点的状态是不是有效
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             // 如果上一个节点不是有效节点,并且ws<=0的话,将上一个节点设置为有效节点
             // pred需要唤醒后继节点
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            // 并且pred的线程不是null
            pred.thread != null) {
            Node next = node.next;
            // 尝试将pred的前驱节点的next指向当前节点的next(必须是有效的next节点)
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        // 如果当前节点是头节点的操作    
        } else {
            unparkSuccessor(node);
        }

        node.next = node; 
    }
}

是不是很懵逼,不着着急,接下来进行画图展示

image-20240308122858781

将这个方法划成两个区域,红色框表示找到离当前节点最近的有效节点,绿框表示根据当前节点的位置进行响应的操作处理

红框代码分析,看图

image-20240308130050988

image-20240308125915322

image-20240308125943317

找到当前节点最近的有效节点,当前节点的pred指针指向这个有效节点

绿框代码分析

如果当前节点时尾节点

image-20240308130415394

image-20240308131551575

如果当前节点不是尾节点

image-20240308131835564

首先分析第一种情况,当前节点不是尾节点并且最近的有效节点不是头节点的情况

image-20240308140006613

分析第二种情况,当前节点不是尾节点并且最近的有效节点是头节点的情况

// node为当前节点(不是尾节点) 
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    // 如果ws<0,就通过cas的方式将ws设置为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    // 如果当前节点的下一个节点是null或者是无效节点,则进入if结构体
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从双向队列的尾端开始遍历,找到离尾端最近的一个有效节点
        // 如果找到了,将此节点引用给s变量
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果s是有效节点,则进入此结构体
    if (s != null)
        // 唤醒这个有效节点的线程
        LockSupport.unpark(s.thread);
}

image-20240308143330983

解锁

reentrantLock.unlock();

下面我们分析解锁的过程,这个过程比较简单,话不多说,直接上源码

// 调用了sync.release方法,点进去查看
public void unlock() {
    sync.release(1);
}

release()

// arg的值为1
public final boolean release(int arg) {
    // 调用tryRelease方法,表示如果解锁成功(state在主存中的值为0),则返回true
    if (tryRelease(arg)) {
        // 如果锁已经完全释放,获得头节点
        Node h = head;
       	// 如果头节点不是null值并且ws不为0的话,进入if结构体
        if (h != null && h.waitStatus != 0)
            // 调用unparkSuccessor方法
            unparkSuccessor(h);
        return true;
    }
    // 如果解锁失败返回false,或者解锁成功后,state的值不为0,代表进行了重入锁的操作,也会返回false
    return false;
}

tryRelease()

// releases值为1
protected final boolean tryRelease(int releases) {
    // 获取主存中state的值,然后进行-1操作
    int c = getState() - releases;
    // 如果主存中Exclusive的值不是当前线程,肯定出现了问题,抛出异常
    // 这里怎么理解呢?比如当前线程获取到了锁,Exclusive的值肯定是当前线程对象...如果不是当前线程对象
    // 就肯定出现了问题
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 设置一个标识符false
    boolean free = false;
    // 判断c是否为0,如果为0,代表锁完全释放,所以将Exclusive的值设置为null
    if (c == 0) {
        // free代表的意思就是锁是否完全释放了
        free = true;
        // 设置Exclusive的值为null,只有锁完全释放了,才为null值
        setExclusiveOwnerThread(null);
    }
    // 将新的state值刷回到主存
    setState(c);
    return free;
}

unparkSuccessor()

// node为头节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
	
	// 这里就是从队列的尾端向前进行遍历操作,找到距离尾端最近的一个有效节点(ws<=0),然后唤醒该节点中对应的线
	// 程
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒该线程,如果头节点的下一个节点符合条件就直接唤醒下一个节点的线程
        LockSupport.unpark(s.thread);
}

各种锁的理解

公平锁、非公平锁

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

非公平锁:非常不公平,可以插队!

Synchronized和Lock锁默认的都是非公平锁,看下面代码实例!

// 默认非公平锁
public ReentrantLock() {
	sync = new NonfairSync();
}

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

可重入锁

又叫递归锁

在这里插入图片描述

手写可重入锁

Synchronized

public class Demo1 {

    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(() ->{
            phone.sms();
        }, "A").start();

        new Thread(() -> {
            phone.call();
        }, "B").start();
    }

}

class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName() + "sms");
        call();// 这里也有锁,拿到外面的锁,自然也就拿到了里面的锁
    }

    public synchronized void call(){
        System.out.println(Thread.currentThread().getName() + "call");
    }
}

Lock

package com.guocl.Lock;

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

public class Demo2 {

    public static void main(String[] args) {
        Phone2 phone2 = new Phone2();

        new Thread(() ->{
            phone2.sms();
        }, "A").start();

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }

}

class Phone2{
    Lock lock = new ReentrantLock();

    public void sms(){
        // 细节问题:lock.lock(); lock.unlock();
        // lock 锁必须配对,否则就会死在里面
        lock.lock();  
        lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + "sms");
            call();// 这里也有锁
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public void call(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "call");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

执行结果:

Asms
Acall
Bcall

Process finished with exit code 0

两个锁的执行结果相同,当A线程拿到第一个锁之后,就自动拿到下面的锁了,等这个线程的锁全部执行完才会释放锁。

自旋锁

spinlock 自旋锁必须有CAS操作

在这里插入图片描述

我们来自定义一个锁测试

import java.util.concurrent.atomic.AtomicReference;

/**
 * 自旋锁
 */
public class SpinlockDemo {

    // int 0
    // Thread null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    // 加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> mylock");

        //自旋锁 必须有CAS操作
        while (!atomicReference.compareAndSet(null, thread)){

        }
    }

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

测试

package com.guocl.Lock;

import java.util.concurrent.TimeUnit;

public class SpinlockTest {

    public static void main(String[] args) throws InterruptedException {
        // ReentrantLock reentrantLock = new ReentrantLock();
        // reentrantLock.lock();
        // reentrantLock.unlock();

        // 底层使用的自旋锁CAS
        SpinlockDemo lock = new SpinlockDemo();

        new Thread(() -> {
            lock.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            lock.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.myUnLock();
            }
        }, "T2").start();
    }
}

执行结果:

T1==> mylock
T2==> mylock
T1==> myUnLock
T2==> myUnLock

Process finished with exit code 0

死锁

死锁是什么

在这里插入图片描述

死锁测试,怎么排除死锁:

import java.util.concurrent.TimeUnit;

public class DeadLockDemo {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new MyThread(lockA, lockB), "T1").start();
        new Thread(new MyThread(lockB, lockA), "T1").start();
    }

}

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB){
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread()
                               .getName() + "lock:" + lockA + "=>get:" + lockB);

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread()
                                   .getName() + "lock:" + lockB + "=>get:" + lockA);
            }
        }
    }
}

执行结果:造成死锁,程序被锁住

image-20240120194627218

解决死锁问题

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

image-20240120194725146

2、使用 jstack 进程号 找到死锁问题

image-20240120195011002

面试,工作中!排查问题:

  • 1、日志

  • 2、堆栈

posted @ 2025-07-06 00:15  PursueExcellence  阅读(11)  评论(0)    收藏  举报