万文详解JUC(超详细)

发布时间:2023年12月18日

生命无罪,健康万岁,我是laity。

我曾七次鄙视自己的灵魂:

第一次,当它本可进取时,却故作谦卑;

第二次,当它在空虚时,用爱欲来填充;

第三次,在困难和容易之间,它选择了容易;

第四次,它犯了错,却借由别人也会犯错来宽慰自己;

第五次,它自由软弱,却把它认为是生命的坚韧;

第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;

第七次,它侧身于生活的污泥中,虽不甘心,却又畏首畏尾。

什么是JUC?

JUC:指的是java.util三个并发编程工具包的简称

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

img

java.util 工具包

线程和进程

线程和进程,如果不能使用一句话说出来的 - 技术不扎实!

  • 进程:一个程序,QQ.exe - 程序的集合

    • 一个进程往往包含多个线程,至少包含一个!
    • Java默认有几个线程?2个 :main、GC线程
  • 线程:开了一个进程notepad++,写字 - 会自动保存(线程负责的!)

在Java中开启线程的方式:

  • Thread
  • Runnable
  • Callable:实现Callable接口 + FutureTask(可以拿到返回值,可以处理异常) jdk1.5
  • 线程池

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/laity-champion/base/fk2vsqq25g3egpyl

问题:Java真的可以开启线程吗?

先说结论:不可以的开启

    public static void main(String[] args) {
        new Thread().start();
    }

点击start(); 查看源码img

在Java等编程语言中,“native” 用来表示由本地代码(通常是用C或C++编写的)实现的方法。这些方法可以通过 Java Native Interface(JNI)调用。这样的方法通常涉及对底层系统或硬件的直接访问,提供了与平台相关的功能。

    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多核,多个线程可以同时执行;
    • 并行是指系统实际上同时执行多个任务或操作,即多个任务在同一时刻内执行。与并发不同,并行关注的是同时发生的事件,而不仅仅是在短时间内交替发生。
    • 并行的目标是更快地完成任务,通过同时处理多个子任务,从而提高整体性能。在硬件层面,多核处理器、GPU等可以实现并行。

并发编程本质

充分利用CPU资源 -> 提高效率

管程

  • 管程是一种用于同步和互斥的高级抽象。它包含了共享数据和用于访问这些数据的一组过程。管程提供了一种结构化的方法来确保在并发程序中对共享资源的访问是安全的。Java中的 synchronized 关键字就是一种管程的实现。

    • 管程是一种用于解决多线程并发问题的高级同步机制。它结合了数据(共享资源)和程序(对共享资源的操作);
    • 在Java中,synchronized 关键字和 wait()、notify() 方法是实现管程的基本工具。管程提供了对共享资源的互斥访问和条件等待的支持。

用户线程

用户线程是由应用程序开发人员创建和控制的线程。这些线程在用户空间中运行,而不需要内核支持。用户线程的创建、调度和销毁都由用户空间的线程库来管理,而不涉及操作系统内核的介入。

  • 用户线程由应用程序开发人员创建和控制,线程的创建、调度和销毁都由用户空间的线程库来管理。
  • 用户线程相对较轻量,线程切换成本较低。然而,它们受限于操作系统的调度,可能会被阻塞。

守护线程

守护线程是在程序运行时在后台提供服务的线程。当所有的非守护线程结束时,程序会退出,不会等待守护线程执行完毕。典型的守护线程例子是垃圾回收线程。在Java中,可以通过 setDaemon(true) 方法将线程设置为守护线程。

  • 守护线程在程序运行时在后台提供服务,典型的例子是垃圾回收线程。
  • 当所有的非守护线程结束时,程序会退出,而不会等待守护线程执行完毕。守护线程通常用于执行一些后台任务,不影响程序的正常运行。

回顾多线程

线程有几个状态?

Thread.State 线程枚举类可以自行查看

    /**
     * A thread state.  A thread can be in one of the following states:
     * <ul>
     * <li>{@link #NEW}<br>
     *     A thread that has not yet started is in this state.
     *     </li>
     * <li>{@link #RUNNABLE}<br>
     *     A thread executing in the Java virtual machine is in this state.
     *     </li>
     * <li>{@link #BLOCKED}<br>
     *     A thread that is blocked waiting for a monitor lock
     *     is in this state.
     *     </li>
     * <li>{@link #WAITING}<br>
     *     A thread that is waiting indefinitely for another thread to
     *     perform a particular action is in this state.
     *     </li>
     * <li>{@link #TIMED_WAITING}<br>
     *     A thread that is waiting for another thread to perform an action
     *     for up to a specified waiting time is in this state.
     *     </li>
     * <li>{@link #TERMINATED}<br>
     *     A thread that has exited is in this state.
     *     </li>
     * </ul>
     *
     * <p>
     * A thread can be in only one state at a given point in time.
     * These states are virtual machine states which do not reflect
     * any operating system thread states.
     *
     * @since   1.5
     * @see #getState
     */
    public enum State {
        // 线程新生
        NEW,
        // 线程运行
        RUNNABLE,
        // 线程阻塞
        BLOCKED,
        // 线程等待
        WAITING,
        // 超时等待 - 死死的等
        TIMED_WAITING,
        // 线程终止
        TERMINATED;
    }

可以看到有6个状态,分别是:

  • 线程新生
  • 线程运行
  • 线程阻塞
  • 线程等待
  • 超时等待 - 死死的等
  • 线程终止

wait和sleep区别

  • 来自不同的类

    • wait -> Object
    • sleep -> Thread
  • 关于锁的释放

    • wait -> 会释放锁
    • sleep -> 睡觉了,抱着锁睡觉,不会释放!
  • 使用的范围不同

    • wait -> 必须在同步代码块中睡
    • sleep -> 可以在任何地方睡
  • 是否需要捕获异常

    • wait -> 不需要捕获异常 - 但是有中断异常需要捕获
    • sleep -> 必须要捕获异常 - 因为有可能发送超时等待问题

Lock锁(重点)

传统 Synchronized

package com.laity.demo1;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo1.SaleTicketDemo01
 * @Date: 2023年11月21日 23:26
 * @Description: 基本卖票例子
 * 真正的多线程开发,公司中的开发,降低耦合性(高内聚、低耦合)
 * 线程就是一个单独的资源类,没有附属的操作
 * 资源类包含:1、属性;2、方法
 */

public class SaleTicketDemo01 {
    public static void main(String[] args) {
        // 多线程操作
        // 并发:多个线程操作同一个资源类,
        // 操作:把资源类丢进线程
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程A").start();

        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程B").start();

        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程C").start();
    }
}

// 单独的资源类  OOP编程
class Ticket {
    // 属性、方法
    private int number = 50;

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

结果 就会很有顺序,不加synchronized就会很乱:

线程A - 卖出了第10张票,剩余9
线程A - 卖出了第9张票,剩余8
线程A - 卖出了第8张票,剩余7
线程A - 卖出了第7张票,剩余6
线程A - 卖出了第6张票,剩余5
线程A - 卖出了第5张票,剩余4
线程A - 卖出了第4张票,剩余3
线程A - 卖出了第3张票,剩余2
线程A - 卖出了第2张票,剩余1
线程A - 卖出了第1张票,剩余0

Process finished with exit code 0

Lock接口

img

实现类

img

img

公平锁:十分公平,先来后到(阳光普照,效率相对低一些)

非公平锁:十分不公平,可以插队(默认,可能会造成线程饿死,但是效率高)

package com.laity.demo1;

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

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo1.SaleTicketDemo01
 * @Date: 2023年11月21日 23:26
 * @Description: 基本卖票例子
 * 真正的多线程开发,公司中的开发,降低耦合性(高内聚、低耦合)
 * 线程就是一个单独的资源类,没有附属的操作
 * 资源类包含:1、属性;2、方法
 */

public class SaleTicketDemo02 {
    public static void main(String[] args) {
        // 多线程操作
        // 并发:多个线程操作同一个资源类,
        // 操作:把资源类丢进线程
        Ticket2 ticket = new Ticket2();
        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程A").start();

        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程B").start();

        new Thread(() -> {
            for (int j = 0; j < 30; j++) {
                ticket.sale();
            }
        }, "线程C").start();
    }
}

// 单独的资源类  OOP编程
// lock三部曲
// 1、new ReentrantLock();
// 2、lock.lock() // 加锁
// 3、finally -> lock.unlock(); // 解锁
class Ticket2 {
    // 属性、方法
    private int number = 50;

    // 使用 juc lock锁
    Lock lock = new ReentrantLock();

