1.JUC并发编程

JUC并发编程

1、什么是JUC

学习方式:源码 + 官方文档

JUC 是 java.util.concurrent

面试高频问 JUC

java.util 是 Java 的一个工具包

业务:普通的线程代码 Thread

Runnable:没有返回值、效率相比于Callable相对较低!

2、线程和进程

进程:一个程序,QQ.exe , Music.exe;数据 + 代码 + pcb ;是程序的集合

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

Java默认有几个线程? 2 个线程! main线程、GC线程

线程:开了一个进程Typora,写字,等待几分钟会进行自动保存(线程负责的)

对于 Java 而言:Thread、Runnable、Callable 进行开启线程的,之前。

提问 ?JAVA 真的可以开启线程吗? 开不了的

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

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

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	
	// 这是一个 C++ 底层,Java是没有权限操作底层硬件的
    private native void start0();

Java 是没有权限去开启线程,操作硬件的,这是一个 native 的一个本地方法,它调用的底层的 C++代码。

并发、并行

并发:多线程操作同一个资源。

  • CPU只有一核,模拟出来多条线程。 天下武功,唯快不破。那么我们就可以使用CPU快速交替,来模拟多线程。

**并行:**多个人一起行走。

  • CPU多核,多个线程可以同时执行。我们可以使用线程池!