    // 卖票的方法
    public void sale() {
        // 加锁
        lock.lock();

        // ctrl + alt + t 快捷键
        try {
            // 业务代码
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + " - 卖出了第" + (number--) + "张票" + ",剩余" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

效果与Synchronized一样

Synchronized和Lock的区别

1.synchronized 是内置的Java关键字,Lock是Java的一个接口

2.synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁

3.synchronized 会自动释放锁,Lock必须手动释放锁!如果不释放锁,会造成死锁

4.synchronized 线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着;Lock就不一定会等待下去

5.synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平/公平(可以自己设置,默认非公平锁)

6.synchronized 适合锁少量的同步代码;Lock适合锁大量同步代码!

7.Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者性能是差不多的,而当竞争资源非常激烈时(即大量线程同时竞争),此时Lock的性能要远远优于synchronized

问题:锁是什么,如何判断锁的是谁?

个人理解:锁是让一个线程在时间内走一个执行代码,锁的是多个线程锁共享的对象(标志位、临界资源)和Class(static修饰)。

生产者和消费者问题

Synchronized版本 -> wait(等待)、notify(唤醒)

JUC版本lock -> Condition

面试四大考点:单例模式单例设计模式-复习、排序算法、生产者和消费者问题、死锁

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

Synchronized版本 -> wait(等待)、notify(唤醒) : 传统的三剑客

package com.laity.demo2;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo2.Communication01
 * @Date: 2023年11月22日 00:25
 * @Description: 线程直接通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行  t1 t2 两个线程操作同一个变量,进行 +1 | -1
 */

public class Communication01 {
    static Thread t1 = null, t2 = null;

    public static void main(String[] args) {
        Data data = new Data();
        t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

// 资源类:线程就是一个单独的资源类,没有附属的操作
// 判断等待、业务、通知
class Data {
    private int number = 0;

    // +1
    // 只要有并发就一定要加锁
    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            // 等待
            this.wait();
        }
        // 业务
        number++;
        // 加1后通知其它线程我加一完毕了
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        this.notifyAll();
    }

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

输出结果:

t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0
t1==>1
t2==>0

两个线程可以看到结果是很正常的;但是也问题存在,4个线程或者8个线程,就不安全了

Synchronized多线程导致不安全

package com.laity.demo2;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo2.Communication01
 * @Date: 2023年11月22日 00:25
 * @Description: 线程直接通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
 * 线程交替执行  t1 t2 两个线程操作同一个变量,进行 +1 | -1
 */

public class Communication01 {
    static Thread t1 = null, t2 = null, t3 = null, t4 = null;

    public static void main(String[] args) {
        Data data = new Data();
        t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2");

        t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t3");

        t4 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

// 资源类:线程就是一个单独的资源类,没有附属的操作
// 判断等待、业务、通知
class Data {
    private int number = 0;

    // +1
    // 只要有并发就一定要加锁
    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            // 等待
            this.wait();
        }
        // 业务
        number++;
        // 加1后通知其它线程我加一完毕了
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        this.notifyAll();
    }

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

输出结果:

t1==>1
t3==>0
t4==>1
t1==>2
t4==>3
t2==>2
t2==>1
t2==>0
t3==>-1
t3==>-2
t3==>-3
t3==>-4
t3==>-5
t3==>-6
t3==>-7
t3==>-8
t3==>-9
t2==>-10
t2==>-11
t2==>-12
t2==>-13
t2==>-14
t2==>-15
t2==>-16
t4==>-15
t1==>-14
t4==>-13
t1==>-12
t4==>-11
t1==>-10
t4==>-9
t1==>-8
t4==>-7
t1==>-6
t4==>-5
t1==>-4
t4==>-3
t1==>-2
t4==>-1
t1==>0

四个线程的运行结果就很明显了,可以看出是有问题的,但是问题是由什么造成的呢?

导致Synchronized多线程不安全的原因

虚假唤醒 - JDK源码文档中有提及:

img

解决线程虚假唤醒

由if判断改为官方的while判断即可

输出结果:

D:\Java\JDK\bin\java.exe "-javaagent:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\lib\idea_rt.jar=64439:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\bin" -Dfile.encoding=UTF-8 -classpath D:\java\JDK\jre\lib\charsets.jar;D:\java\JDK\jre\lib\deploy.jar;D:\java\JDK\jre\lib\ext\access-bridge-64.jar;D:\java\JDK\jre\lib\ext\cldrdata.jar;D:\java\JDK\jre\lib\ext\dnsns.jar;D:\java\JDK\jre\lib\ext\jaccess.jar;D:\java\JDK\jre\lib\ext\jfxrt.jar;D:\java\JDK\jre\lib\ext\localedata.jar;D:\java\JDK\jre\lib\ext\mysql-connector-java-5.1.48.jar;D:\java\JDK\jre\lib\ext\nashorn.jar;D:\java\JDK\jre\lib\ext\sunec.jar;D:\java\JDK\jre\lib\ext\sunjce_provider.jar;D:\java\JDK\jre\lib\ext\sunmscapi.jar;D:\java\JDK\jre\lib\ext\sunpkcs11.jar;D:\java\JDK\jre\lib\ext\zipfs.jar;D:\java\JDK\jre\lib\javaws.jar;D:\java\JDK\jre\lib\jce.jar;D:\java\JDK\jre\lib\jfr.jar;D:\java\JDK\jre\lib\jfxswt.jar;D:\java\JDK\jre\lib\jsse.jar;D:\java\JDK\jre\lib\management-agent.jar;D:\java\JDK\jre\lib\plugin.jar;D:\java\JDK\jre\lib\resources.jar;D:\java\JDK\jre\lib\rt.jar;D:\LaityWork\architecture\foodie-dev\target\classes;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter\2.1.5.RELEASE\spring-boot-starter-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot\2.1.5.RELEASE\spring-boot-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-context\5.1.7.RELEASE\spring-context-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-autoconfigure\2.1.5.RELEASE\spring-boot-autoconfigure-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-logging\2.1.5.RELEASE\spring-boot-starter-logging-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\jul-to-slf4j\1.7.26\jul-to-slf4j-1.7.26.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-core\5.1.7.RELEASE\spring-core-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jcl\5.1.7.RELEASE\spring-jcl-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-web\2.1.5.RELEASE\spring-boot-starter-web-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-json\2.1.5.RELEASE\spring-boot-starter-json-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-databind\2.9.8\jackson-databind-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-core\2.9.8\jackson-core-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.8\jackson-datatype-jdk8-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.8\jackson-datatype-jsr310-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.8\jackson-module-parameter-names-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-tomcat\2.1.5.RELEASE\spring-boot-starter-tomcat-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-core\9.0.19\tomcat-embed-core-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-el\9.0.19\tomcat-embed-el-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.19\tomcat-embed-websocket-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\hibernate\validator\hibernate-validator\6.0.16.Final\hibernate-validator-6.0.16.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-web\5.1.7.RELEASE\spring-web-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-beans\5.1.7.RELEASE\spring-beans-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-webmvc\5.1.7.RELEASE\spring-webmvc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-expression\5.1.7.RELEASE\spring-expression-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-aop\2.1.5.RELEASE\spring-boot-starter-aop-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-aop\5.1.7.RELEASE\spring-aop-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\aspectj\aspectjweaver\1.9.4\aspectjweaver-1.9.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-configuration-processor\2.1.5.RELEASE\spring-boot-configuration-processor-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\projectlombok\lombok\1.18.10\lombok-1.18.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.1.0\mybatis-spring-boot-starter-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-jdbc\2.1.5.RELEASE\spring-boot-starter-jdbc-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\zaxxer\HikariCP\3.2.0\HikariCP-3.2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jdbc\5.1.7.RELEASE\spring-jdbc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-tx\5.1.7.RELEASE\spring-tx-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.1.0\mybatis-spring-boot-autoconfigure-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis-spring\2.0.2\mybatis-spring-2.0.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-starter\2.1.5\mapper-spring-boot-starter-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-core\1.1.5\mapper-core-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\persistence\persistence-api\1.0\persistence-api-1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-base\1.1.5\mapper-base-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-weekend\1.1.5\mapper-weekend-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring\1.1.5\mapper-spring-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-extra\1.1.5\mapper-extra-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-autoconfigure\2.1.5\mapper-spring-boot-autoconfigure-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-starter\1.2.12\pagehelper-spring-boot-starter-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-autoconfigure\1.2.12\pagehelper-spring-boot-autoconfigure-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper\5.1.10\pagehelper-5.1.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\jsqlparser\jsqlparser\2.0\jsqlparser-2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-codec\commons-codec\1.11\commons-codec-1.11.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-io\commons-io\2.4\commons-io-2.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger2\2.4.0\springfox-swagger2-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-annotations\1.5.6\swagger-annotations-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-models\1.5.6\swagger-models-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spi\2.4.0\springfox-spi-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-core\2.4.0\springfox-core-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-schema\2.4.0\springfox-schema-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-common\2.4.0\springfox-swagger-common-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spring-web\2.4.0\springfox-spring-web-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\google\guava\guava\18.0\guava-18.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\classmate\1.4.0\classmate-1.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-core\1.2.0.RELEASE\spring-plugin-core-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-metadata\1.2.0.RELEASE\spring-plugin-metadata-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-ui\2.4.0\springfox-swagger-ui-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\xiaoymin\swagger-bootstrap-ui\1.6\swagger-bootstrap-ui-1.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-api\1.7.21\slf4j-api-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-log4j12\1.7.21\slf4j-log4j12-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\log4j\log4j\1.2.17\log4j-1.2.17.jar com.laity.demo2.Communication01
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0
t1==>1
t3==>0
t4==>1
t2==>0

Process finished with exit code 0

可以看到结果正常,已解决线程虚假唤醒问题。

JUC版本的生产者和消费者问题

通过java.util.concurrent.locks 的Lock找到Condition

img

img

代码实现:

package com.laity.demo2;

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

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo2.Communication02
 * @Date: 2023年11月22日 21:51
 * @Description: JUC版本 生产者和发布者问题
 */

public class Communication02 {

    static Thread t1 = null, t2 = null, t3 = null, t4 = null;

    public static void main(String[] args) {
        DataSource data = new DataSource();
        t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2");

        t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t3");

        t4 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

// 资源类:线程就是一个单独的资源类,没有附属的操作
class DataSource {
    private int number = 0;

    // 使用JUC创建锁
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();

    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                // 等待
                notFull.await();
            }
            // 业务
            number++;
            System.out.println(Thread.currentThread().getName() + "==>" + number);
            // 通知唤醒
            notFull.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                // 等待
                notFull.await();
            }
            // 业务
            number--;
            System.out.println(Thread.currentThread().getName() + "==>" + number);
            // 通知唤醒
            notFull.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

结果没有问题,但是Condition的优势在哪里呢?这和Synchronized的完全没有区别啊?

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

采用JUC的Condition优势

  • 可以精准的通知和唤醒线程

代码测试:

package com.laity.demo2;

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

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.demo2.Communication03
 * @Date: 2023年11月22日 22:19
 * @Description: 基于JUC的Condition实现精准的通知和唤醒线程
 * 需求 t1执行完,执行t2,t2执行完,执行t3,t3执行完,执行t4,t4执行完 调用t1
 */

public class Communication03 {
    static Thread t1 = null, t2 = null, t3 = null, t4 = null;
    public static void main(String[] args) {
        DataService data = new DataService();
        t1 = new Thread(data::printA, "t1");

        t2 = new Thread(data::printB, "t2");

        t3 = new Thread(data::printC, "t3");

        t4 = new Thread(data::printD, "t4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

// 唯一资源类
class DataService {
    // 使用JUC创建锁
    private final Lock lock = new ReentrantLock();
    // 监视器
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();
    private final Condition condition3 = lock.newCondition();
    private final Condition condition4 = lock.newCondition();

    // 模拟一个标识位
    private int number = 1; // 1-t1;2-t2;3-t3;4-t4

    public void printA(){
        lock.lock();
        try {
            // 业务代码 : while条件判断 -> 程序执行 -> 通知唤醒
            while (number!=1) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + " - number == 1");
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            // 业务代码 : while条件判断 -> 程序执行 -> 通知
            while (number!=2) {
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + " - number == 2");
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 业务代码 : while条件判断 -> 程序执行 -> 通知
            while (number!=3) {
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + " - number == 3");
            number = 4;
            condition4.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printD(){
        lock.lock();
        try {
            // 业务代码 : while条件判断 -> 程序执行 -> 通知
            while (number!=4) {
                condition4.await();
            }
            System.out.println(Thread.currentThread().getName() + " - number == 4");
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

输出结果:

t1number == 1
t2number == 2
t3number == 3
t4number == 4

8锁现象

在Java中,"8锁"问题通常指的是在多线程环境下对对象的锁定问题,特别是在使用synchronized关键字时可能遇到的情况。这个问题涉及到Java中的对象锁、方法锁以及静态方法锁。

具体来说,"8锁"问题通常涉及以下几种情况:

  1. 对象锁:
    • 普通同步方法,锁是当前实例对象。
    • 静态同步方法,锁是当前类的Class对象。
    • 同步方法块,锁是括号里面的对象。
  1. 方法参数问题:
    • 如果两个线程访问的是同一个对象实例的不同普通同步方法,它们之间是有竞争关系的。
    • 如果两个线程访问的是同一个对象的不同同步方法,一个是普通同步方法,一个是静态同步方法,它们之间也是有竞争关系的。
  • 这里的"8锁"主要来源于这四种情况的组合,即2种对象锁(普通同步方法和同步方法块)乘以2种方法锁(普通方法和静态方法)等于8种可能的组合。

下面是一个简单的例子,展示了可能的"8锁"情况:

public class SynchronizedExample {
    public synchronized void method1() {
        // 对象锁:当前实例对象
    }

    public static synchronized void method2() {
        // 对象锁:当前类的Class对象
    }

    public void method3() {
        synchronized (this) {
            // 对象锁:括号里面的对象
        }
    }

    public static void method4() {
        synchronized (SynchronizedExample.class) {
            // 对象锁:括号里面的对象是当前类的Class对象
        }
    }
}

在多线程环境中,对于这些同步方法和同步块的组合,可能会导致不同的线程竞争锁,从而产生不同的执行结果。这就是"8锁"问题的核心。解决这个问题通常需要考虑同步的粒度、锁的选择以及代码的执行顺序等因素。

小结

  1. 锁的种类:主要有两种锁,分别是对象锁(实例锁)和类锁。
  2. 锁的粒度:分为两种,分别是粗粒度锁和细粒度锁。

下面是这8种加锁情况:

  1. 对象锁(实例锁)+ 同步方法:锁住当前实例对象。
  2. 对象锁(实例锁)+ 同步代码块:锁住括号里面的对象。
  3. 对象锁(实例锁)+ 静态方法:锁住当前类的Class对象。
  4. 对象锁(实例锁)+ 静态代码块:锁住当前类的Class对象。
  5. 类锁 + 同步方法:锁住当前类的Class对象。
  6. 类锁 + 同步代码块:锁住当前类的Class对象。
  7. 类锁 + 静态方法:锁住当前类的Class对象。
  8. 类锁 + 静态代码块:锁住当前类的Class对象。

这里涉及到了两个不同的锁:对象锁和类锁。同时,锁的粒度也有两种:粗粒度锁和细粒度锁。

在多线程环境下,如果不同的线程分别使用了这些锁的组合,可能会导致不同的线程争夺锁资源,从而产生死锁、性能下降等并发问题。理解并正确使用锁是多线程编程中非常重要的一部分,避免出现不必要的并发问题。

集合类不安全

List不安全

代码:集合类不安全 - List不安全

package com.laity.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.unsafe.ListTest
 * @Date: 2023年11月25日 21:20
 * @Description: 集合类不安全 - List不安全
 */

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        // List<String> list = Collections.synchronizedList(arrayList);
        // List<String> list = new Vector<>();
        // List<String> list = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            // 单线程下确实很安全,所以模拟下多下次
            // arrayList.add(UUID.randomUUID().toString().substring(0, 5));
            // System.out.println(arrayList);

            // 多线程下:异常
            /*
            Exception in thread "5" Exception in thread "1" Exception in thread "7" Exception in thread "0" java.util.ConcurrentModificationException
            java.util.ConcurrentModificationException 只要并发下,集合里面都会出现这个问题
            ConcurrentModificationException => 并发修改异常
             */
            // 并发下ArrayList是不安全的
            /*
            解决方案:
            1、将 new ArrayList<>(); 换为 new Vector<>(); 不好
            2、使用工具类 List<String> list = Collections.synchronizedList(arrayList);
            3、使用JUC中的 List<String> list = new CopyOnWriteArrayList<>();
                - private transient volatile Object[] array;
                volatile:使用 volatile 修饰 instance 可以确保在多线程环境下,一个线程对 instance 的修改对其他线程是可见的。
                - CopyOnWrite:写入时复制,COW 计算机程序设计领域的一种优化策略;
                    - list被多个线程调用的时候,读取到的是固定的,写入的时候避免其它线程将数据覆盖,造成数据问题(脏读);
                    - 就是一种读写分离的思想,写入的时候复制出来,写入完再插回,保证线程安全
                - CopyOnWriteArrayList 比 Vector
                    - Vector效率低(只要使用了Synchronized就会效率低)
                    - CopyOnWriteArrayList使用的是Lock锁
             */
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(Thread.currentThread().getName() + list);
            }, String.valueOf(i)).start();
        }
    }
}

Vector JDK1.0发布的,比ArrayList发布还要早,所以不推荐

img

CopyOnWriteArrayList 写入时复制 - 推荐

img

Set不安全

img

package com.laity.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.unsafe.SetTest
 * @Date: 2023年11月25日 21:57
 * @Description: 集合不安全 - Set集合不安全
 */

public class SetTest {
    public static void main(String[] args) {
        // Set<String> set = new CopyOnWriteArraySet<>();
        Set<Object> set = Collections.synchronizedSet(new HashSet<>());
        // Set<String> set = new HashSet<>();

        // 同理可证:java.util.ConcurrentModificationException ==> 并发修改异常
        /*
        解决方案:
        1、Set<Object> set = Collections.synchronizedSet(new HashSet<>());
            - 还是内部使用了Synchronized锁,性能比较低 - 不推荐
        2、Set<String> set = new CopyOnWriteArraySet<>();
            - 内部使用:final transient ReentrantLock lock = new ReentrantLock();
                :JUC的重入锁
         */
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

HashSet底层是HashMap

Map不安全

回顾Map

img

代码

package com.laity.unsafe;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.unsafe.MapTest
 * @Date: 2023年11月26日 02:16
 * @Description: 集合不安全 - Map集合不安全
 */

public class MapTest {
    public static void main(String[] args) {
        // map是这样用的吗? 不是,工作中不用 HashMap
        // 默认等价于什么?
        // Map<String, String> map = new HashMap<>();
        // 加载因子,初始化容量
        // (16, 0.75f)
        // Map<Object, Object> map = new HashMap<>(1 << 4, 0.75f);
        Map<String, String> map = new ConcurrentHashMap<>(1 << 1);
        // java.util.ConcurrentModificationException - 并发修改异常
        /*
        ConcurrentHashMap<>();
         */
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(1, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

分析小技巧

  • 先会用
  • 货比三家 - 比较|寻找 其它解决方案
  • 分析源码

List和Set区别

  • list可以插入多个null元素,而set只允许插入一个null元素;
  • list容器是有序的,而set容器是无序的;
  • list方法可以允许重复的对象,而set方法不允许重复对象等等。

HashSet底层是什么?

// HashSet底层是HashMap
public HashSet() {
    map = new HashMap<>();    
}

所以HashSet的本质就是一个HashMap

img

可以看出 值存自于HashMap中的Key map的key是一个无法重复的,Value是一个Object空对象 - 不变的值。

Java位运算基本操作

位运算是一种计算机中的基本运算,它针对二进制位进行操作。

Java中的位运算符包括按位与(&)、按位或(|)、按位异或(^)、取反(~)以及

左移位和右移位运算符(<<, >>, >>>)等。

按位与(&)

按位与的原理

按位与运算是指两个二进制数对应位上的数字都为1时,结果位上的数字才为1;否则结果位上的数字为0。按位与通常用于掩码操作或清零操作。

举例说明:

3 & 5 = 1
3 的二进制表示为 0011
5 的二进制表示为 0101
& 的运算规则:
    0 & 0 = 0
    0 & 1 = 0
    1 & 0 = 0
    1 & 1 = 1
因此,3 & 5 = 0011 & 0101 = 0001 = 1
按位与的用途

按位与运算主要用于掩码操作或清零操作。通过按位与运算可以将某些二进制位设置为0,从而达到掩码操作或清零操作的目的。

例如:

  • 判断一个数是否为奇数:x & 1 == 1
  • 清除一个数的二进制末n位:x & (~0 << n)
按位与的实例

以下是按位与运算的几个实例:

public class BitwiseAndExample {
    public static void main(String[] args) {
        int a = 63; // 二进制:0011 1111
        int b = 15; // 二进制:0000 1111
        int c = a & b; // 二进制:0000 1111
        System.out.println("a & b = " + c); // 输出:a & b = 15

        int x = 10; // 二进制:1010
        if ((x & 1) == 1) {
            System.out.println(x + " is odd number."); // 输出:10 is odd number.
        } else {
            System.out.println(x + " is even number.");
        }

        int num = 0b1111_0000_1111_0101;
        int n = 8;
        num &= ~(0xFF >>> n);
        System.out.println(Integer.toBinaryString(num)); // 输出:1111

    }
}

按位或(|)

按位或的原理

按位或运算是指两个二进制数对应位上的数字有一个为1时,结果位上的数字就为1。按位或运算通常用于设置某些二进制位为1。

举例说明:

3 | 5 = 7
3 的二进制表示为 0011
5 的二进制表示为 0101
| 的运算规则:
    0 | 0 = 0
    0 | 1 = 1
    1 | 0 = 1
    1 | 1 = 1
因此,3 | 5 = 0011 | 0101 = 0111 = 7
按位或的用途

按位或运算主要用于设置某些二进制位为1。通过按位或运算可以将某些二进制位设置为1,从而达到设置操作的目的。

例如:

  • 将一个数的二进制末n位设置为1:x | ((1 << n) - 1)
按位或的实例

以下是按位或运算的几个实例:

public class BitwiseOrExample {
    public static void main(String[] args) {
        int a = 63; // 二进制:0011 1111
        int b = 15; // 二进制:0000 1111
        int c = a | b; // 二进制:0011 1111
        System.out.println("a | b = " + c); // 输出:a | b = 63

        int x = 10; // 二进制:1010
        x |= 1; // 将x的最后一位设置为1,即变成奇数
        System.out.println(x); // 输出:11

        int num = 0b1111;
        int n = 8;
        num |= (0xFF >>> n);
        System.out.println(Integer.toBinaryString(num)); // 输出:1111_1111
    }
}

按位异或(^)

按位异或的原理

按位异或运算是指两个二进制数对应位上的数字不相同时,结果位上的数字为1;否则结果位上的数字为0。按位异或运算通常用于加密和解密操作。

举例说明:

3 ^ 5 = 6
3 的二进制表示为 0011
5 的二进制表示为 0101
^ 的运算规则:
    0 ^ 0 = 0
    0 ^ 1 = 1
    1 ^ 0 = 1
    1 ^ 1 = 0
因此,3 ^ 5 = 0011 ^ 0101 = 0110 = 6
按位异或的用途

按位异或运算主要用于加密和解密操作。通过按位异或运算可以将某些二进制位取反,达到加密或解密的目的。

例如:

  • 加密字符串:s1 ^ s2
  • 取消加密:s1 ^ s2 ^ s2 = s1
按位异或的实例

以下是按位异或运算的几个实例:

public class BitwiseXorExample {
    public static void main(String[] args) {
        int a = 63; // 二进制:0011 1111
        int b = 15; // 二进制:0000 1111
        int c = a ^ b; // 二进制:0011 0000
        System.out.println("a ^ b = " + c); // 输出:a ^ b = 48

        String s1 = "hello";
        String s2 = "world";
        String encrypted = encrypt(s1, s2);
        String decrypted = encrypt(encrypted, s2);
        System.out.println(encrypted); // 输出:|¥?¤?
        System.out.println(decrypted); // 输出:hello

        int x = 10; // 二进制:1010
        int y = 5;  // 二进制:0101
        x ^= y;     // x = 1010 ^ 0101 = 1111
        y ^= x;     // y = 0101 ^ 1111 = 1010
        x ^= y;     // x = 1111 ^ 1010 = 0101
        System.out.println(x + " " + y); // 输出:5 10
    }

    private static String encrypt(String s1, String s2) {
        char[] chars1 = s1.toCharArray();
        char[] chars2 = s2.toCharArray();
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < chars1.length; i++) {
            builder.append((char) (chars1[i] ^ chars2[i % chars2.length]));
        }
        return builder.toString();
    }
}

按位取反(~)

按位取反的原理

按位取反运算是指一个二进制数的每个位取反,即0变成1,1变成0。

举例说明:

~3 = -4
3 的二进制表示为 0011
~ 的运算规则:
    ~0 = -1
    ~1 = -2
    ~2 = -3
    ~3 = -4
因此,~3 = 1100 = -4
按位取反的用途

按位取反运算主要用于一些特殊的应用场景,如在某些加密算法中,通过连续两次按位取反操作可以恢复原值。

按位取反的实例

以下是按位取反运算的几个实例:

public class BitwiseNotExample {
    public static void main(String[] args) {
        int a = 63; // 二进制:0011 1111
        int b = ~a; // 二进制:1100 0000
        System.out.println("~a = " + b); // 输出:~a = -64

        int x = 10; // 二进制:1010
        int y = ~x; // 二进制:0101
        System.out.println(y); // 输出:-11

        int num = 10; // 二进制:1010
        num = (~num) & 0xFF; // 二进制:0101
        System.out.println(num); // 输出:5
    }
}

Java位运算高级操作 == 位移运算

左移运算(<<)

运算规则:实际操作的是位,可以理解为左移几位就相当于乘以 2 的几次方
左移运算的原理

左移运算是将一个数的二进制位向左移动指定的位数,因此在左移运算中,左移操作数的值相当于乘以 22 的左移位数次幂。左移操作数的数据类型可以是 int、long 和任意基本数据类型的 char、short、byte。

举例说明:

3 << 2 = 12  // 3 * 2的2次方 3 * 4 = 12
3 的二进制表示为 0011
3 << 2 的运算过程如下:
    0011 -> 1100
因此,3 << 2 = 12
左移运算的用途

左移运算主要用于实现乘法运算,通过将乘数左移运算后得到结果。

例如,对于 a * 4,可以使用 a << 2 来代替。

左移运算的实例

以下是左移运算的几个实例:

public class LeftShiftExample {
    public static void main(String[] args) {
        int a = 3; // 二进制:0011
        int b = a << 2; // 二进制:1100
        System.out.println("a << 2 = " + b); // 输出:a << 2 = 12

        int c = 10; // 二进制:1010
        int d = c << 1; // 二进制:10100
        System.out.println("c << 1 = " + d); // 输出:c << 1 = 20

        int e = 5; // 二进制:0101
        int f = e << 3; // 二进制:0101000
        System.out.println("e << 3 = " + f); // 输出:e << 3 = 40
    }
}

右移运算(>>)

运算规则:实际操作的是位,可以理解为除以 2 的 n 次幂,如果不能整除,向下取整

右移运算的原理

右移运算是将一个数的二进制位向右移动指定的位数,其特点是符号位不变,空出来的高位补上原符号位的值。右移操作数的数据类型可以是 int、long 和任意基本数据类型的 char、short、byte。

举例说明:

-6 >> 1 = -3
-6 的二进制表示为 1111 1111 1111 1111 1111 1111 1111 1010
-6 >> 1 的运算过程如下:
    1111 1111 1111 1111 1111 1111 1111 1010 -> 1111 1111 1111 1111 1111 1111 1111 1101
因此,-6 >> 1 = -3

注意:对于正数而言,右移运算和无符号右移运算结果相同。

右移运算的用途

右移运算主要用于实现除法运算,通过将被除数右移运算后得到结果。

例如,对于 a / 2,可以使用 a >> 1 来代替。

右移运算的实例

以下是右移运算的几个实例:

public class RightShiftExample {
    public static void main(String[] args) {
        int a = -6; // 二进制:1111 1111 1111 1111 1111 1111 1111 1010
        int b = a >> 1; // 二进制:1111 1111 1111 1111 1111 1111 1111 1101
        System.out.println("a >> 1 = " + b); // 输出:a >> 1 = -3

        int c = 10; // 二进制:1010
        int d = c >> 1; // 二进制:0101
        System.out.println("c >> 1 = " + d); // 输出:c >> 1 = 5

        int e = 5; // 二进制:0101
        int f = e >> 2; // 二进制:0001
        System.out.println("e >> 2 = " + f); // 输出:e >> 2 = 1
    }
}

无符号右移运算(>>>)

无符号右移运算的原理

无符号右移运算是将一个数的二进制位向右移动指定的位数,其特点是空出来的高位用0补充。右移操作数的数据类型可以是 int、long 和任意基本数据类型的 char、short、byte。

举例说明:

-6 >>> 1 = 2147483645
-6 的二进制表示为 1111 1111 1111 1111 1111 1111 1111 1010
-6 >>> 1 的运算过程如下:
    1111 1111 1111 1111 1111 1111 1111 1010 -> 0111 1111 1111 1111 1111 1111 1111 1101
因此,-6 >>> 1 = 2147483645

注意:对于正数而言,无符号右移运算和右移运算结果相同。

无符号右移运算的用途

无符号右移运算主要用于将有符号数转换为无符号数,或者将无符号数右移运算后得到结果。

无符号右移运算的实例

以下是无符号右移运算的几个实例:

public class UnsignedRightShiftExample {
    public static void main(String[] args) {
        int a = -6; // 二进制:1111 1111 1111 1111 1111 1111 1111 1010
        int b = a >>> 1; // 二进制:0111 1111 1111 1111 1111 1111 1111 1101
        System.out.println("a >>> 1 = " + b); // 输出:a >>> 1 = 2147483645
        int c = 10; // 二进制:1010
        int d = c >>> 1; // 二进制:0101
        System.out.println("c >>> 1 = " + d); // 输出:c >>> 1 = 5

        int e = -5; // 二进制:1111 1111 1111 1111 1111 1111 1111 1011
        int f = e >>> 2; // 二进制:0011 1111 1111 1111 1111 1111 1111 1110
        System.out.println("e >>> 2 = " + f); // 输出:e >>> 2 = 1073741822
    }
}

Callable

和Runnable是一样的,只是Runnable没有返回值,而Callable可以有返回值并且可以抛出异常,方法不同run() & call();

代码实例:

package com.laity.callable;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.callable.CallableTest
 * @Date: 2023年11月26日 03:13
 * @Description: Callable使用
 */

public class CallableTest {
    public static void main(String[] args) {
        /*
        内部类实现Callable接口
        new Thread(new Runnable() {}).start();
        new Thread(new FutureTask<V>() {}).start();
        new Thread(new FutureTask<V>( Callable<V> ) {}).start();

        实现原理就是:futureTask传入callable对象,在run方法中调用callable的call方法,将结果返回给futureTask
         */
        try {
            // 1. 创建Callable对象
            Callable1 callable1 = new Callable1();
            // 2. 创建FutureTask对象 - 适配类
            FutureTask<Integer> futureTask = new FutureTask<>(callable1);
            // 3. 创建线程
            Thread thread = new Thread(futureTask, "Callable1");
            // 4. 启动线程
            thread.start();
            // 5. 获取结果
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


/**
 * @author Laity
 */
class Callable1 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable1" + "==>" + Thread.currentThread().getName());
        return 1 << 4;
    }
}

细节点:

  • 是具有缓存的;
  • 结果可能会需要等待,阻塞;

JUC常用的辅助类(必会)

CountDownLatch

减法计数器

代码案例:

package com.laity.add;

import java.util.concurrent.CountDownLatch;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.add.CountDownLatchDemo
 * @Date: 2023年11月26日 22:49
 * @Description: 计数器
 */

public class CountDownLatchDemo {

    public static void main(String[] args) {
        // 总数为6,必须要执行任务的时候,再使用
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                countDownLatch.countDown(); // 计数器减1
                System.out.println(Thread.currentThread().getName() + "走了");
            }, "thread-" + i).start();
        }
        try {
            countDownLatch.await();
            System.out.println("所有线程都走了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

原理:

  • countDownLatch.countDown(); // 计数器减1
  • countDownLatch.await(); // 等待计数器归零,然后再向下执行

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

CyclicBarrier

加法计数器

代码实例:

package com.laity.add;

import java.util.concurrent.CyclicBarrier;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.add.CyclicBarrierDemo
 * @Date: 2023年11月26日 22:58
 * @Description: CyclicBarrier 加法计数器
 */

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });
        /*
        集齐七颗龙珠,召唤神龙
         */
        for (int i = 1; i <= 7; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 收集到" + index + "颗龙珠");
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i).start();
        }
    }
}
Thread-1 收集到1颗龙珠
Thread-2 收集到2颗龙珠
Thread-3 收集到3颗龙珠
Thread-4 收集到4颗龙珠
Thread-7 收集到7颗龙珠
Thread-6 收集到6颗龙珠
Thread-5 收集到5颗龙珠
召唤神龙

Semaphore

一个计数器的信号量,相当于通行证

package com.laity.add;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.add.SemaphoreDemo
 * @Date: 2023年11月26日 23:08
 * @Description: Semaphore 信号量
 */

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 线程池大小 数量 - 一次性只能进入3个线程
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            // semaphore.acquire(); 获取信号量
            // semaphore.release(); 释放信号量
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 准备");
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 进入");
                    TimeUnit.SECONDS.sleep(1);
                    // Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

原理:

// semaphore.acquire(); 获取信号量,如果已经满了,等待释放
// semaphore.release(); 释放信号量,会将当前信号量释放,然后唤醒等待的线程。

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

读写锁 ReadWriteLock

img

img

未加锁

package com.laity.readWriteLock;

import java.util.HashMap;
import java.util.Map;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.readWriteLock.ReadWriteLockDemo
 * @Date: 2023年11月26日 23:28
 * @Description: ReadWriteLock 读写锁
 */

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        Cache cache = new Cache();
        for (int i = 1; i < 5; i++) {
            final int index = i;
            new Thread(() -> {
                System.out.println(cache.get("key"));
            }, "读线程" + i).start();
            new Thread(() -> {
                cache.put("key", "value" + index);
            }, "写线程" + i).start();
        }
    }
}

// 自定义未加锁的一个缓存
class Cache {
    private final Map<String, Object> cache = new HashMap<>();

    // 读方法
    public Object get(String key) {
        System.out.println(Thread.currentThread().getName() + " 正在读取数据");
        Object o = cache.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取数据完毕");
        return o;
    }

    // 写方法
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + " 正在写入数据");
        cache.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写入数据完毕");
    }
}

未加锁结果

D:\Java\JDK\bin\java.exe "-javaagent:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\lib\idea_rt.jar=54249:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\bin" -Dfile.encoding=UTF-8 -classpath D:\java\JDK\jre\lib\charsets.jar;D:\java\JDK\jre\lib\deploy.jar;D:\java\JDK\jre\lib\ext\access-bridge-64.jar;D:\java\JDK\jre\lib\ext\cldrdata.jar;D:\java\JDK\jre\lib\ext\dnsns.jar;D:\java\JDK\jre\lib\ext\jaccess.jar;D:\java\JDK\jre\lib\ext\jfxrt.jar;D:\java\JDK\jre\lib\ext\localedata.jar;D:\java\JDK\jre\lib\ext\mysql-connector-java-5.1.48.jar;D:\java\JDK\jre\lib\ext\nashorn.jar;D:\java\JDK\jre\lib\ext\sunec.jar;D:\java\JDK\jre\lib\ext\sunjce_provider.jar;D:\java\JDK\jre\lib\ext\sunmscapi.jar;D:\java\JDK\jre\lib\ext\sunpkcs11.jar;D:\java\JDK\jre\lib\ext\zipfs.jar;D:\java\JDK\jre\lib\javaws.jar;D:\java\JDK\jre\lib\jce.jar;D:\java\JDK\jre\lib\jfr.jar;D:\java\JDK\jre\lib\jfxswt.jar;D:\java\JDK\jre\lib\jsse.jar;D:\java\JDK\jre\lib\management-agent.jar;D:\java\JDK\jre\lib\plugin.jar;D:\java\JDK\jre\lib\resources.jar;D:\java\JDK\jre\lib\rt.jar;D:\LaityWork\architecture\foodie-dev\target\classes;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter\2.1.5.RELEASE\spring-boot-starter-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot\2.1.5.RELEASE\spring-boot-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-context\5.1.7.RELEASE\spring-context-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-autoconfigure\2.1.5.RELEASE\spring-boot-autoconfigure-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-logging\2.1.5.RELEASE\spring-boot-starter-logging-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\jul-to-slf4j\1.7.26\jul-to-slf4j-1.7.26.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-core\5.1.7.RELEASE\spring-core-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jcl\5.1.7.RELEASE\spring-jcl-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-web\2.1.5.RELEASE\spring-boot-starter-web-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-json\2.1.5.RELEASE\spring-boot-starter-json-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-databind\2.9.8\jackson-databind-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-core\2.9.8\jackson-core-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.8\jackson-datatype-jdk8-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.8\jackson-datatype-jsr310-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.8\jackson-module-parameter-names-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-tomcat\2.1.5.RELEASE\spring-boot-starter-tomcat-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-core\9.0.19\tomcat-embed-core-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-el\9.0.19\tomcat-embed-el-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.19\tomcat-embed-websocket-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\hibernate\validator\hibernate-validator\6.0.16.Final\hibernate-validator-6.0.16.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-web\5.1.7.RELEASE\spring-web-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-beans\5.1.7.RELEASE\spring-beans-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-webmvc\5.1.7.RELEASE\spring-webmvc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-expression\5.1.7.RELEASE\spring-expression-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-aop\2.1.5.RELEASE\spring-boot-starter-aop-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-aop\5.1.7.RELEASE\spring-aop-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\aspectj\aspectjweaver\1.9.4\aspectjweaver-1.9.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-configuration-processor\2.1.5.RELEASE\spring-boot-configuration-processor-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\projectlombok\lombok\1.18.10\lombok-1.18.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.1.0\mybatis-spring-boot-starter-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-jdbc\2.1.5.RELEASE\spring-boot-starter-jdbc-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\zaxxer\HikariCP\3.2.0\HikariCP-3.2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jdbc\5.1.7.RELEASE\spring-jdbc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-tx\5.1.7.RELEASE\spring-tx-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.1.0\mybatis-spring-boot-autoconfigure-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis-spring\2.0.2\mybatis-spring-2.0.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-starter\2.1.5\mapper-spring-boot-starter-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-core\1.1.5\mapper-core-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\persistence\persistence-api\1.0\persistence-api-1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-base\1.1.5\mapper-base-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-weekend\1.1.5\mapper-weekend-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring\1.1.5\mapper-spring-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-extra\1.1.5\mapper-extra-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-autoconfigure\2.1.5\mapper-spring-boot-autoconfigure-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-starter\1.2.12\pagehelper-spring-boot-starter-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-autoconfigure\1.2.12\pagehelper-spring-boot-autoconfigure-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper\5.1.10\pagehelper-5.1.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\jsqlparser\jsqlparser\2.0\jsqlparser-2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-codec\commons-codec\1.11\commons-codec-1.11.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-io\commons-io\2.4\commons-io-2.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger2\2.4.0\springfox-swagger2-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-annotations\1.5.6\swagger-annotations-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-models\1.5.6\swagger-models-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spi\2.4.0\springfox-spi-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-core\2.4.0\springfox-core-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-schema\2.4.0\springfox-schema-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-common\2.4.0\springfox-swagger-common-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spring-web\2.4.0\springfox-spring-web-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\google\guava\guava\18.0\guava-18.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\classmate\1.4.0\classmate-1.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-core\1.2.0.RELEASE\spring-plugin-core-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-metadata\1.2.0.RELEASE\spring-plugin-metadata-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-ui\2.4.0\springfox-swagger-ui-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\xiaoymin\swagger-bootstrap-ui\1.6\swagger-bootstrap-ui-1.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-api\1.7.21\slf4j-api-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-log4j12\1.7.21\slf4j-log4j12-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\log4j\log4j\1.2.17\log4j-1.2.17.jar com.laity.readWriteLock.ReadWriteLockDemo
写线程1 正在写入数据
写线程1 写入数据完毕
读线程1 正在读取数据
写线程2 正在写入数据 -- 这里2开始写入
读线程2 正在读取数据
读线程2 读取数据完毕
读线程1 读取数据完毕
value1
读线程3 正在读取数据
读线程3 读取数据完毕
value2
写线程3 正在写入数据 --2还没有写完,3开始写了
value2
写线程2 写入数据完毕  -- 这里2写入完毕
写线程3 写入数据完毕
读线程4 正在读取数据
写线程4 正在写入数据
读线程4 读取数据完毕
value3
写线程4 写入数据完毕

Process finished with exit code 0

加锁后

package com.laity.readWriteLock;

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

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.readWriteLock.ReadWriteLockDemo
 * @Date: 2023年11月26日 23:28
 * @Description: ReadWriteLock 读写锁
 */
/*
读写锁的特点:
 读 - 读 可以共存并发
 写 - 写 不能共存并发
 读 - 写 不能共存并发
独占锁:一次只能被一个线程占用
共享锁:同时可以被多个线程占用
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
//         Cache cache = new Cache();
        CacheLock cache = new CacheLock();

        for (int i = 1; i < 5; i++) {
            final int index = i;
            new Thread(() -> {
                cache.put("key", "value" + index);
            }, "写线程" + i).start();
            new Thread(() -> {
                System.out.println(cache.get("key"));
            }, "读线程" + i).start();
        }
    }
}

// 自定义未加锁的一个缓存
class Cache {
    private final Map<String, Object> cache = new HashMap<>();

    // 读方法
    public Object get(String key) {
        System.out.println(Thread.currentThread().getName() + " 正在读取数据");
        Object o = cache.get(key);
        System.out.println(Thread.currentThread().getName() + " 读取数据完毕");
        return o;
    }

    // 写方法
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + " 正在写入数据");
        cache.put(key, value);
        System.out.println(Thread.currentThread().getName() + " 写入数据完毕");
    }
}

class CacheLock {
    private final Map<String, Object> cache = new HashMap<>();

    // 读写锁 读写锁可以保证读写操作的顺序 更加细粒度控制
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 这是一个正常的可重入锁 - 普通锁
    // private final Lock lock  = new ReentrantLock();

    // 读方法,读取的时候,可以允许多个线程同时读取
    public Object get(String key) {
        readWriteLock.readLock().lock();
        Object o = null;
        try {
            System.out.println(Thread.currentThread().getName() + " 正在读取数据");
            o = cache.get(key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        System.out.println(Thread.currentThread().getName() + " 读取数据完毕");
        return o;
    }

    // 写方法,写入的时候,只希望同时只有一个线程可以写入
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在写入数据");
            cache.put(key, value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
        System.out.println(Thread.currentThread().getName() + " 写入数据完毕");
    }
}

加锁后结果:

D:\Java\JDK\bin\java.exe "-javaagent:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\lib\idea_rt.jar=55807:E:\IdeaInstall\IntelliJ IDEA 2020.3.4\bin" -Dfile.encoding=UTF-8 -classpath D:\java\JDK\jre\lib\charsets.jar;D:\java\JDK\jre\lib\deploy.jar;D:\java\JDK\jre\lib\ext\access-bridge-64.jar;D:\java\JDK\jre\lib\ext\cldrdata.jar;D:\java\JDK\jre\lib\ext\dnsns.jar;D:\java\JDK\jre\lib\ext\jaccess.jar;D:\java\JDK\jre\lib\ext\jfxrt.jar;D:\java\JDK\jre\lib\ext\localedata.jar;D:\java\JDK\jre\lib\ext\mysql-connector-java-5.1.48.jar;D:\java\JDK\jre\lib\ext\nashorn.jar;D:\java\JDK\jre\lib\ext\sunec.jar;D:\java\JDK\jre\lib\ext\sunjce_provider.jar;D:\java\JDK\jre\lib\ext\sunmscapi.jar;D:\java\JDK\jre\lib\ext\sunpkcs11.jar;D:\java\JDK\jre\lib\ext\zipfs.jar;D:\java\JDK\jre\lib\javaws.jar;D:\java\JDK\jre\lib\jce.jar;D:\java\JDK\jre\lib\jfr.jar;D:\java\JDK\jre\lib\jfxswt.jar;D:\java\JDK\jre\lib\jsse.jar;D:\java\JDK\jre\lib\management-agent.jar;D:\java\JDK\jre\lib\plugin.jar;D:\java\JDK\jre\lib\resources.jar;D:\java\JDK\jre\lib\rt.jar;D:\LaityWork\architecture\foodie-dev\target\classes;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter\2.1.5.RELEASE\spring-boot-starter-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot\2.1.5.RELEASE\spring-boot-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-context\5.1.7.RELEASE\spring-context-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-autoconfigure\2.1.5.RELEASE\spring-boot-autoconfigure-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-logging\2.1.5.RELEASE\spring-boot-starter-logging-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\jul-to-slf4j\1.7.26\jul-to-slf4j-1.7.26.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-core\5.1.7.RELEASE\spring-core-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jcl\5.1.7.RELEASE\spring-jcl-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-web\2.1.5.RELEASE\spring-boot-starter-web-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-json\2.1.5.RELEASE\spring-boot-starter-json-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-databind\2.9.8\jackson-databind-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-core\2.9.8\jackson-core-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.8\jackson-datatype-jdk8-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.8\jackson-datatype-jsr310-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.8\jackson-module-parameter-names-2.9.8.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-tomcat\2.1.5.RELEASE\spring-boot-starter-tomcat-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-core\9.0.19\tomcat-embed-core-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-el\9.0.19\tomcat-embed-el-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.19\tomcat-embed-websocket-9.0.19.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\hibernate\validator\hibernate-validator\6.0.16.Final\hibernate-validator-6.0.16.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-web\5.1.7.RELEASE\spring-web-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-beans\5.1.7.RELEASE\spring-beans-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-webmvc\5.1.7.RELEASE\spring-webmvc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-expression\5.1.7.RELEASE\spring-expression-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-aop\2.1.5.RELEASE\spring-boot-starter-aop-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-aop\5.1.7.RELEASE\spring-aop-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\aspectj\aspectjweaver\1.9.4\aspectjweaver-1.9.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-configuration-processor\2.1.5.RELEASE\spring-boot-configuration-processor-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\projectlombok\lombok\1.18.10\lombok-1.18.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\mysql\mysql-connector-java\5.1.47\mysql-connector-java-5.1.47.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-starter\2.1.0\mybatis-spring-boot-starter-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\boot\spring-boot-starter-jdbc\2.1.5.RELEASE\spring-boot-starter-jdbc-2.1.5.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\zaxxer\HikariCP\3.2.0\HikariCP-3.2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-jdbc\5.1.7.RELEASE\spring-jdbc-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\spring-tx\5.1.7.RELEASE\spring-tx-5.1.7.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\spring\boot\mybatis-spring-boot-autoconfigure\2.1.0\mybatis-spring-boot-autoconfigure-2.1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis\3.5.2\mybatis-3.5.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\mybatis\mybatis-spring\2.0.2\mybatis-spring-2.0.2.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-starter\2.1.5\mapper-spring-boot-starter-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-core\1.1.5\mapper-core-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\javax\persistence\persistence-api\1.0\persistence-api-1.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-base\1.1.5\mapper-base-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-weekend\1.1.5\mapper-weekend-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring\1.1.5\mapper-spring-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-extra\1.1.5\mapper-extra-1.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\tk\mybatis\mapper-spring-boot-autoconfigure\2.1.5\mapper-spring-boot-autoconfigure-2.1.5.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-starter\1.2.12\pagehelper-spring-boot-starter-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper-spring-boot-autoconfigure\1.2.12\pagehelper-spring-boot-autoconfigure-1.2.12.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\pagehelper\pagehelper\5.1.10\pagehelper-5.1.10.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\jsqlparser\jsqlparser\2.0\jsqlparser-2.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-codec\commons-codec\1.11\commons-codec-1.11.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\apache\commons\commons-lang3\3.4\commons-lang3-3.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\commons-io\commons-io\2.4\commons-io-2.4.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger2\2.4.0\springfox-swagger2-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-annotations\1.5.6\swagger-annotations-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\swagger\swagger-models\1.5.6\swagger-models-1.5.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spi\2.4.0\springfox-spi-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-core\2.4.0\springfox-core-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-schema\2.4.0\springfox-schema-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-common\2.4.0\springfox-swagger-common-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-spring-web\2.4.0\springfox-spring-web-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\google\guava\guava\18.0\guava-18.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\fasterxml\classmate\1.4.0\classmate-1.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-core\1.2.0.RELEASE\spring-plugin-core-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\springframework\plugin\spring-plugin-metadata\1.2.0.RELEASE\spring-plugin-metadata-1.2.0.RELEASE.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\io\springfox\springfox-swagger-ui\2.4.0\springfox-swagger-ui-2.4.0.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\com\github\xiaoymin\swagger-bootstrap-ui\1.6\swagger-bootstrap-ui-1.6.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-api\1.7.21\slf4j-api-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\org\slf4j\slf4j-log4j12\1.7.21\slf4j-log4j12-1.7.21.jar;D:\java\Maven\apache-maven-3.6.3\mvn_resp\log4j\log4j\1.2.17\log4j-1.2.17.jar com.laity.readWriteLock.ReadWriteLockDemo
写线程1 正在写入数据
写线程1 写入数据完毕
读线程1 正在读取数据
读线程1 读取数据完毕
value1
读线程2 正在读取数据
读线程2 读取数据完毕
value1
写线程2 正在写入数据
写线程2 写入数据完毕
写线程4 正在写入数据
写线程4 写入数据完毕
写线程3 正在写入数据
写线程3 写入数据完毕
读线程4 正在读取数据
读线程3 正在读取数据
读线程4 读取数据完毕
value3
读线程3 读取数据完毕
value3

Process finished with exit code 0

总结

读写锁能解决哪些问题?

读写锁(ReadWriteLock)是一种并发编程中的锁机制,它能够解决一些特定类型的问题,提高多线程环境下的性能。读写锁允许多个线程同时读取共享资源,但只有一个线程能够写入共享资源。这种机制在读操作远远多于写操作的情况下,可以提供比独占锁更好的性能。

以下是读写锁能够解决的问题:

  1. 读写冲突问题: 当多个线程同时读取一个共享资源时,不会发生冲突。读写锁允许多个线程同时获取读锁,这提高了并发性。
  2. 提高性能: 在读操作远远多于写操作的情况下,使用读写锁能够显著提高性能。因为多个线程可以同时获得读锁,这样可以并发执行读操作,而写操作仍然是互斥的,防止写写冲突。
  3. 写操作的原子性: 读写锁在写操作时是独占的,这确保了写操作的原子性。只有一个线程能够获得写锁,从而避免了写写冲突。
  4. 避免饥饿: 与独占锁相比,使用读写锁可以避免读线程由于等待写线程释放锁而陷入饥饿状态。在读写锁中,读操作不会阻塞其他读操作,因此读线程能够更加灵活地获取锁。
  5. 减少锁竞争: 读写锁通过允许多个线程同时获取读锁,减少了读操作之间的锁竞争,提高了并发度。

需要注意的是,读写锁并不适用于所有场景。它的优势主要体现在读操作远多于写操作的情况下。如果写操作非常频繁,读写锁的性能提升可能会受到限制。在某些情况下,精细的锁设计或其他并发数据结构可能更适合解决特定的并发问题。

但是写操作也只是一瞬间的操作啊,第二个人如果晚几秒执行接口的调用,那不是还是会覆盖吗?

读写锁并不能解决写操作本身的并发问题。它主要是为了解决读-写冲突,允许多个线程同时读取数据,但在写操作时保持独占。如果写操作非常短暂,而且多个写操作在非常短的时间内发生,那么仍然有可能发生竞争条件,导致其中一个写操作的修改被覆盖。

对于写操作短暂的情况,可以考虑使用更细粒度的锁或其他并发控制机制,以确保在写操作间的竞争条件下不会发生数据覆盖。例如,可以使用独占锁(ReentrantLock)或者一些特定的数据结构,如AtomicReference等,来保证写操作的原子性。

总体而言,读写锁在处理读-写冲突时是有帮助的,但对于写操作本身的并发问题,可能需要考虑其他机制来确保数据的一致性。

所以我的理解是:读写锁是解决同一时间同一秒的并发,而不能保障下一秒数据的覆盖

读写锁的主要优势在于对于长时间的读操作是非常有帮助的,因为它允许多个线程同时获取读锁,从而提高了并发性。但是对于短暂的写操作,即使使用读写锁,也不能完全消除写操作的并发问题。如果多个写操作在极短的时间内发生,仍然有可能导致其中一个写操作的修改被另一个写操作覆盖,这被称为"写丢失"。

读写锁是为了解决读-写冲突而设计的,而不是为了解决写操作本身的并发问题。如果对写入的一致性要求很高,可能需要考虑其他机制,比如更细粒度的锁(比如独占锁),或者使用一些并发安全的数据结构(比如AtomicReference)。

总体来说,读写锁适用于读操作远多于写操作的场景,可以提高并发性。但在写操作频繁、且写操作间隔非常短的情况下,还需要额外的考虑和措施以确保数据的一致性。

其他线程在写操作期间无法获得读锁,因此在写操作时,读操作会被阻塞。读写锁是互斥的。

通过这种方式,读写锁在一定程度上平衡了读写操作的并发性能和数据一致性。在读操作远多于写操作的场景中,读写锁能够提供更好的并发性能。

阻塞队列

阻塞

队列

img

BlockingQueue

    // BlockingQueue并列 List和Set Queue:阻塞队列(BlockingQueue)、非阻塞队列(AbstractQueue)、双端队列(Deque)
    // BlockingQueue是一种先进先出(FIFO)的队列,队列满了之后,再往队列中添加元素,会被阻塞,直到队列有空余位置。
    // BlockingQueue的特点:
    // 1. 队列满了之后,再往队列中添加元素,会被阻塞,直到队列有空余位置。
    // 2. 队列空了之后,再往队列中取元素,会被阻塞,直到队列有元素。
    // 3. BlockingQueue提供了四个方法:
    //    put(E e):向队列中添加元素,如果队列满了,则调用此方法会被阻塞,直到队列有空余位置。
    //    take():从队列中取出一个元素,如果队列空了,则调用此方法会被阻塞,直到队列有元素。
    //    poll():从队列中取出一个元素,如果队列空了,则返回null。
    //    peek():从队列中取出一个元素,如果队列空了,则返回null。
    // 4. BlockingQueue下有两个实现类:
    //    ArrayBlockingQueue:基于数组实现的有界阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于LinkedList。
    //    LinkedBlockingQueue:基于链表实现的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于LinkedList。
    // 5. BlockingQueue的应用场景:
    //    生产者消费者模型:生产者线程将产品放入队列,消费者线程从队列中取出产品进行处理。
    //    缓冲区:生产者线程将产品放入队列,消费者线程从队列中取出产品进行处理。
    // 什么时候我们需要使用BlockingQueue呢?
    // 在多线程的时候,A去调用B,B需要调用A,但是A和B是并行的,如果A调用B的时候,B还没有准备好,那么A就会一直等待B准备好,这就是阻塞。

四组API

  • 抛出异常
  • 不会抛出异常,有返回值
  • 阻塞 等待
  • 超时等待
方式抛出异常不会抛出异常,有返回值阻塞 等待超时等待
添加addofferputoffer(有参)
移除removepoll(空参)takepoll(有参)
判断队列首elementpeek--
    /*
    抛出异常
     */
    public static void test1() {
        // capacity 指定了队列的大小,如果队列满了,再往队列中添加元素,会被阻塞,直到队列有空余位置。
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(2);
        System.out.println(blockingQueue.add("a"));  // true
        System.out.println(blockingQueue.add("b"));  // true
        // System.out.println(blockingQueue.add("c"));  // Queue full:报错信息中已经提示了"Queue full",说明队列已满,无法再添加元素。
        System.out.println("=========================================");
        System.out.println(blockingQueue.element()); // 查看队列中第一个元素,如果队列为空,则调用此方法会被阻塞,直到队列有元素。
        System.out.println("=========================================");
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        // System.out.println(blockingQueue.remove());  // java.util.NoSuchElementException 这个报错信息表示在移除一个元素时,队列中没有元素可供移除。
    }
    /*
    不抛出异常,有返回值
     */
    public static void test2() {
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(2);
        System.out.println(blockingQueue.offer("a"));  // true
        System.out.println(blockingQueue.offer("b"));  // true
        System.out.println(blockingQueue.offer("c"));  // false
        System.out.println("=========================================");
        System.out.println(blockingQueue.peek()); // 查看队列中第一个元素,如果队列为空,则返回null。
        System.out.println("=========================================");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll()); // null
    }
    /*
    等待、阻塞 (一直阻塞、超时阻塞)
     */
    public static void test3() {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);
        try {
            // put方法无返回值
            blockingQueue.put("a");
            blockingQueue.put("b");
            // blockingQueue.put("c"); // 队列已满,一直阻塞
            System.out.println("===========================");
            System.out.println(blockingQueue.take());
            System.out.println(blockingQueue.take());
            // System.out.println(blockingQueue.take()); // 队列为空,一直阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /*
    超时等待
     */
    public static void test4() {
        BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);
        try {
            blockingQueue.offer("a");
            blockingQueue.offer("b");
            blockingQueue.offer("c", 2, TimeUnit.SECONDS);
            System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS)); // 等待1秒,如果队列中有元素,则返回队列中的第一个元素,如果1秒内没有元素,则返回null。
            System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS)); // 等待1秒,如果队列中有元素,则返回队列中的第一个元素,如果1秒内没有元素,则返回null。
            System.out.println(blockingQueue.poll(1, TimeUnit.SECONDS)); // 等待1秒,如果队列中有元素,则返回队列中的第一个元素,如果1秒内没有元素,则返回null。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

SynchronousQueue

同步队列

SynchronousQueue是BlockingQueue实现类的一种

img

img

阻塞队列是没有容量的,必须等待取出之后,才能再往里面放一个元素:可以理解为容量为1

package com.laity.bQueue;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.bQueue.SynchronousQueueDemo
 * @Date: 2023年11月29日 00:09
 * @Description: SynchronousQueue 同步队列
 * 和其他的阻塞队列不同的是,SynchronousQueue 是一个没有缓冲区的队列,
 * put()方法会一直阻塞,直到有消费者消费(take)了这个值。
 */

public class SynchronousQueueDemo {
    // SynchronousQueue 同步队列
    public static void main(String[] args) {
        test1();
    }

    public static void test1() {
        // 同步队列
        BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
        // 生产者
        new Thread(() -> {
            try {
                // 存值
                System.out.println(Thread.currentThread().getName() + " 生产PUT 1");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " 生产PUT 2");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " 生产PUT 3");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者").start();

        new Thread(() -> {
            try {
                // 取值
                TimeUnit.SECONDS.sleep(2);
                System.out.println(synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(synchronousQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者").start();
    }
}

多线程

Java多线程&&线程池

线程池必会:三大方法、七大参数、四种拒绝策略

三大方法:

package com.laity.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.pool.Demo01
 * @Date: 2023年12月01日 10:54
 * @Description: 线程池
 * 使用了线程池之后,使用线程池来创建线程,可以避免频繁创建和销毁线程,提高性能
 */

public class Demo01 {
    public static void main(String[] args) {
        // Executors 类提供了四种线程池 - 就是一个线程池工具类 三大方法
        // 1. newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
        // 2. newCachedThreadPool() 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
        // 3. newSingleThreadExecutor() 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
        // 4. newScheduledThreadPool(int corePoolSize) 创建一个定长线程池,支持定时及周期性任务执行
        // 5. newWorkStealingPool(int parallelism) 创建一个支持并行执行任务的线程池,可控制线程最大并行数,超出的线程会被阻塞,直到有可用线程为止
        ExecutorService executorService = Executors.newSingleThreadExecutor();// 它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
        ExecutorService threadPool = Executors.newFixedThreadPool(10);// 创建固定大小的线程池
        ExecutorService service = Executors.newCachedThreadPool();// 可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程(可伸缩)

        try {
            for (int i = 0; i < 10; i++) {
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池的关闭
            executorService.shutdown();
            threadPool.shutdown();
            service.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>());
}

// Integer.MAX_VALUE是16进制的数(0x7fffffff;) 转换为10进制就是 2147483647
// 因为最大线程数过大,可能会导致请求堆积而造成OOM:java.lang.OutOfMemoryError
// java.lang.OutOfMemoryError: Java heap space
// Java 堆内存溢出, 此种情况最常见, 一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露, 需要通过内存监控软件查找程序中的泄露代码, 而堆大小可以通过虚拟机参数 - Xms,-Xmx 等修改。
// java.lang.OutOfMemoryError: PermGen space
// Java 永久代溢出, 即方法区溢出了, 一般出现于大量 Class 或者 jsp 页面, 或者采用 cglib 等反射机制的情况, 因为上述情况会产生大量的 Class 信息存储于方法区。当出现此种情况时可以通过更改方法区的大小来解决, 使用类似 - XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。注意, 过多的常量尤其是字符串也会导致方法区溢出。
// java.lang.StackOverflowError
// 不会抛 OOM error, 但也是比较常见的 Java 内存溢出。JAVA 虚拟机栈溢出, 一般是由于程序中存在死循环或者深度递归调用造成的, 栈大小设置太小也会出现此种溢出。可以通过虚拟机参数 - Xss 来设置栈的大小。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

七大参数:

package com.laity.gulimall.search.thread;

import java.util.concurrent.*;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.gulimall.search.thread.ThreadTest
 * @Date: 2022年10月20日 18:13
 * @Description: 线程回顾
 */
public class ThreadTest {
    // 给线程池直接提交任务 - 保证当前系统中只有一两个,每个异步任务交给线程池
    public static ExecutorService service = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 1、继承Thread
         * 2、实现Runnable接口
         * 3、实现Callable接口 + FutureTask(可以拿到返回值,可以处理异常) jdk1.5
         * 4、线程池
         *      - 为什么用线程池 1、2、3,太浪费资源,尤其是高并发系统;
         *      - 将所有的多线程异步任务都交给线程池执行。
         *      - 降低资源的消耗
         *      - 提高响应速度
         *      - 提高线程的可管理性
         *    1、使用:
         *      1)、Executors 工具类
         *          public static ExecutorService service = Executors.newFixedThreadPool(10);
         *          service.execute(new Runnable01());
         *      2)、new ThreadPoolExecutor(); 原生
         * 区别:
         *      1、2没有返回值
         *      1、2、3不能控制资源;4可以,性能稳定。
         */
        Thread01 thread01 = new Thread01();
        thread01.start(); // 启动线程
        Runnable01 runnable01 = new Runnable01();
        new Thread(runnable01).start();
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
        new Thread(futureTask).start();
        // 等待整个线程执行完成,获取的返回结果
        Integer integer = futureTask.get();
        System.out.println("Callable返回结果" + integer);
        // 给线程池直接提交任务 - 保证当前系统中只有一两个,每个异步任务交给线程池
        // service.submit()
        // service.execute();
        service.execute(new Runnable01());
        // 原生创建线程池
        /**
         * 七大参数
         *  int corePoolSize,    核心线程数量;线程池,创建好以后就准备就绪的线程数量,就等待来接收异步任务来执行
         *  int maximumPoolSize, 最大线程数量;控制资源
         *  long keepAliveTime,  存活时间;如果当前正在运行的线程数量大于核心线程数量。
         *                       释放空闲的线程资源(maximumPoolSize-corePoolSize),只要线程空闲大于指定的keepAliveTime;
         *  TimeUnit unit,       时间单位
         *  BlockingQueue<Runnable> workQueue,  阻塞队列;如果任务有很多, 大于maximumPoolSize的任务就会放到队列里面。
         *                                      只要有线程空闲,就会去队列里面抽取出新的任务继续执行。
         *                                      new LinkedBlockingQueue<>(); 默认是Integer的最大值
         *  ThreadFactory threadFactory,        线程的创建工厂。
         *  RejectedExecutionHandler handler    如果队列满了,按照我们指定的拒绝策略拒绝执行任务。
         *
         *  工作顺序:
         *  运行流程:
         *      1、线程池创建,准备好 core 数量的核心线程,准备接受任务
         *      2、新的任务进来,用 core 准备好的空闲线程执行。
         *          (1) 、core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队列获取任务执行
         *          (2) 、阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量
         *          (3) 、max 都执行好了。Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁。最终保持到 core 大小
         *          (4) 、如果线程数开到了 max 的数量,还有新任务进来,就会使用 reject 指定的拒绝策略进行处理
         *      3、所有的线程创建都是由指定的 factory 创建的。
         */
        // Executors.defaultThreadFactory() 默认的线程工厂
        // new ThreadPoolExecutor.AbortPolicy() 丢弃拒绝策略 可自行查看源码,根据业务需求自行选用。
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 200,
                10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10000),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        executor.execute(new Runnable01());
        // Executors工具类可以帮我们创建的几种常见线程池
        // Executors.newCachedThreadPool() // 核心线程为0,所有线程都可回收;创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
        // Executors.newFixedThreadPool()  // 固定大小,core=max,都不可回收;创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
        // Executors.newScheduledThreadPool() // 定时任务线程池;创建一个定长线程池,支持定时及周期性任务执行。
        // Executors.newSingleThreadExecutor() // 单线程的线程池,后台从队列中获取任务顺序逐一执行;创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

    }

    public static class Thread01 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread当前线程" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println(i);
        }
    }

    public static class Runnable01 implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable当前线程" + Thread.currentThread().getId());
            int i = 12 / 2;
            System.out.println(i);
        }
    }

    public static class Callable01 implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("Callable当前线程" + Thread.currentThread().getId());
            return 14 / 2;
        }
    }
}

img

    public ThreadPoolExecutor(int corePoolSize, // 核心线程数大小
                              int maximumPoolSize, // 最大线程数大小
                              long keepAliveTime, // 线程存活时间(超时了没有人去调用就会释放)
                              TimeUnit unit, // 时间单位
                              BlockingQueue<Runnable> workQueue, // 阻塞队列
                              ThreadFactory threadFactory, // 线程工厂(创建线程使用),一般不用动
                              RejectedExecutionHandler handler // 拒绝策略
                             ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

四种拒绝策略:

img

new ThreadPoolExecutor.AbortPolicy() 丢弃拒绝策略,抛出异常;可自行查看源码,根据业务需求自行选用。
new ThreadPoolExecutor.CallerRunsPolicy() 直接运行策略,不做任何处理,直接在调用者线程中运行 main线程。
new ThreadPoolExecutor.DiscardPolicy() 丢弃策略,不做任何处理,直接丢弃任务。
new ThreadPoolExecutor.DiscardOldestPolicy() 丢弃最旧的任务,即队列中最旧的任务被丢弃,然后重新执行最新的任务。
// 使用线程池的最大线程数量为 = 最大线程数 + 阻塞queue的数量
// 当达到最大线程数量还有人访问就会执行拒绝策略 抛出异常
throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());

线程池的好处:

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

开发手册

img

// Integer.MAX_VALUE是16进制的数(0x7fffffff;) 转换为10进制就是 2147483647
// 因为最大线程数过大,可能会导致请求堆积而造成OOM:java.lang.OutOfMemoryError
// java.lang.OutOfMemoryError: Java heap space
// Java 堆内存溢出, 此种情况最常见, 一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露, 需要通过内存监控软件查找程序中的泄露代码, 而堆大小可以通过虚拟机参数 - Xms,-Xmx 等修改。
// java.lang.OutOfMemoryError: PermGen space
// Java 永久代溢出, 即方法区溢出了, 一般出现于大量 Class 或者 jsp 页面, 或者采用 cglib 等反射机制的情况, 因为上述情况会产生大量的 Class 信息存储于方法区。当出现此种情况时可以通过更改方法区的大小来解决, 使用类似 - XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。注意, 过多的常量尤其是字符串也会导致方法区溢出。
// java.lang.StackOverflowError
// 不会抛 OOM error, 但也是比较常见的 Java 内存溢出。JAVA 虚拟机栈溢出, 一般是由于程序中存在死循环或者深度递归调用造成的, 栈大小设置太小也会出现此种溢出。可以通过虚拟机参数 - Xss 来设置栈的大小。

CPU密集型和IO密集型

最大线程 maximumPoolSize 到底如何去定义?根据什么去定义?

CPI密集型

可以通过任务管理器来查看自己本机的电脑核数(虚拟处理器):几核数就将线程数量定义为几,可以保证CPU的效率最高