获取本机cpu 的核数

 public class Test01 {
    public static void main(String[] args) {
        // 获取 cpu 的核数
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

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

线程有几个状态? 6个状态

线程的状态: 6 个状态

public enum State {
        // 新生
        NEW,

        // 运行
        RUNNABLE,

        // 阻塞
        BLOCKED,

        // 等待
        WAITING,

        // 超时等待
        TIMED_WAITING,

        // 终止
        TERMINATED;
    }
    

wait / sleep 的区别

  1. 来自不同的类
    1. wait ==> Object
    2. sleep ==> Thread
  2. 关于锁的释放
    1. wait 会释放锁
    2. sleep 睡着了 ,不会释放锁
  3. 使用的范围是不同的
    1. wait 必须在同步代码块中
    2. sleep 可以在任何地方睡
  4. 是否需要抛出异常
    1. wait 是不需要捕获异常的
    2. sleep 必须捕获异常

3、Lock锁(重点)

传统的 Synchronized

package com.hou.demo;


/**
 * 真正的多线程开发
 * 线程就是一个单独的资源类,没有任何的附属操作
 */
public class SaleTicketDemo01 {
    public static void main(String[] args) {
        // 多线程操作
        // 并发:多线程操作同一个资源类,把资源丢入线程
        Ticket_ ticket = new Ticket_();

        // @FunctionalInterface  函数式接口, jdk1.8 之后, lambda表达式
        new Thread(()->{for (int i = 0; i < 40; i++) {ticket.sale();}},"A").start();
        new Thread(()->{for (int i = 0; i < 40; i++) {ticket.sale();}},"B").start();
        new Thread(()->{for (int i = 0; i < 40; i++) {ticket.sale();}},"C").start();

    }
}


// 资源类
// 属性 + 方法
// oop
class Ticket_{
    private int number = 50;

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

Lock 接口

  • lock() 加锁
  • unlock() 释放锁
  • 所有已知的实现类
    • ReentrantLock 可重入锁
    • ReentrantReadWriteLock.ReadLock 可重入读锁
    • ReentrantReadWriteLock.WriteLock 可重入写锁
public ReentrantLock() {
        sync = new NonfairSync(); // 非公平锁
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
    	                  // 公平           非公平
        sync = fair ? new FairSync() : new NonfairSync();
    }

**公平锁:**十分公平,必须先来后到 1s 1hour

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

package com.hou.demo;

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

public class SaleTicketDemo2 {
    public static void main(String[] args) {
        // 多线程操作
        // 并发:多线程同时操作同一个资源类,把资源类丢入线程
        Ticket2 ticket2 = new Ticket2();
        new Thread(()->{for (int i=0; i<40; i++) ticket2.sale();},"A").start();
        new Thread(()->{for (int i=0; i<40; i++) ticket2.sale();},"B").start();
        new Thread(()->{for (int i=0; i<40; i++) ticket2.sale();},"C").start();
    }
}

// lock 三部曲
// 1.  Lock lock=new ReentrantLock();
// 2.  lock.lock() 加锁
// 3.  finally => 解锁:lock.unlock();
class Ticket2{
    private int number = 50;

    Lock lock = new ReentrantLock();

    // 卖票的方式
    // 使用Lock锁
    public void sale(){
        // 加锁
        lock.lock();

        try {
            // 业务代码
            if (number>=0){
                System.out.println(Thread.currentThread().getName() + "卖出了第:" + number + "张票,剩余:" + number);
                number--;
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
Synchronized 和 Lock 区别
  • 1、Synchronized 是内置的 Java 关键字,Lock是一个 Java 类
  • 2、Synchronized 无法判断获取锁的状态,Lock可以判断
  • 3、Synchronized 会自动释放锁,Lock 必须要手动加锁和手动释放锁!如果不释放锁,死锁
  • 4、Synchronized 线程1(获得锁->阻塞)、线程2(一直等待),Lock就不一定会一直等待下去,Lock会有一个trylock() 方法去尝试获取锁,不会造成长久的等待
  • 5、Synchronized 是可重入锁,不可以中断的,非公平的; Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁
  • 6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的代码同步问题
锁到底是什么? 如何判断锁的是谁?

4、生产者和消费者问题!

Synchronized wait notify 可以实现,该方法是传统方法

Synchronized 版本

package com.hou.pc;

// 生产者消费者问题 Synchronized
public class A {
    public static void main(String[] args) {
        Data data = new Data();

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

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

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

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

//  数字 资源类
class Data{
    private int number = 0;

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

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

使用 if 判断 问题存在,A线程B线程,现在如果有更多个线程执行,就会产生虚假唤醒的问题

解决:将 if 改为 while 即可, 防止虚假唤醒,这样问题就不存在了

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

await、signal 替换 wait、notify

通过Lock找到Condition

package com.hou.pc;

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

public class B {
    public static void main(String[] args) {
        Data2 data2 = new Data2();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data2.increment();
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data2.decrement();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data2.increment();
            }
        },"C").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data2.decrement();
            }
        },"D").start();
    }
}

// 资源类
class Data2{
    private int number = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

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

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

    }

}

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

Condition 的优势:精准的通知和唤醒的线程

如果我们要指定通知的下一个进行顺序怎么办呢?我们可以使用Condition来指定通知进程~

package com.hou.pc;

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

// 生产者消费者问题  使用 Condition 精准唤醒线程
public class C {
    public static void main(String[] args) {
        Data3 data = new Data3();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"C").start();
    }
}

// 资源类
class Data3{
    private int number = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void printA(){
        lock.lock();

        try {
            while (number!=1){
                // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAAA");
            number = 2;
            // 唤醒2
            condition2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();

        try {
            while (number!=2){
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBBBBB");
            number = 3;
            // 唤醒3
            condition3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();

        try {
            while (number!=3){
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>CCCCCCC");
            number = 1;
            // 唤醒1
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

5、8锁现象

如何判断锁的是谁! 锁到底锁的是谁?

锁会锁住:对象,Class

8锁,其实就是关于锁的8个问题

深刻理解我们的锁

  • 问题1
package com.hou.lock8;

import java.util.concurrent.TimeUnit;

/**
 * 8锁,其实就是关于锁的8个问题
 * 1.标准情况下,俩个线程,先打印发短信,还是打电话?
 */
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();

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

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

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

class Phone{
    public synchronized void sendSms(){
        System.out.println("发短信");
    }

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


结果是:先 发短信,然后打电话

  • 问题2 让发短信 延时4s
package com.hou.lock8;

import java.util.concurrent.TimeUnit;

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

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

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

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

class Phone2{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");

    }

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

结果:先 发短信 , 然后打电话

原因:并不是因为顺序执行,是因为synchronized锁的对象是方法的调用,对于俩个方法用的是同一个锁,谁先拿到谁先执行

  • 问题3

在添加一个普通方法,那么先执行哪一个方法呢?

package com.hou.lock8;

import java.util.concurrent.TimeUnit;

public class Test3 {
    public static void main(String[] args) {
        Phone3 phone3 = new Phone3();

        new Thread(()->{phone3.sendSms();},"A").start();
        new Thread(()->{phone3.call();},"A").start();
        new Thread(()->{phone3.hello();},"A").start();
    }
}

class Phone3{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4package com.hou.lock8;

import java.util.concurrent.TimeUnit;

public class Test4 {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(()->{phone1.sendSms();}).start();

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

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

class Phone4{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}
);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

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

    public void hello(){
        System.out.println("hello");
    }
}

结果:先 hello,然后发短信,打电话

原因:hello 是一个普通方法,不受synchronized锁的影响,普通方法是顺序执行的

  • 问题4

如果我们使用的是俩个对象,一个调用发短信,一个调用打电话,那么整个顺序是怎么样的呢?

package com.hou.lock8;

import java.util.concurrent.TimeUnit;

public class Test4 {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(()->{phone1.sendSms();}).start();

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

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

class Phone4{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

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

结果:先打电话,然后发短信

原因:在发短信方法中延时了4s,又因为synchronized锁的是对象,但是我们这使用的是俩个对象,所以每个对象都有一把锁,所以不会造成锁的等待。正常执行

  • 问题5,问题6

我们把synchronized的方法加上static变成静态方法,那么顺序又是怎样的?

(1)先使用一个对象调用俩个方法

结果是:先发短信,然后打电话

(2)使用俩个对象调用俩个方法

结果是:先发短信,然后打电话

原因:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一个锁,

如果静态static方法使用的synchronized锁,那么这个锁是这个class的,不管多少对象,对于class锁都只有一个,谁先拿到这个锁就先执行,其他的进程都需要等待!

  • 问题7

使用一个静态同步方法,一个同步方法,一个对象调用顺序是什么?

package com.hou.lock8;

import java.util.concurrent.TimeUnit;

public class Test5 {
    public static void main(String[] args) {
        Phone5 phone5 = new Phone5();
        new Thread(()->{Phone5.sendSms();}).start();

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

        new Thread(()->{phone5.call();}).start();
    }
}

class Phone5{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("发短信");
    }

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

结果:先打电话,然后发短信

原因:因为 一个锁锁的是Class类模板,一个锁的是对象调用者

  • 问题8

使用一个静态同步方法,一个同步方法,俩个对象调用顺序是什么?

package com.hou.lock8;

import java.util.concurrent.TimeUnit;

public class Test5 {
    public static void main(String[] args) {
        Phone5 phone1 = new Phone5();
        Phone5 phone2 = new Phone5();
        new Thread(()->{phone1.sendSms();}).start();

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

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

class Phone5{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("发短信");
    }

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

结果:先打电话,后发短信

原因:俩吧锁锁的不是同一个东西,不需要等待释放锁

小结:

锁 锁的是调用者 (对象 或者 Class)

  • new 出来的 this 是具体的一个对象

  • static Class 是唯一的一个模板

6、集合类不安全

List 在并发情况下是不安全

java.util.ConcurrentModificationException 并发修改异常

package com.hou.unsafe;

import java.util.ArrayList;
import java.util.UUID;

public class ListTest {
    public static void main(String[] args) {
        ArrayList<Object> arrayList = new ArrayList<>();

        // java.util.ConcurrentModificationException 并发修改异常
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }
    }
}

会造成 并发修改异常 (ConcurrentModificationException)

解决方案:

1、切换成Vector

package com.hou.unsafe;

import java.util.UUID;
import java.util.Vector;

public class ListTest {
    public static void main(String[] args) {
        Vector<Object> vector = new Vector<>();

        // Vector add()方法采用的是 synchronized方法
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                vector.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(vector);
            },String.valueOf(i)).start();
        }
    }
}

2、使用Collections.synchronizedList(new ArrayList<>()) 方法

package com.hou.unsafe;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

public class ListTest {
    public static void main(String[] args) {
        List<Object> synchronizedList = Collections.synchronizedList(new ArrayList<>());
        
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                synchronizedList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(synchronizedList);
            },String.valueOf(i)).start();
        }
    }
}

3、使用 JUC 中的包:new CopyOnWriteArrayList<>();

package com.hou.unsafe;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListTest {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Object> arrayList = new CopyOnWriteArrayList<>();

        // CopyOnWriteArrayList   add()方法采用的是lock.lock()锁的方法
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }
    }
}

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

多个线程调用的时候,list,读取的时候,固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题,读写分离

CopyOnWriteArrayList 比 Vector 厉害在哪里?

  • Vector 底层是使用 synchronized 关键字来实现的,效率比较低下
  • CopyOnWriteArrayList 使用的是Lock锁,效率会更加高效
Set 在并发情况下不安全
Iterable
Collection
List
Set
BlockingQueue

和List、Set 同级的还有一个BlockingQueue 阻塞队列;

Set 和 List 同理可得:在多线程情况下,普通的Set集合是线程不安全的

解决方案还是俩种:

  • 使用 Collections 工具类的 synchronized 包装的 Set类
  • 使用CopyOnWriteArraySet 写入时复制的 JUC 解决方案
package com.hou.unsafe;

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

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

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }

    }
}

HashSet 底层是什么?

  • hashSet 底层就是HashMap

    image-20220331213009044

    image-20220331213051678

  • add() 方法的本质就是一个map的key,map 的key是无法重复的,所以使用的就是map存储

  • hashSet 就是使用了 hashmap key 不重复的原理

  • PRESENT 是什么? 是一个常量,不会改变的常量, 无用的占位符

Map在并发情况下不安全

回顾 map 的基本操作:

  • 默认的加载因子是0.75,默认的初始容量是16
  • new HashMap<>() 等价于 new HashMap<>(16,0.75)

同样的HashMap 基础类也存在并发修改异常

解决方案:

  • 使用Collections.synchronizedMap(new HashMap<>()) 处理
  • 使用ConcurrentHashMap 进行并发处理
package com.hou.unsafe;

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

public class MapTest {
    public static void main(String[] args) {
        // HashMap<String, String> hashMap = new HashMap<>();
        // Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

研究ConcurrentHashMap底层原理

7、Callable (简单)

Callable接口类似于Runnable,因为它们都是为其实例可能由另一个线程执行的类设计的。然而,Runnable不返回结果,也不能抛出被检查的异常

Callable的特点

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

代码测试

使用传统的线程方式:

package com.hou.callable;

public class CallableTest {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(new MyThread()).start();
        }
    }
}

class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

使用Callable进行多线程操作

image-20220331212636815

Callable 泛型 V 就是call() 运行方法的返回值类型

如何使用呢?

Callable 怎么放入到 Thread里面呢?

源码分析:

image-20220331212604894

对于Thread运行,只能传入Runnable类型的参数

我们这是Callable怎么办呢?

看JDK api文档:

​ 在Runnable里面有一个叫做FutureTask的实现类

​ FutureTask中可以接收Callable参数

这样我们就可以先把Callable放入到FurureTask中,然后再把FutureTask 放入到Thread当中就可以了

package com.hou.callable;

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

public class CallableTest2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread2 thread = new MyThread2();

        for (int i = 1; i < 10; i++) {
            // 适配类:FutureTask
            FutureTask<String> futureTask = new FutureTask<String>(thread);
            // 放入Thread使用
            new Thread(futureTask,String.valueOf(i)).start();
            // 获取返回值
            String s = futureTask.get();
            // get()方法可能会被阻塞,如果在call方法中是一个耗时的方法,所以一般情况我们会把这个放在最后,或者使用异步通信	
            System.out.println("返回值:" + s);

        }
    }
}

class MyThread2 implements Callable{

    @Override
    public String call() throws Exception {
        System.out.println("Call:" + Thread.currentThread().getName());
        return "String:" + Thread.currentThread().getName();
    }
}

这样我们就可以使用Callable来进行多线程编程了,并且我们发现可以有返回值,并且可以抛出异常

重点:

  • new Thread(futureTask,“A”).start(); //结果会被缓存
  • futureTask.get() get()方法可能会被阻塞,如果在call方法中是一个耗时的方法,所以一般情况我们会把这个放在最后,或者使用异步通信

8、常用的辅助类(必会)

8.1、CountDownLatch

其实就是一个减法计数器,对于计数器归零之后在进行后面的操作,这是一个计数器

package com.hou.auxiliarywclass;

import java.util.concurrent.CountDownLatch;

// 这是一个技术去,减法
public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " GO OUT");
                countDownLatch.countDown(); // 每个线程都数量 -1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零,然后在向下执行

        System.out.println("close door");
    }
}

主要方法:

  • countDown() 减一操作
  • await() 等待计数器归零

await() 等待计数器为0,就唤醒,再继续向下执行

8.2、CyclickBarrier

其实就是一个加法计数器

package com.hou.auxiliarywclass;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

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

        // 主线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙~");
        });

        for (int i = 1; i <= 7; i++) {
            // 子线程
            int finallI = i;  // 由于作用域的问题,在线程中拿不到 i 的值,所以需要一个中间常量
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "收集了第 " + finallI + " 颗龙珠");
                try {
                    cyclicBarrier.await(); // 加法计数,等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

8.3、Semaphore

Semaphore:信号量

抢车位:

3个车位,6辆车:

package com.hou.auxiliarywclass;

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

public class SemaphoreTest {
    public static void main(String[] args) {
        // 停车位为3个
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    semaphore.acquire(); // 得到
                    // 抢到车位
                    System.out.println(Thread.currentThread().getName() + " 抢到了车位 " + finalI);
                    TimeUnit.SECONDS.sleep(2); // 停车2s
                    System.out.println(Thread.currentThread().getName() + "离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放
                }
            },String.valueOf(i)).start();
        }
    }
}

原理:

  • semaphore.acquire() 获得资源,如果资源已经使用完了,就等待资源释放后在进行使用
  • semaphore.relese() 释放,会将当前的信号量释放 +1,然后唤醒等待的线程
  • 作用:多个共享资源互斥的使用!并发限流,控制最大的线程数

9、读写锁

ReadWriteLock

先对于不加锁的情况:

如果我们做一个我们自己的cache缓存。分别有写入操作,读取操作

我们采用五个线程区写入,使用十个线程区读取。

我们来看下这个效果,在不加锁的情况下:

package com.hou.rw;

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;

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache_ReadWriteLock mycache = new MyCache_ReadWriteLock();

        // 开启五个线程  写入数据
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            },String.valueOf(i)).start();
        }

        // 开启十个线程  读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.get(String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
    }
}

class MyCache_ReadWriteLock{
    private volatile Map<String,String> map = new HashMap<>();
    // 使用读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 普通锁
    private Lock lock = new ReentrantLock();

    public void put(String key, String value){
        // 写入
        System.out.println(Thread.currentThread().getName() + "线程  开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程  写入OK");
    }

    public String get(String key){
        // 读取
        System.out.println(Thread.currentThread().getName() + "线程  开始读取");
        String s = map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程  读取OK");
        return s;
    }
}