    // CPU密集型任务: 几核数就将线程数量定义为几,可以保证CPU的效率最高
    public static void main(String[] args) {
        // 获取CPU的核心数 - 放入线程池中
        int i = Runtime.getRuntime().availableProcessors();
        System.out.println(i);
    }

IO密集型

IO密集型:判断你程序中十分消耗资源(IO)的线程

四大函数式接口

函数式接口

Lambda表达式

链式编程

stream流式计算

函数式接口

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

可以简化编程模型,在新版本的框架中底层大量使用

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

img

Function函数型接口代码测试

img

package com.laity.function;

import java.util.function.Function;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.function.Demo01
 * @Date: 2023年12月01日 15:16
 * @Description: Function 函数型接口:接收一个参数,返回一个结果
 * Function<T, R> 泛型:T 表示输入的参数类型,R 表示输出的结果类型
 * 只要是函数式接口,都可以用 Lambda 表达式来简化实现
 */

public class Demo01 {
    public static void main(String[] args) {
        // 定义一个函数
        Function<Integer, Integer> function = (x) -> x + 1;
        System.out.println(function.apply(10));
        System.out.println(function.apply(20));
    }
}

Predicate断定型接口代码测试

img

package com.laity.function;

import java.util.function.Predicate;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.function.Demo02
 * @Date: 2023年12月01日 15:22
 * @Description: Predicate 函数式接口
 * Predicate 接口中有一个方法 test(T t),返回一个布尔值。
 */

public class Demo02 {
    public static void main(String[] args) {
        // Predicate 接口中有一个方法 test(T t),返回一个布尔值。
        Predicate<Integer> predicate = new Predicate<Integer>() {
            @Override
            public boolean test(Integer integer) {
                return integer > 10;
            }
        };

        System.out.println(predicate.test(9));
        // 使用Lambda表达式
        Predicate<Integer> pre = (x) -> x > 10;
        System.out.println(pre.test(11));
    }
}

Consumer消费型接口代码测试

img

package com.laity.function;

import java.util.function.Consumer;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.function.Demo03
 * @Date: 2023年12月01日 15:29
 * @Description: Consumer 函数式接口
 * Consumer 接口定义了一个 accept 方法,该方法接收一个参数,并不返回任何结果。
 */

public class Demo03 {
    public static void main(String[] args) {
        Consumer<String> consumer = (String s) -> {
            System.out.println(s);
        };
        consumer.accept("Hello World");
        // 顾名思义并不去生成,只获取
    }
}

Supplier供给型接口代码测试

img

package com.laity.function;

import java.util.function.Supplier;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.function.Demo04
 * @Date: 2023年12月01日 15:32
 * @Description: Supplier 函数式接口
 * Supplier 接口中有一个 get() 方法,该方法没有参数,返回一个 T 类型的值。
 */

public class Demo04 {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "Hello World";
        System.out.println(supplier.get());
    }
}

Stream流计算

package com.laity.stream;

import java.util.Comparator;
import java.util.stream.Stream;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.stream.Test
 * @Date: 2023年12月04日 09:44
 * @Description: Stream 测试类
 * 题目要求:一分钟内完成此题,只能用一行代码实现
 * 现在有五个用户,筛选
 * 1.Id必须是偶数
 * 2.年龄必须大于23
 * 3.用户名转为大写
 * 4.用户名字母倒着排序
 * 5.只输出一个用户
 */

public class Test {
    public static void main(String[] args) {
        User user1 = new User(1, "l", 17);
        User user2 = new User(2, "la", 25);
        User user3 = new User(3, "laity", 26);
        User user4 = new User(4, "laitylaity", 27);
        User user5 = new User(5, "laitylaitylaity", 28);
        // Stream流 + 链式操作 + 函数式编程 + lambda表达式
        Stream.of(user1, user2, user3, user4, user5).filter(user -> user.getId() % 2 == 0)
                .filter(user -> user.getAge() > 23)
                .map(user -> user.getName().toUpperCase())
                .sorted(Comparator.reverseOrder()) // (u1,u2) -> u2.compareTo(u1);
                .limit(1)
                .forEach(System.out::println);
    }
}

ForkJoin

分支合并

JDK1.7出现的