运行效果如下:

image-20220331211803685

在一个线程写入的情况下,插入了其他线程的写入

对于这种情况会出现 数据不一致等情况

所以如果我们在多线程的情况下不加锁,会造成数据不可靠的问题

我们也可以采用Synchronized这种重量锁和轻量锁 lock 去保证数据的可靠

但是我们采用更细粒度的锁:ReadWriteLock 读写锁来保证,读写锁,也叫(独占锁,共享锁)

package com.hou.rw;

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;

// 写,指可以一个一个线程的写,读,可以一起读
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache_ReadWriteLock mycache = new MyCache_ReadWriteLock();

        // 开启五个线程  写入数据
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            },String.valueOf(i)).start();
        }

        // 开启十个线程  读取数据
        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(()->{
                mycache.get(String.valueOf(finalI));
            },String.valueOf(i)).start();
        }
    }
}

class MyCache_ReadWriteLock{
    private volatile Map<String,String> map = new HashMap<>();
    // 使用读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 普通锁
    private Lock lock = new ReentrantLock();

    public void put(String key, String value){
        // 加锁 写锁,也叫独占锁
        readWriteLock.writeLock().lock();
        try {
            // 写入
            System.out.println(Thread.currentThread().getName() + "线程  开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程  写入OK");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.writeLock().unlock();
        }

    }

    public String get(String key){
        // 加锁 读锁 也叫共享锁
        readWriteLock.readLock().lock();
        String s = "";
        try {
            // 读取
            System.out.println(Thread.currentThread().getName() + "线程  开始读取");
            s = map.get(key);
            System.out.println(Thread.currentThread().getName() + "线程  读取OK");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readWriteLock.readLock().unlock();
        }
        return s;
    }
}

运行结果:

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

在整个过程没有在出现错乱的情况,对于读取,我们运行多个线程同时读取,

对于写入, 我们一个线程一个线程的写入,这样就不会造成数据不一致问题

10、阻塞队列 BlockingQueue

BlockingQueue

阻塞

队列

FIFO 先进先出

写入:如果队列满了,就必须阻塞等待

读取:如果队列是空的,必须阻塞等待生产

BlockingQueue

blockingQueue 是 Collection 的一个子类

什么情况下我们会使用 阻塞队列呢?

多线程并发处理、线程池

graph TB;
Collection --> Set;
Collection --> Queue;
Collection --> List;
Queue --> Deque;
Queue --> BlockingQueue;
Queue --> AbstractQueue;
BlockingQueue --> LinkedBlockingQueue;
BlockingQueue --> ArrayBlockingQueue;

整个阻塞队列的家族如下:Queue以下实现的有Deque(双端队列)、AbstractQueue(非阻塞队列)、BlockingQueue(阻塞队列)

BlockingQueue 以下有Link 链表实现的阻塞队列、也有Array数组实现的阻塞队列

如何使用阻塞队列呢?

new ArrayBlockingQueue<>(); LinkedBlockingQueue<>();


BlockingQueue四组API

方式抛出异常不会抛出异常,有返回值阻塞等待超时等待
添加add()offer()put()offer(timenum,timeUnit)
移除remove()poll()take()poll(timenum,timeUnit)
判断队列首element()peek()
package com.hou.queue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

public class BlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        test04();
    }

    // 抛出异常
    public static void test01(){
        // 需要初始化队列的大小
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        // 抛出异常 java.lang.IllegalStateException   Queue null
        // System.out.println(blockingQueue.add("d"));

        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());

        // 如果多移除一个
        // 这会造成 java.util.NoSuchElementException 抛出异常
        System.out.println(blockingQueue.remove());
    }

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

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

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

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

        // 一直阻塞,不会返回
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");

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

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

    // 等待,超时等待
    // 这中情况也会等待队列有位置,或者有产品,但是会有超时结束
    public static void test04() throws InterruptedException {
        ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println("开始等待");
        System.out.println(blockingQueue.offer("d",2, TimeUnit.SECONDS)); // 超时2s,等待如果超时2s就结束等待
        System.out.println("结束等待");


        System.out.println("========取值========");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println("开始等待");
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));//超时等待2s
        System.out.println("结束等待");

    }
}

同步队列 SynchronousQueue

同步队列 没有容量,也可以视为容量为 1 的队列

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素

put 方法 和 take 方法

SynchronousQueue 和其他的 BlockingQueue 不一样,它不存储元素

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

并且SynchronousQueue 的take是使用了 lock锁保证线程安全的

package com.hou.queue;

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

/**
 * 同步队列
 * 同步队列 没有容量 ,也可以视为容量为1的队列
 * SynchronousQueue 和其他的 BlockingQueue 不一样
 * put 了一个元素,必须从里面先 take 取出来,否则不能在put进去值
 */
public class SynchronousQueueTest {
    public static void main(String[] args) {
        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();
            }
        },"T1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  " + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  " + synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "  " + synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}

11、线程池(重点)

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

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

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

池化技术:事先准备好一些资源,有人要用,就来拿,用完归还,以此来提高效率

线程池的好处:

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

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

线程池:三大方法

  • ExecutorService threadPool = Executors.newSingleThreadExecutor() 单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5) 创建一个固定的线程池大小
  • ExecuttorService threadPool3 = Executors.newCachedThraedPool() 可伸缩的

// 工具类 Executors 三大方法

package com.hou.pool;

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

// 工具类  Executors 三大方法:
public class Demo1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
        ExecutorService threadPool1 = Executors.newFixedThreadPool(5); // 固定线程池大小
        ExecutorService threadPool2 = Executors.newCachedThreadPool(); // 可伸缩的

        // 线程池用完必须关闭
        try {

            for (int i = 1; i <= 100; i++) {
                // 通过线程池创建线程
                threadPool2.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "  ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown(); // 关闭
        }
    }
}

七大参数

源码分析

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

本质:ThreadPoolExecutor()

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

阿里巴巴的Java操作手册中明确说明:对于 Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池

image-20220317220453682

OOM 溢出 ,内存溢出

业务图

image-20220318000608755

手动创建一个线程池

package com.hou.pool;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