并行执行任务!提高效率,那么对于什么样的数据量才会有显著的效果呢?在大数据量时。就好比Hadoop(MapReduce)、Spark差不多,都是在大数量面前才会有显著的效率提高效果,分而治之的思想。

ForkJoin特点

  • 工作窃取(假设有A、B两个线程,每个线程有4个任务;此时线程B执行完毕,而A线程执行到第二个任务,那么B线程就会去A线程中一个线程来执行:也可以称之为互相帮助)
  • 双端队列()

ForkJoin操作

  • 逻辑代码
package com.laity.forkjoin;

import java.util.concurrent.RecursiveTask;

import static java.lang.Long.sum;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.forkjoin.ForkJoinDemo
 * @Date: 2023年12月04日 10:31
 * @Description: 基于ForkJoin框架的求和计算
 * 如何使用ForkJoin框架进行计算
 * 1. 继承RecursiveTask
 * 2. 重写compute方法
 * 3. 调用ForkJoinPool.invoke()方法
 * 4. 调用ForkJoinPool.invokeAll()方法
 * 5. 调用ForkJoinPool.invokeAny()方法
 * 6. 调用ForkJoinPool.submit()方法
 * - 通过调用ForkJoinPool来执行任务
 * - 计算任务:调用ForkJoinPool.execute()方法
 */

public class ForkJoinDemo extends RecursiveTask<Long> {

    private static Long start; // 1
    private static Long end;   // 19_0000_0000

    // 临时变量
    private static Long temp = 100000L;

    public ForkJoinDemo(Long start, Long end) {
        ForkJoinDemo.start = start;
        ForkJoinDemo.end = end;
    }

    public static void main(String[] args) {

    }

    public void test() {
        // 1. 继承RecursiveTask
        ForkJoinDemo forkJoinDemo = new ForkJoinDemo(start, end);
        // 2. 重写compute方法
        forkJoinDemo.fork();
        // 3. 调用ForkJoinPool.invoke()方法
        // forkJoinDemo.invoke();
        // 4. 调用ForkJoinPool.invokeAll()方法
        // forkJoinDemo.invokeAll();
        // 5. 调用ForkJoinPool.invokeAny()方法
        // forkJoinDemo.invokeAny();
        // 6. 调用ForkJoinPool.submit()方法
        // forkJoinDemo.submit();
        // 7. 调用ForkJoinPool.execute()方法
        // forkJoinDemo.execute();
    }

    public void ord() {
        // 普通方式
        int sum = 0;
        for (int i = 1; i <= 10_0000_0000; i++) {
            sum += i;
        }
        System.out.println(sum);
    }

    // 计算方法
    @Override
    protected Long compute() {

        if ((Long) (end - start) <= temp) {
            Long sum = 0L;
            for (long i = 1L; i <= end; i++) {
                sum += i;
            }
            System.out.println(sum);
            return sum;
        } else {
            // 使用ForkJoin框架
            // 递归
            long mid = (start + end) / 2; // 中间值
            // 拆分任务
            ForkJoinDemo left = new ForkJoinDemo(start, mid);
            left.fork(); // 拆分任务,把任务压入线程队列(双端队列)
            ForkJoinDemo right = new ForkJoinDemo(mid + 1, end);
            right.fork();
            // 等待子任务
            return sum(left.join(), right.join());
        }
    }
    /*
    人生自信两百年,会当击水三千里
     */
}
  • 执行代码
package com.laity.forkjoin;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.forkjoin.ExecuteDemo
 * @Date: 2023年12月04日 10:58
 * @Description: 模拟执行任务
 * @Version: 1.0
 */

public class ExecuteDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        // 执行任务
        test3();
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start));
    }

    // 普通程序员 491
    public static void test1() {
        long sum = 0L;
        for (long i = 1L; i <= 10_0000_0000L; i++) {
            sum += i;
        }
        System.out.println(sum);
    }

    // 使用ForkJoin
    public static void test2() throws ExecutionException, InterruptedException {
        ForkJoinPool joinPool = new ForkJoinPool();

        // joinPool.execute(new ForkJoinDemo(1L, 10_0000_0000L)); // 同步执行任务;无法获取结果
        ForkJoinTask<Long> task = joinPool.submit(new ForkJoinDemo(0L, 10_0000_0000L));// 异步执行任务
        System.out.println(task.get());
        /*
        125000000250000000
        125000000250000000
        250000000500000000
        耗时:2632
         */
    }

    // Stream流 并行流 - 最佳方案 331
    public static void test3() {
        long reduce = LongStream.rangeClosed(1L, 10_0000_0000L).parallel().reduce(0L, Long::sum);
        System.out.println(reduce);
    }
}

异步回调

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

img

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    ……
}

异步编排Code

package com.laity.gulimall.search.thread;

import java.util.concurrent.*;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Description: 异步
 */
public class CompletableFutureTest {
    public static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture.runAsync(() -> {
            System.out.println("runAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println(i);
        }, executorService);

        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            return 12 / 2;
        }, executorService).whenComplete((t, u) -> {
            // 虽然能够得到异常信息,但是无法修改返回值
            System.out.println("异步任务成功完成了~");
            System.out.println("结果是:" + t);
            System.out.println("异常是:" + u);
        }).exceptionally((e) -> {
            // 能够得到异常信息,同时可以修改返回值
            System.out.println("异常是:" + e);
            return Integer.valueOf(e.toString());
        }).handle((res, thr) -> {
            // 方法执行完成后的处理
            if (res != null) {
                System.out.println("handle");
                return res * 2;  // 结果 * 2
            } else {
                return 0;
            }
        });
        Integer integer = completableFuture.get();
        System.out.println(integer);
        // 线程串行化
        /**
         * thenRunAsync: 不能获取到上一步的执行结果
         * thenAccept: 能接收上一步结果,但是无返回值。
         * thenApplyAsync: 既能接收上一步的执行结构,又有返回值。
         */
        /*thenRunAsync: 不能获取到上一步的执行结果*/
        CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            return 12 / 2;
        }, executorService).thenRunAsync(() -> {
            System.out.println("thenRunAsync线程任务2启动~~");
        }, executorService);
        /*thenAccept: 能接收上一步结果,但是无返回值。*/
        CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            return 12 / 2;
        }, executorService).thenAcceptAsync(res -> {
            System.out.println("thenAcceptAsync任务2启动了~~");
        }, executorService);
        /*thenApplyAsync: 既能接收上一步的执行结构,又有返回值。*/
        CompletableFuture<String> runAsync = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            return 12 / 2;
        }, executorService).thenApplyAsync(res -> {
            System.out.println("thenApplyAsync任务2启动了" + res);
            return res + "";
        }, executorService);
        String s = runAsync.get();
        System.out.println("返回值" + s);

        // 两个任务合并 - 组合合并 两个任务都完成,触发任务
        /**
         * thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值
         * thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值。
         * runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后,处理该任务。
         */
        System.out.println("========================组合合并==================================");
        CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前任务1线程:" + Thread.currentThread().getId());
            return 12 / 2;
        }, executorService);

        CompletableFuture<Integer> future02 = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前任务2线程:" + Thread.currentThread().getId());
            return 14 / 2;
        }, executorService);
        future01.runAfterBothAsync(future02, () -> {
            System.out.println("runAfterBothAsync任务1与任务2执行完毕~");
            System.out.println("runAfterBothAsync任务3执行" + Thread.currentThread().getId());
        }, executorService);
        future01.thenAcceptBothAsync(future02, (f1, f2) -> {
            System.out.println("thenAcceptBothAsync任务1与任务2执行完毕~");
            System.out.println("thenAcceptBothAsync任务3执行" + Thread.currentThread().getId());
            System.out.println("future01:" + f1 + ", future02" + f2);
        }, executorService);
        CompletableFuture<Integer> combineAsync = future01.thenCombineAsync(future02, Integer::sum, executorService);
        Integer sum = combineAsync.get();
        System.out.println("返回结构:" + sum);

        // 两个任务中,任意一个future任务完毕的时候,执行任务。
        /**
         * applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。
         * acceptEither:两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。
         * runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值。
         */
        /*applyToEither*/
        future01.applyToEitherAsync(future02, (f1) -> f1 + 2, executorService);
        /*acceptEither*/
        future01.acceptEitherAsync(future02, (res) -> {
            System.out.println(res);
        }, executorService);
        /*runAfterEither*/
        future01.runAfterEitherAsync(future02, () -> System.out.println("runAfterEither"), executorService);
//        ExecutorCompletionService<String> service = new ExecutorCompletionService<String>(executorService);
//        Future<String> take = service.take();

        // 多任务组合
        /**
         * allOf:等待所有任务完成
         * anyOf:只要有一个任务完成
         */
        System.out.println("==============多任务组合==============");
        CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的图片信息");

            return "hello.jpg";
        }, executorService);

        CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性信息");

            return "黑色 + 521g";
        }, executorService);
        CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的介绍");

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "三星";
        }, executorService);

        // futureImg.get();futureDesc.get();futureAttr.get(); // 阻塞等待太麻烦

        CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureDesc, futureAttr);
        System.out.println("end...");
        allOf.get(); // 等待所有结果完成
    }
//    public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
//        return andTree(cfs, 0, cfs.length - 1);
//    }
}

JMM

 JMM:Java内存模型,是不存在的!是概念、是约定。

关于JMM同步的约定:

  • 线程解锁前:必须把共享变量立刻刷回主存(线程中的工作内存就是CPU中的Cache不是每次刷新到主存中,而是失败的时候刷回到主存);
  • 线程加锁前:必须读取主存中的最新值到工作内存中;
  • 必须加锁与解锁是同一把锁。

线程分为:工作内存主内存

八种操作

由于JVM运行时的实体是线程,每一个线程运行时都会创建一个工作内存,作为线程的私有空间,每个线程操作变量都在自己的工作内存,从主内存copy一份数据到工作内存,在工作内存中进行操作后,再将其刷新回主内存。如果是在多线程的情况下,则会出现线程安全问题。因此JMM规范规定了如下八种操作来完成数据的操作

数据同步八大原子操作

(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态

(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量

(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作

(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

如果一个变量从主内存中复制到工作内存中,就需要按顺序的执行read、load指令,如果是工作内存刷新到主内存则需要按顺序的执行store、write操作。但JMM只保证了顺序执行,并没有保证连续执行。

img

可见性问题

img

Code

package com.laity.tvolatile;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.tvolatile.JMMDEMO
 * @Date: 2023年12月13日 16:12
 * @Description:
 */

public class JmmDemo {

    private static int i = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            while (i == 0) {

            }
        }, "t1").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        i = 1;

        System.out.println("结束标志:" + i);
    }
}

程序会不停止:但是i已经更改为了1;

需要线程t1知道主内存中的值 i 发生了变化(使用volatile:保证可见性)。

package com.laity.tvolatile;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.tvolatile.JMMDEMO
 * @Date: 2023年12月13日 16:12
 * @Description:
 */

public class JmmDemo {

    private static volatile int i = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            while (i == 0) {

            }
        }, "t1").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        i = 1;

        System.out.println("结束标志:" + i);
    }
}

同步规则分析

  1. 不允许线程无原因的将变量从工作内存写回主内存(没有经过任何的assign)。
  2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量(load或者assign)的变量,就是对一个变量进行use和store操作之前,必须先自行load与assign操作。
  3. 一个变量在同一时刻只能被一个线程lock,同一个线程能多次lock,必须对应的多次unlock,lock与unlock必须成对的出现。
  4. 如果对一个变量执行lock操作,将会清除工作内存中此变量的值,在执行引擎使用这个变量时,必须重新load、assign操作。
  5. 如果一个线程对一个变量没有进行lock操作,则不允许unlock,也不允许对其他线程进行unlock。
  6. 对一个变量进行unlock时,必须先将变量刷新到主内存。

可见性、原子性与有序性

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程给打断。

在java中,对基本的数据类型的操作都是原子性的操作,但是要注意的是对于32位系统的操作对于long、double类型的并不是原子性操作(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作)。因为对于32位的操作系统来说,每次读写都是32位,而doubel、long则是64位存储单位。就会导致一个线程操作完前面32位后,另一个线程刚好读到后面的32位,这样一来一个64位被两个线程分别读取。

可见性

可见性指的是当一个共享变量被一个线程修改后,其他线程是否能够立即感知到。对于串行执行的程序是不存在可见性,当一个线程修改了共享变量后,后续的线程都能感知到共享变量的变化,也能读取到最新的值,所以对于串行程序来讲是不存在可见性问题。

对于多线程程序,就不一定了,前面分析过对于共享变量的操作,线程都是将主内存的变量copy到工作内存进行操作后,在赋值到主内存中。这样就会导致,一个线程改了之后还未回写到主内存,其余线程就无法感知到变量的更新,线程之间的工作内存是不可见的。另外指令重排序以及编译器优化也会导致可见性的问题。

有序性

有序性是指对于单线程的代码,我们总是认为程序是按照代码的顺序进行执行,对于单线程的场景这样理解是没有问题,但是在多线程情况下, 程序就会可能发生乱序的情况,编译器编译成机器码指令后,指令可能会被重排序,重排序的指令并不能保证与没有排序前的保持一致。

在java程序中,倘若在本线程内,所有的操作都可视为有序性,在多线程环境下,一个线程观察另外一个线程,都视为无顺序可言。

JMM如何解决可见性、原子性与有序性

原子性问题

除了jvm自身提供的对基本类型的原子性操作以外,可以通过synchronized和Lock实现原子性。synchronized与lock在同一时刻始终只会存在一个线程访问对应的代码块。

可见性问题

volatile关键字保证了可见性。当一个共享变量被volatile修饰时,它会保证共享变量修改的值立即被其他线程可见,即修改的值立即刷新到主内存,当其它线程去需要读取变量时,从主内存中读取。synchronized和Lock也保证了可见性。因为同一时刻只有一个线程能访问同步代码块,所以是能保证可见性。

有序性问题

volatile关键字保证了有序性,synchronized和Lock也保证了有序性(因为同一时刻只允许一个线程访问同步代码块,自然保证了线程之间在同步代码块的有序执行)。

JMM内存模型:每个线程都有自己的工作内存。线程对变量的操作只能在工作内存中进行操作,并且线程之前的工作内存是不可见的。java内粗模型具备一定的先天有序性,happens-before 原则。如果两个操作无法推断出happens-before 原则,则无法保证程序的有序性。虚拟机可以随意的将它们进行排序。

指令重排:即只要程序的最终结果与顺序执行的结果保持一致,则虚拟机就可以进行排序,此过程就叫指令重排序,为啥需要指令重排序?jvm根据处理器特性(cpu多级缓存、多核处理器等)适当的对机器指令进行排序,使机器指令能更符合CPU的执行特性,最大的限度发挥机器性能。

img

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before 原则

只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:

  1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  4. 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  5. 传递性 A先于B ,B先于C 那么A必然先于C
  6. 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  8. 对象终结规则对象的构造函数执行,结束先于finalize()方法

Volatile

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

  • 保证可见性

    • 保证被volatile修饰的共享变量对所有的线程可见的,也就是当一个线程修改一个被volatile修饰的共享变量,能立即刷新到主内存,其他线程能及时感知。(解决可见性问题)
  • 不保证原子性

  • 禁止指令重排

可见性Code演示

Code

package com.laity.tvolatile;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.tvolatile.JMMDEMO
 * @Date: 2023年12月13日 16:12
 * @Description: volatile 可见性的验证
 */

public class JmmDemo {

    private static int i = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            while (i == 0) {

            }
        }, "t1").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        i = 1;

        System.out.println("结束标志:" + i);
    }
}

程序会不停止:但是i已经更改为了1;

需要线程t1知道主内存中的值 i 发生了变化(使用volatile:保证可见性)。

package com.laity.tvolatile;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.tvolatile.JMMDEMO
 * @Date: 2023年12月13日 16:12
 * @Description: volatile 可见性的演示
 */

public class JmmDemo {

    private static volatile int i = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            while (i == 0) {

            }
        }, "t1").start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        i = 1;

        System.out.println("结束标志:" + i);
    }
}

不保证原子性Code演示

什么是原子性?不可分割就是原子性:要么成功要么失败

ACID;删除锁必须保证原子性。使用redis+Lua脚本完成;CAS

线程A在执行任务的时候,不能被打扰,也不能被分割

Code

package com.laity.tvolatile;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.tvolatile.VDemo01
 * @Date: 2023年12月13日 16:28
 * @Description: volatile 不保证原子性
 * 总线嗅探机制:
 * 1. 总线嗅探机制是一种特殊的指令,它会在CPU执行指令时,检查总线上是否有其他CPU正在执行指令,如果有,则CPU会暂停执行,直到其他CPU完成指令。
 */

public class VDemo01 {

    private static volatile int count = 0;

    // synchronized保证原子性
    public static void add() {
        count++;
    }

    public static void main(String[] args) {
        // 理论上,count 应该是 10000,但是实际上可能是 9999,因为有可能有其他线程正在执行 add 方法,导致 count 不是 10000
        for (int i = 0; i < 10000; i++) {
            new Thread(VDemo01::add).start();
        }
        while (Thread.activeCount() > 2) {
            // 让其他线程执行
            // yield是让当前线程让出CPU执行权,让其他线程有机会执行
            Thread.yield();
        }

        // while (count < 10000) { }
        System.out.println(count);
    }

}

如果不加 lock 与 synchronized,怎么保证原子性呢?

img

img

这些类的底层都与操作系统挂钩!在内存中修改值!Unsafe类是一个特殊的存在。

// setup to use Unsafe.compareAndSwapInt for updates

private static final Unsafe unsafe = Unsafe.getUnsafe();

解决不保证原子性问题(不加锁就保证)

public class VDemo01 {

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

    // synchronized保证原子性
    public static void add() {
        count.getAndIncrement();  // AtomicInteger + 1 方法,CAS
    }

    public static void main(String[] args) {
        // 理论上,count 应该是 10000,但是实际上可能是 9999,因为有可能有其他线程正在执行 add 方法,导致 count 不是 10000
        for (int i = 0; i < 10000; i++) {
            new Thread(VDemo01::add).start();
        }
        while (Thread.activeCount() > 2) {
            // 让其他线程执行
            // yield是让当前线程让出CPU执行权,让其他线程有机会执行
            Thread.yield();
        }

        // while (count < 10000) { }
        System.out.println(count);
    }

}

禁止指令重排Code演示

什么是指令重排?你写的程序,计算机执行过程中并不是按照你写的那样执行的。

img

处理器在进行指令重排的时候,是要考虑:数据直接的依赖性!可以看下面的例子:

int i = 1;  // 1
int n = 4;  // 2
i = i + 5;  // 3
n = i * i;  // 4

// 我们所期望的执行顺序是:1 2 3 4 但是 2 1 3 4、1 3 2 4。
// 可不可能是 4 1 2 3;这个是不可能的

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

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

正常结果是: x = 0;y=0;a=2;b=1.但是可能由于指令重排导致成诡异结果:

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

可能由于指令重排导致成诡异结果:x=2;y=1.

而volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)这是一个CPU指令。

内存屏障的作用:

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

img

硬件层的内存屏障

Intel硬件提供了一系列的内存屏障,主要有:

\1. lfence,是一种Load Barrier 读屏障

\2. sfence, 是一种Store Barrier 写屏障

\3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力

\4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:

img

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

public class DoubleCheckLock {
    private volatile static DoubleCheckLock instance;
    
    private DoubleCheckLock(){}
    
    public static DoubleCheckLock getInstance(){
        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new  DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}      

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。

因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);//2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
private volatile static DoubleCheckLock instance;

volatile内存语义的实现

前面提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图

img

上图中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

下图是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图

img

上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例

代码进行说明。

    class VolatileBarrierExample {
        int a;
        volatile int v1 = 1;
        volatile int v2 = 2;

        void readAndWrite() {
            int i = v1;      // 第一个volatile读
            int j = v2;       // 第二个volatile读
            a = i + j;         // 普通写
            v1 = i + 1;       // 第一个volatile写
            v2 = j * 2;       // 第二个 volatile写
        }
    }