// 使用原生的 ThreadPoolExecutor  实现银行办理业务
public class Demo2 {
    public static void main(String[] args) {
    	// 七大参数
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2, // 核心线程大小
                5, // 最大的线程池大小
                3, // 超时了没有人调用就会释放
                TimeUnit.SECONDS, // 超时单位
                new LinkedBlockingQueue<>(3), // 阻塞队列
                Executors.defaultThreadFactory(), // 线程工厂  创建线程的,一般不用动
                new ThreadPoolExecutor.AbortPolicy());// 拒绝策略,
  (// ThreadPoolExecutor.AbortPolicy()  功能: 银行满了,还有人进来,不处理这个人的,抛出异常)

        try {
            // 最大承载  队列(Deque) + max 值
            // 超过异常 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

四种拒绝策略

image-20220318001242899

package com.hou.pool;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

// 使用原生的 ThreadPoolExecutor  实现银行办理业务
public class Demo2 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                // new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
                // new ThreadPoolExecutor.CallerRunsPolicy() // 哪里来的去哪里 main线程进行处理
                // new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的进程竞争,不会抛出异常
                new ThreadPoolExecutor.DiscardPolicy()  // 队列满了,丢掉,不会抛出异常
                );


        try {
            // 最大承载  队列(Deque) + max 值
            // 超过异常 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

  • new ThreadPoolExecutor.AbortPolicy() :该拒绝策略为:银行满了,还有人进来,不处理这个人,抛出异常

    超出最大承载,就会抛出异常,最大承载:队列容量大小 + maxPoolSize

  • new ThreadPoolExecutor.CallerRunsPolicy() :该拒绝策略为:哪里来的去哪里 main线程进行处理

  • new ThreadPoolExecutor.DiscardPolicy() :该拒绝策略为:队列满了,丢掉,不会抛出异常

  • new ThreadPoolExecutor.DiscardOldestPolicy() :该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

小结和拓展

**如何去设置线程池的最大大小? ** CPU密集型 和 IO密集型

  • CPU密集型:电脑的核数是几核就选择几,选择maximunPoolSize的大小
    • 我们可以使用代码来获取逻辑处理器的数量
    • image-20220318141450878
  • IO密集型:在程序中有15个大型任务,IO十分占资源,I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到俩倍之间

12、四大函数式接口(必须掌握)

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

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

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
// 超级多的@FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用
// foreach()的参数也是一个函数式接口,消费者类的函数式接口

image-20220318145840221

函数型接口可以使用 lambda表达式

代码测试:

Function 函数型接口

image-20220318150307852

package com.hou.function;

import java.util.function.Function;

/**
 * Function 函数型接口
 */
public class Demo1 {
    public static void main(String[] args) {
        Function<String,String> function = new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        };

        // 使用 lambda 表达式
        Function<String,String> function1 = (str)->{return str;};

        System.out.println(function1.apply("string"));
    }
}

Predicate 断定型接口

image-20220318151437425

package com.hou.function;

import java.util.function.Predicate;

/**
 * Predicate 断定型接口:有一个输入参数,返回值只能是 布尔值
 */
public class Demo2 {
    public static void main(String[] args) {
        // 判断字符串是否为空
        Predicate<String> predicate = new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.isEmpty();
            }
        };

        // 使用lambda表达式
        Predicate<String> predicate1 = (str)->{return str.isEmpty();};

        System.out.println(predicate1.test("string"));
        System.out.println(predicate1.test(""));

    }
}

Consumer 消费型接口

image-20220318153156970

package com.hou.function;

import java.util.function.Consumer;

/**
 * Consumer 消费型接口 没有返回值 只有输入
 */
public class Demo3 {
    public static void main(String[] args) {
        Consumer consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };

        // 使用lambda表达式
        Consumer consumer1 = (str) ->{
            System.out.println(str);
        };

        consumer1.accept("abc");
    }
}

Supplier 供给型接口

image-20220318153854608

package com.hou.function;

import java.util.function.Supplier;

/**
 * Supplier 供给型接口  没有参数,只有返回值
 */
public class Demo4 {
    public static void main(String[] args) {
        Supplier<Integer> supplier =  new Supplier<Integer>() {
            @Override
            public Integer get() {
                return 1024;
            }
        };

        // 使用 lambda 表达式
        Supplier<Integer> supplier1 = ()->{
            return 1024;
        };

        System.out.println(supplier.get());
        System.out.println(supplier1.get());

    }
}

13、Stream流式计算

什么是Stream流式计算?

存储+计算

存储:集合、MySQL

计算:流式计算

=== 链式编程 ===

package com.hou.stream;

import java.util.Arrays;
import java.util.List;

public class TestStream {
    public static void main(String[] args) {
        User user1 = new User(1, "a", 21);
        User user2 = new User(2, "b", 22);
        User user3 = new User(3, "c", 23);
        User user4 = new User(4, "d", 24);
        User user5 = new User(5, "e", 25);
        User user6 = new User(6, "f", 26);

        List<User> list = Arrays.asList(user1, user2, user3, user4, user5, user6);

        // 计算交给 stream流
        // 链式编程
        list.stream()
                .filter((u)->{return u.getId()%2==0;})
                .filter((u)->{return u.getAge()>23;})
                .map((u)->{return u.getName().toUpperCase();})
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

14、ForkJoin

什么是FrokJoin?

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

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

image-20220321172034139

ForkJoin 特点:工作窃取

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

image-20220321173043398

如何使用 ForkJoin?

  • 1、通过ForkJoinPool来执行

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

image-20220321173621799

  • 3、计算类要去继承ForkJoinTask

ForkJoin的计算类

package com.hou.forkjoin;

import java.util.concurrent.RecursiveTask;

public class ForkJoinDemo extends RecursiveTask<Long> {

    private long star;
    private long end;

    // 临界值
    private long temp = 10000L;

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


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

            return taskSum;
        }
    }
}

测试类

package com.hou.forkjoin;

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

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1(); // 34547
        test2(); // 371045
        test3(); // 5440
    }

    /**
     * 普通计算
     */
    public static void test1(){
        long star = System.currentTimeMillis();
        long sum = 0L;

        for (long i = 1L; i < 1000_0000_0000L; i++) {
            sum+=i;
        }

        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end-star));
        System.out.println(sum);
    }


    /**
     * 使用 ForkJoin
     */
    public static void test2() throws ExecutionException, InterruptedException {
        long star = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, 1000_0000_0000L);
        // forkJoinPool.execute(task); // 没有返回值  submit 有返回值
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        System.out.println(sum);

        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end-star));
    }


    /**
     * 使用 Stream 并行流
     */
    public static void test3(){
        long star = System.currentTimeMillis();

        // Stream 并行流
        long sum = LongStream.range(0L, 1000_0000_0000L).parallel().reduce(0, Long::sum);

        System.out.println(sum);
        long end = System.currentTimeMillis();
        System.out.println("时间:" + (end-star));
    }
}

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

image-20220321182646652

reduce()方法的优点

image-20220321183716216

15、异步回调

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

其实就是前端 --> 发送ajax异步请求给后端

image-20220321184148296

但是我们平时都是使用CompletableFuture

(1)没有返回值的 runAsync 异步回调

package com.hou.future;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * CompletableFuture   异步回调
 * runAsync  没有返回值
 * supplyAsync 有返回值
 */
public class TestCompletableFuture {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        testRunFuture();
        testSupplyAsync();
    }

    // 没有返回值的异步回调
    public static void testRunFuture() throws ExecutionException, InterruptedException {
        // 发起一个请求
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            // 发起一个异步任务
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 输出执行结果
        System.out.println(future.get()); // 获取执行结果
    }

    // 有返回值的异步回调
    public static void testSupplyAsync() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());

            try {
                TimeUnit.SECONDS.sleep(2);
                // int i = 1/0;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return 1024;
        });

        System.out.println(future.whenComplete((t,u) -> {
            // success 回调
            System.out.println("t=>" + t); // 正常的返回结果
            System.out.println("u=>" + u); // 抛出异常的返回结果 错误信息
        }).exceptionally((e)->{
            // error 回调
            System.out.println(e.getMessage());
            return 404;
        }).get());
    }
}

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

T:是代表的 正常返回的结果

U:是代表的 抛出异常的错误信息

如果发生了异常,get可以获取到 exceptionally() 返回的值

16、JMM

请你谈谈你对Volatile 的理解

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

1、保证可见性

2、不保证原子性 原子性:不可分割,即一个操作或者多个操作,要么全部执行并且不被打断,要么就都不执行

3、禁止指令重排

什么是JMM?

JMM :Java 内存模型,不存在的东西,是一个概念,也是一个约定

1、线程解锁前,必须要把共享变量立刻刷回主存

2、线程加锁前,必须读取主存中的最新值到工作内存中

3、加锁和解锁是同一把锁

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

八种操作(每个操作都为原子操作)

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

image-20220322183805608

image-20220322184220910

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

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

image-20220322185109771

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

17、Volatile

1、保证可见性

2、不保证原子性

3、禁止指令重排

1、保证可见性

package com.hou.jmm;

import java.util.concurrent.TimeUnit;

public class JMMDemo01 {

    // 如果不加volatile 程序会死循环
    // 加了volatile 是可以保证可见性的
    private volatile static Integer number = 0;

    public static void main(String[] args) {
        // main 线程
        // 子线程 1
        new Thread(()->{
            while (number == 0){

            }
        }).start();

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

        // 子线程 2
        new Thread(()->{
            while (number == 0){

            }
        }).start();

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

        number = 1;
        System.out.println(number);

    }
}

不保证原子性

原子性:不可分割

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

package com.hou.jmm;

/**
 * 不保证原子性
 * number <= 2W
 */
public class JMMDemo02 {

    private static volatile int number = 0;

    public static void add(){
        number++;
        // ++ 不是一个原子性操作,是俩个~三个操作
    }

    public static void main(String[] args) {
        // 理论上 number = 2w
        for (int i = 1; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();

        }

        // Thread.activeCount()  运行中的线程数量
        while (Thread.activeCount() > 2){
            // main gc
            Thread.yield(); // 礼让
        }

        System.out.println(Thread.currentThread().getName() + ",num=" + number);
    }
}

如果不加lock和synchronized,怎样保证原子性呢?

使用 java 反编译 javap

image-20220322192841596

解决方法:使用JUC下的原子包下的class

image-20220322193122422

代码如下:

package com.hou.jmm;

import java.util.concurrent.atomic.AtomicInteger;

public class VDemo01 {

    private static volatile AtomicInteger number = new AtomicInteger();

    public static void add(){
        number.incrementAndGet(); // 底层是CAS  保证的原子性
    }

    public static void main(String[] args) {
        // 理论上 number = 2w

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            // main gc
            Thread.yield();
        }

        System.out.println(number);
    }
}

这些类的底层都直接和操作系统挂钩的!是在内存中修改值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yc7VPqPC-1649344418938)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303626.png)]

Unsafe 类是一个很特殊的存在

原子类为什么这么高级?

3、禁止指令重排

什么是指令重排?

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

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

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

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

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

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

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

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

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

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

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

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

volatile可以避免指令重排

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

内存屏障:CPU指令,使用:

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

image-20220322200019938

总结

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

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

18、玩转单例模式

饿汉式、DCL懒汉式

饿汉式

package com.hou.single;

/**
 * 饿汉式单例
 */
public class Hungry {

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

    private Hungry(){

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

DCL懒汉式

在多线程并发的情况下,单例是有问题的

package com.hou.single;

public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + " ok");
    }

    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }

        return lazyMan;
    }

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

image-20220326232216872

所以需要加锁

package com.hou.single;

public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + " ok");
    }

    private volatile static LazyMan lazyMan;

    // 双重检测锁模式的 懒汉式单例  DCL懒汉式
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan(); // 不是一个原子性操作
                    /**
                     * 会经过三个步骤
                     *      1、分配内存空间
                     *      2、执行构造方法,初始化对象
                     *      3、把这个对象指向这个空间
                     * 		可能会由于指令重排的原因,造成其他线程进入之后没有完成构造
                     *		所以使用 volatile 保证不会被指令重排
                     */
                }
            }
        }

        return lazyMan;
    }

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

采用静态内存类的方法

package com.hou.single;

// 静态内部类
public class Holder {

    private Holder(){

    }

    private static Holder getInstance(){
        return InnerClass.HOLDER;
    }

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

但是在这种情况下单例还是可以被破坏,因为Java有强大的反射

单例有问题, 反射

package com.hou.single;

import java.lang.reflect.Constructor;

public class LazyMan1 {

    private LazyMan1(){
        System.out.println(Thread.currentThread().getName() + "  ok");
    }

    private volatile static LazyMan1 lazyMan1;

    // 双重检测锁模式的 懒汉式单例  DCL单例
    public static LazyMan1 getInstance(){
        if (lazyMan1==null){
            synchronized (LazyMan1.class){
                if (lazyMan1==null){
                    lazyMan1 = new LazyMan1(); // 不是一个原子性操作
                }
            }
        }

        return  lazyMan1;
    }