volatile 可以保证 数据可见性,不能保证其原子性,由于内存屏障,可以保证避免指令重排的现象。

同时****针对readAndWrite()方法,编译器在生成字节码时也是可以进一步去优化的,具体文章后续会出。

单例设计模式

饿汉式,DCL懒汉式

具体文章可以观看我的另一篇文章:设计模式 - 单例设计模式

深入理解CAS

什么是CAS?

atomicInteger.getAndIncrement(); // ++ 操作; 下图是其源码分析

img

img

img

而图二中有个 compareAndSwapInt 比较和交换:下面我们通过compareAndSet进一步了解它

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.cas.CasDemo
 * @Date: 2023年12月14日 22:25
 * @Description: CAS
 */

public class CasDemo {

    // CAS 是乐观锁的一种实现方式,它允许一个线程在执行时检查另一个线程是否修改过共享变量,
    // 如果没有被修改过,则执行,如果被修改过,则重试,直到成功为止。
    // CAS 包含三个操作数,内存值 V、预估值 A、更新值 B。如果内存值 V 等于预估值 A,则将内存值修改为 B,否则什么都不做。
    // 这三个操作数必须是原子操作,即在一个时刻只能有一个线程对它们进行操作。
    // compareAndSet() 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2023);
        /*
            compareAndSet的源码:
            public final boolean compareAndSet(int expect, int update) {
                return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
            }
         */
        // 如果我期望的值达到2023,那么我就将它修改为2021,如果不满足,就不修改。
        atomicInteger.compareAndSet(2023, 2021);
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2023, 2021));
        atomicInteger.getAndIncrement(); // ++ 操作

    }
}

所以 !this.compareAndSwapInt(var1, var2, var5, var5 + var4) 代码意思就是 var1偏移var2后如果等于var5,那么就 + 1

while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

总结CAS: 比较并交换

  • 比较当前工作内存中的值和主内存的值,如果这个值是期望的那么则执行操作!如果不是就自旋。

  • 缺点:

    • 由于底层是自旋锁,循环会耗时!
    • 一次性只能保证一个共享变量的原子性;
    • 会参数ABA问题。

CAS: ABA问题:狸猫换太子

原子引用

原子引用解决ABA问题

那什么是ABA问题呢?

详解ABA问题

狸猫换太子

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

img

package com.laity.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.cas.AbaDemo
 * @Date: 2023年12月15日 10:26
 * @Description: 复现ABA问题并解决
 */

public class AbaDemo {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2023);


        // ===========================捣乱的线程===========================
        System.out.println(atomicInteger.compareAndSet(2023, 2024));
        System.out.println(atomicInteger.get());

        System.out.println(atomicInteger.compareAndSet(2024, 2023));
        System.out.println(atomicInteger.get());

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

银行案例

假设小琳银行卡有 100 块钱余额,且假定银行转账操作就是一个单纯的 CAS 命令,对比余额旧值是否与当前值相同,如果相同则发生扣减/增加,我们将这个指令用 CAS(origin,expect) 表示。于是,我们看看接下来发生了什么:

  1. 小琳在 ATM 1 转账 100 块钱给小李;
  2. 由于ATM 1 出现了网络拥塞的原因卡住了,这时候小琳跑到旁边的 ATM 2 再次操作转账;
  3. ATM 2 没让小琳失望,执行了 CAS(100,0),很痛快地完成了转账,此时小琳的账户余额为 0;
  4. 小王这时候又给小琳账上转了 100,此时小琳账上余额为 100;
  5. 这时候 ATM 1 网络恢复,继续执行 CAS(100,0),居然执行成功了,小琳账户上余额又变为了 0;
  6. 这时候小王微信跟小琳说转了 100 过去,是否收到呢?小琳去查了下账,摇了摇头,那么问题来了,钱去了哪呢?

关于钱的去向,有一种可能就是小王给小琳的 100 大洋,因为 ATM 1 网络恢复再次被转给了小李,毕竟小琳尝试了两次转账,出现这种情况虽不合理,但情有可原。假设我们作为银行系统设计者和开发者,不接受这种情况存在,那我们就需要着手处理这种 ABA 问题了。

解决ABA问题

引入原子引用:AtomicStampedReference reference = new AtomicStampedReference<>(1, 1);

对应思想:乐观锁!

package com.laity.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.cas.AbaDemo
 * @Date: 2023年12月15日 10:26
 * @Description: 复现 CAS-ABA问题并 解决(乐观锁原理)
 */

public class AbaDemo {

    public static void main(String[] args) {
        // AtomicInteger atomicInteger = new AtomicInteger(2023);
        // 使用原子引用
        // new AtomicReference<>() // 基本的原子引用

        // AtomicStampedReference(V initialRef, int initialStamp)  初始值和初始时间戳
        // 携带时间戳的原子引用 这个时间戳就相当于版本号,每次修改值的时候,时间戳都会加1
        // TODO: Integer使用了对象缓存机制,默认范围是-128到127,超过范围会自动装箱,导致引用不同,导致交换失败
        //  推荐使用静态工厂方法valueOf()获取对象实例,而不是new 因为valueOf()使用缓存,而new一定会创建新的对象分配新的内存空间
        //  new AtomicStampedReference<>(2023, 123) 将2023改小些就不会出现问题了。正常业务操作比较的是一个对象
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(1, 1);
        // 自动装箱导致引用不同,导致交换失败


        new Thread(() -> {
            int stamp = reference.getStamp(); // 获取时间戳
            System.out.println("t1获取时间戳:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(reference.compareAndSet(1, 2, reference.getStamp(), reference.getStamp() + 1));
            System.out.println("t1修改后的时间戳:" + reference.getStamp());

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

            System.out.println("t1最后修改后的时间戳:" + reference.getStamp());
        }, "t1").start();

        new Thread(() -> {
            int stamp = reference.getStamp(); // 获取时间戳
            System.out.println("t2获取时间戳:" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(reference.compareAndSet(1, 3, reference.getStamp(), reference.getStamp() + 1));
            System.out.println("t2值:" + reference.getStamp());
        }, "t2").start();

//        // ===========================捣乱的线程===========================
//        System.out.println(atomicInteger.compareAndSet(2023, 2024));
//        System.out.println(atomicInteger.get());
//
//        System.out.println(atomicInteger.compareAndSet(2024, 2023));
//        System.out.println(atomicInteger.get());
//
//        // ===========================期望的线程===========================
//        System.out.println(atomicInteger.compareAndSet(2023, 2025));
//        System.out.println(atomicInteger.get());
    }
}

各种锁的理解

1.公平锁、非公平锁

  • 公平锁:不可插队
  • 非公平锁:不可插队
ReentrantLock reentrantLock = new ReentrantLock();

// 源码
public ReentrantLock() {
    sync = new NonfairSync();
}
ReentrantLock reentrantLock = new ReentrantLock(true);

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

2.乐观锁、悲观锁

悲观锁

当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发的发生。

为什么叫做悲观锁呢?因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

数据库中的行锁,表锁,读锁,写锁,以及 synchronized 实现的锁均为悲观锁。

顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

img

乐观锁

乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。

乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。

反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

img

如何实现

我们知道悲观锁和乐观锁是用来控制并发下数据的顺序变动问题的。那么我们就模拟一个需要加锁的场景,来看不加锁会出什么问题,并且怎么利用悲观锁和乐观锁去解决。

我们以商品为例,现在 线程A 和线程 B 都想吃红薯,但是红薯数量只有 1 个了。在不加锁的情况下,如果A,B同时下单,就有可能导致超卖。

img

悲观锁

利用悲观锁的解决思路是,我们认为数据修改产生冲突的概率比较大,所以在更新之前,我们显示的对要修改的记录进行加锁,直到自己修改完再释放锁。加锁期间只有自己可以进行读写,其他事务只能读不能写。

此时线程 A 下单前先给红薯这行数据(id=C001)加上悲观锁(行锁)。此时这行数据只能 A 来操作,也就是只有 A 能买。B 想买就必须一直等待。当 A 买好后,B 再想去买的时候会发现库存数量已经为 0,那么 B 看到后就会放弃购买。

那怎么样给这行数据加上悲观锁呢?当然是在select给这行数据加上锁,如下所示:

select num from commodity where id = C001 for update

悲观锁图解:

img

乐观锁

下面我们利用乐观锁来解决该问题。上面乐观锁的介绍中,我们提到了,乐观锁是通过版本号 version 来实现的。所以,我们需要给 commodity 表加上 version 字段。

img

我们认为数据修改产生冲突的概率并不大,多个线程在修改数据的之前先查出版本号,在修改时把当前版本号作为修改条件,只会有一个线程可以修改成功,其他线程则会失败。

A 和 B 同时将红薯(id=C001)的数据查出来,然后 A 先买,A 将 id=C001 和 version=0 作为条件进行数据更新,即将数量 -1,并且将版本号+1。

此时版本号变为 1。A 此时就完成了商品的购买。最后 B 开始买,B 也将 id=C001 和 version=0 作为条件进行数据更新,但是更新完后,发现更新的数据的库存为 0,此时就说明已经有人修改过数据,此时就应该提示用户重新查看最新数据购买。

乐观锁图解:

img

1.版本号机制:

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。就是通过version版本号作为一个标识,标识这个字段所属的数据是否被改变。

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

2.CAS算法:

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

这里推荐篇文章【面试必备之深入理解自旋锁】:https://blog.csdn.net/qq_34337272/article/details/81252853

如何选择

  • 乐观锁适用于读多写少的场景,可以省去频繁加锁、释放锁的开销,提高吞吐量
  • 在写比较多的场景下,乐观锁会因为版本不一致,不断重试更新,产生大量自旋,消耗 CPU,影响性能。这种情况下,适合悲观锁

乐观锁的缺点

1 ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

青春好比吸烟。烟在飞扬。烟灰在坠落。我是Laity,正在前行的Laity。

面试八股文: https://github.com/Snailclimb/JavaGuide

3.可重入锁

  • 拿到外面的锁,就自动拿到里面的锁
  • 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平锁
  • ReetrantLock();Synchronized都是重入锁

注意:ReetrantLock锁的就是ReetrantLock对象,ReetrantLock中有AQS,AQS可以判断两次lock方法是否都是同一个线程。

锁必须配对,否则就会出现死锁。

4.自旋锁

  • spinlock

CAS中的自旋锁

img

package com.laity.lock;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: Laity
 * @Project: JavaLaity
 * @Package: com.laity.lock.SpinlockDemo
 * @Date: 2023年12月15日 17:44
 * @Description: 实现自旋锁
 */

public class SpinlockDemo {

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

    // 创建锁
    public void myLock() {
        Thread thread = Thread.currentThread();

        // 如果我的线程是空,则把我的线程丢进去,设置当前线程为锁
        // 当前线程不为空,则等待
        System.out.println("当前线程:" + thread.getName() + "获取锁");
//        do {
//            // 自旋
//            System.out.println("当前线程:" + thread.getName() + "获取锁");
//        } while (!atomicReference.compareAndSet(null, thread));
        while (!atomicReference.compareAndSet(null, thread)){

        }
    }
    // 释放锁
    public void myUnlock() {
        Thread thread = Thread.currentThread();
        System.out.println("当前线程:" + thread.getName() + "释放锁");
        // 释放锁
        atomicReference.compareAndSet(thread, null);
    }


    public static void main(String[] args) throws InterruptedException {

        // 底层使用的是CAS自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                spinlockDemo.myLock();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                spinlockDemo.myUnlock();
            }, "t1").start();
        }
        Thread.sleep(3000);
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                spinlockDemo.myLock();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                spinlockDemo.myUnlock();
            }, "t2").start();
        }
    }
}

5.死锁

死锁(Deadlock)情况是指:两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。线程竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西

img

概念性的东西读起来可能有点抽象,那我举个比较容易理解的例子:假设有两个线程A 和 B,两把锁 LockA和LockB,线程A持有LockA锁,线程B持有LockB锁 , 在双方不释放锁的情况下,再尝试去获取对方持有的锁,也就是说,线程A 手里拿着LockA锁不放,又去获取LockB锁(此时LockB锁在线程B手里),而线程B手里拿着LockB锁不放,又去获取LockA锁(此时LockA锁在线程A手里),在这种情况下,就会陷入无限等待的死锁状态。

排除死锁

知道了什么是死锁,下一步就是当遇到死锁问题时,如何利用工具去排查和检测死锁问题,下面就介绍两款好用的工具来帮助我们检测死锁。

  • Jstack 工具

    • JDK中自带的 jstack 工具是一个线程堆栈分析工具,可以帮助我们排查Java程序中是否有死锁问题。
    • 可以通过jps查看当前正在执行任务的PID,不做过多介绍。
  • 使用Jconsole 工具

    • Jconsole是JDK自带的监控工具。它用于连接正在运行的本地或者远程的JVM,对正在运行的Java应用程序的资源消耗和性能进行监控,提供强大的可视化界面,本身占用的服务器内存很小,甚至可以说几乎不消耗。
    • jconsole 指令

如何避免产生死锁

其实产生死锁也不是那么容易的,需要满足四个条件,才会产生死锁。

  • 互斥,也就是多个线程在同一时间使用的不是同一个资源。
  • 持有并等待,持有当前的锁,并等待获取另一把锁
  • 不可剥夺,当前持有的锁不会被释放
  • 环路等待,就是两个线程互相尝试获取对方持有的锁,并且当前自己持有的锁不会释放。

我们只需要让其中一个条件不成立,那么就可以避免死锁问题的产生。一般最常见的解决方式就是使用资源有序分配法,来使环路等待条件不成立。

环路等待就是我们刚开始代码演示的那种情况,两个线程互相尝试获取对方的锁,但是他们两个都不会释放自己的锁,这样就会陷入一个无限循环等待,这种情况就是环路等待

资源有序分配法其实很简单,就是**把线程获取资源的顺序调整为一致的即可,**资源可以理解为代码示例中的锁。

那么我们只需要修改线程A或者线程B的代码即可。线程B原本是先获取LockB再获取LockA,改成和线程A一样的获取顺序,先获取LockA,再获取LockB:

总结

  • 简单来说,死锁就是多个线程并行执行的时候,抢夺资源而互相等待造成的。
  • 只有满足四个条件的时候才会发生。
  • 避免死锁问题只需要破坏其中一个条件即可。

谋生的路上不抛弃良知,谋爱的路上不放弃尊严,做一个自尊自爱的人,如此安好,所爱任性,所作随心……我是Laity,正在前进的Laity。

文章来源:https://blog.csdn.net/duyun0/article/details/135054846
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。