    // 反射
    public static void main(String[] args) throws Exception {
        LazyMan1 instance = LazyMan1.getInstance();
        // 获取空参构造器
        Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan1 instance1 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

    }
}

image-20220327210745194

这时发现我们的单例已经被破坏

解决方式:(在无参构造中再添加一把锁)

package com.hou.single;

import java.lang.reflect.Constructor;

public class LazyMan1 {

    private LazyMan1(){
        synchronized (LazyMan1.class){
            if (lazyMan1!=null){
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan1 lazyMan1;

    // 双重检测锁模式的 懒汉式单例  DCL单例
    public static LazyMan1 getInstance(){
        if (lazyMan1==null){
            synchronized (LazyMan1.class){
                if (lazyMan1==null){
                    lazyMan1 = new LazyMan1(); // 不是一个原子性操作
                }
            }
        }

        return  lazyMan1;
    }


    // 反射
    public static void main(String[] args) throws Exception {
        LazyMan1 instance = LazyMan1.getInstance();
        // 获取空参构造器
        Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan1 instance1 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

    }
}

image-20220327211057388

这时发现我们的单例已经避免了这种反射的破坏

但是我们换一个反射方式破坏还是有问题的

这次我们不使用getInstance()方式创建对象

package com.hou.single;

import java.lang.reflect.Constructor;

public class LazyMan1 {

    private LazyMan1(){
        synchronized (LazyMan1.class){
            if (lazyMan1!=null){
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan1 lazyMan1;

    // 双重检测锁模式的 懒汉式单例  DCL单例
    public static LazyMan1 getInstance(){
        if (lazyMan1==null){
            synchronized (LazyMan1.class){
                if (lazyMan1==null){
                    lazyMan1 = new LazyMan1(); // 不是一个原子性操作
                }
            }
        }

        return  lazyMan1;
    }


    // 反射
    public static void main(String[] args) throws Exception {
        // LazyMan1 instance = LazyMan1.getInstance();
        // 获取空参构造器
        Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan1 instance = declaredConstructor.newInstance();
        LazyMan1 instance1 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yZCDZnsO-1649344418945)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303631.png)]

发现我们的单例模式又被破坏了

解决方式: (加一个加密变量)

package com.hou.single;

import java.lang.reflect.Constructor;

public class LazyMan1 {

    private static boolean flag = false;

    private LazyMan1(){
        synchronized (LazyMan1.class){
            if (flag==false){
                flag=true;
            }else {
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan1 lazyMan1;

    // 双重检测锁模式的 懒汉式单例  DCL单例
    public static LazyMan1 getInstance(){
        if (lazyMan1==null){
            synchronized (LazyMan1.class){
                if (lazyMan1==null){
                    lazyMan1 = new LazyMan1(); // 不是一个原子性操作
                }
            }
        }

        return  lazyMan1;
    }


    // 反射
    public static void main(String[] args) throws Exception {
        // LazyMan1 instance = LazyMan1.getInstance();
        // 获取空参构造器
        Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan1 instance = declaredConstructor.newInstance();
        LazyMan1 instance1 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

    }
}

image-20220327211832280

这时发现我们的单例没有被破坏

但是无论怎么加密都是可以被解密的

如果我们的变量被知道了

package com.hou.single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class LazyMan1 {

    private static boolean flag = false;

    private LazyMan1(){
        synchronized (LazyMan1.class){
            if (flag==false){
                flag=true;
            }else {
                throw new RuntimeException("不要试图使用反射破坏异常");
            }
        }
    }

    private volatile static LazyMan1 lazyMan1;

    // 双重检测锁模式的 懒汉式单例  DCL单例
    public static LazyMan1 getInstance(){
        if (lazyMan1==null){
            synchronized (LazyMan1.class){
                if (lazyMan1==null){
                    lazyMan1 = new LazyMan1(); // 不是一个原子性操作
                }
            }
        }

        return  lazyMan1;
    }


    // 反射
    public static void main(String[] args) throws Exception {
        // LazyMan1 instance = LazyMan1.getInstance();

        Field flag = LazyMan1.class.getDeclaredField("flag");
        flag.setAccessible(true);

        // 获取空参构造器
        Constructor<LazyMan1> declaredConstructor = LazyMan1.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan1 instance = declaredConstructor.newInstance();

        flag.set(instance,false);

        LazyMan1 instance1 = declaredConstructor.newInstance();

        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

    }
}

image-20220327212327187

这时发现我们的单例又被破坏了

怎们解决呢?

枚举

通过源码分析

image-20220327212744728

发现枚举类型不能被破坏

enum 是一个什么? 本身也是一个Class类

从编译之后的代码中发现类中有一个无参构造

image-20220327213553625

所以我们使用这个无参构造

package com.hou.single;

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

// enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }


}


class Test{

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

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMU0z587-1649344418952)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303636.png)]

这时发现出现的异常尽然是 没有无参构造方法 而不是newInstance()方法的源码中所说的异常

这时再用java反编译查看编译后的class文件

image-20220327214558275

发现这里也是有一个无参的构造器

最后我们使用专业工具反编译

image-20220327220944021

发现这里其实是一个有参的构造器

最后我们修改代码

package com.hou.single;

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

// enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }


}


class Test{

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

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ivrWRKyT-1649344418955)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303639.png)]

发现结果抛出的异常和我们的源码抛出的异常一致

19、深入理解CAS

什么是 CAS ?

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

AtomicInteger 原子类

package com.hou.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {

    // CAS : compareAndSet()  比较并交换
    public static void main(String[] args) {
        //AtomicInteger 原子类
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // boolean compareAndSet(int expectedValue, int newValue)
        // 期望值,更新值
        // 如果实际值 和 期望值相同就更新   不相投,就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());


        // 因为期望值是 2020, 实际值变成了2022  所以会修改失败
        // CAS 是CPU的并发原语
        atomicInteger.getAndIncrement(); // ++操纵
        System.out.println(atomicInteger.compareAndSet(2020,2021));
        System.out.println(atomicInteger.get());
    }
}

image-20220331182821309

Unsafe 类

image-20220331183430361

image-20220331184033546

总结:

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

缺点:

  • 使用的是自旋锁,所以循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • 它会存在ABA问题

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

image-20220331191045712

测试:

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

线程2:俩个操作:

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

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

package com.hou.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo1 {
    // CAS  ABA 问题
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);

        // 对于我们平是写的SQL: 采用 乐观锁
        // 假设
        System.out.println("=========捣乱的线程===========");
        System.out.println(atomicInteger.compareAndSet(1,3));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(3,1));
        System.out.println(atomicInteger.get());

        // 这种情况是我们不想存在的,被操作过都不知道
        System.out.println("=========期望的线程==============");
        System.out.println(atomicInteger.compareAndSet(1,2));
        System.out.println(atomicInteger.get());

    }

}

20、原子引用

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

带版本号的 原子操作!

Integer 使用了对象缓存机制,默认范围是 -128~127,推荐使用静态工厂方法valueOf 获取对象实例,

而不是new,因为valueOf 使用cache(缓存),而new一定会创建新的对象分配新的内存空间

image-20220331192607497

所以如果遇到,使用大于128的时候,使用原子引用的时候,如果超过了这个值,那么就不会进行版本上升

image-20220331193903395

修改使用小于128的时候

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4fIEghBt-1649344418962)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303646.png)]

这时发现我们的版本号上升了

package com.hou.cas;

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

public class CASDemo2 {
    public static void main(String[] args) {
        // 正常情况不会使用 Integer包装类,一般使用的是一个个对象
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();  // 获取版本号
            System.out.println("A=>" + stamp);

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

            // Version + 1
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println("A2=>" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(2,1,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
            System.out.println("A3=>" + atomicStampedReference.getStamp());

        },"A").start();


        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println("B=>" + stamp);

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

            atomicStampedReference.compareAndSet(1,6,stamp,stamp+1);
            System.out.println("B2=>" + atomicStampedReference.getStamp());

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

正常业务操作中,我们一般使用的是一个个对象,一般情况不会使用Integer

21、各种锁的理解


1、公平锁、非公平锁

  • 公平锁:非常公平,不允许插队的,必须先来后到

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    
  • 非公平锁:非常不公平,允许插队的,可以改变顺序 (默认为非公平锁)

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

2、可重入锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suTG6beH-1649344418964)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303647.png)]

Synchronized 锁

package com.hou.lock;

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

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

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

    }
}

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

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

lock 锁

package com.hou.lock;

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

public class Demo2 {
    public static void main(String[] args) {
        Phone2 phone2 = new Phone2();
        new Thread(()->{
            phone2.sms();
        },"A").start();

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

}

class Phone2{
    // 细节:这个是俩吧锁,俩个钥匙
    Lock lock = new ReentrantLock();
    // lock锁必须配对,否则就会死锁在里面

    public void sms(){
        lock.lock();
        // lock.lock();

        try {
            System.out.println(Thread.currentThread().getName() + " => sms");
            call();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            // lock.unlock();
        }

    }

    public void call(){
        lock.lock();

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

  • lock 锁必须配对,相当于 lock 和 unlock 必须数量相同
  • 在外面加的锁,也可以加在里面解锁,在里面加的锁,也可以在外面解锁

3、自旋锁

spinlock

image-20220331213749149

设计自旋锁

package com.hou.lock;

import java.util.concurrent.TimeUnit;

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

        // 使用CAS实现自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        new Thread(()->{
            spinlockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                spinlockDemo.unMyLock();
            }

        },"T1").start();

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

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

            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                spinlockDemo.unMyLock();
            }
        },"T2").start();
    }
}

运行结果:

T2 进程必须等待 T1 进程unlock后,才能Unlock,在这之前进行自旋等待。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLzyYiej-1649344418967)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303649.png)]

4、死锁

死锁是什么?

image-20220331215134453

死锁测试,怎么排除死锁

package com.hou.lock;

import java.util.concurrent.TimeUnit;

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

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

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

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

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

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

            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "lock" + lockB + "===>get" + lockA);
            }
        }
    }
}

运行结果:死锁

image-20220331215641594

死锁排查

解决问题

1、使用 jps 定位进程号,jdk 的 bin 目录下有一个 jps

命令 jps -l

image-20220331215920572

2、 使用 jstack 进程号 找到死锁信息

一般情况信息在最后

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGoNT0TF-1649344418973)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303653.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EuOOnTQa-1649344418974)(https://gitee.com/libaistudy/image/raw/master/%E5%9B%BE%E5%BA%8A/202204072303654.png)]

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

  • 日志
  • 堆栈信息

;
},“B”).start();
}

}

class Phone2{
// 细节:这个是俩吧锁,俩个钥匙
Lock lock = new ReentrantLock();
// lock锁必须配对,否则就会死锁在里面

public void sms(){
    lock.lock();
    // lock.lock();

    try {
        System.out.println(Thread.currentThread().getName() + " => sms");
        call();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        lock.unlock();
        // lock.unlock();
    }

}

public void call(){
    lock.lock();

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

}


- lock 锁必须配对,相当于 lock 和 unlock 必须数量相同
- 在外面加的锁,也可以加在里面解锁,在里面加的锁,也可以在外面解锁



3、自旋锁

spinlock

[外链图片转存中...(img-fi3Lc5Dr-1649344418966)]



设计自旋锁

```java
package com.hou.lock;

import java.util.concurrent.TimeUnit;

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

        // 使用CAS实现自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        new Thread(()->{
            spinlockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                spinlockDemo.unMyLock();
            }

        },"T1").start();

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

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

            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                spinlockDemo.unMyLock();
            }
        },"T2").start();
    }
}

运行结果:

T2 进程必须等待 T1 进程unlock后,才能Unlock,在这之前进行自旋等待。

[外链图片转存中…(img-HLzyYiej-1649344418967)]

4、死锁

死锁是什么?

[外链图片转存中…(img-U1w1vf7v-1649344418969)]

死锁测试,怎么排除死锁

package com.hou.lock;

import java.util.concurrent.TimeUnit;

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

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

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

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

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

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

            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "lock" + lockB + "===>get" + lockA);
            }
        }
    }
}

运行结果:死锁

[外链图片转存中…(img-zBU5978K-1649344418970)]

死锁排查

解决问题

1、使用 jps 定位进程号,jdk 的 bin 目录下有一个 jps

命令 jps -l

[外链图片转存中…(img-VvJD8Dd7-1649344418972)]

2、 使用 jstack 进程号 找到死锁信息

一般情况信息在最后

[外链图片转存中…(img-xGoNT0TF-1649344418973)]

[外链图片转存中…(img-EuOOnTQa-1649344418974)]

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

  • 日志
  • 堆栈信息
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值