JUC编程

该博客围绕Java多线程与并发编程展开,涵盖线程、锁、集合安全、线程池等内容。介绍了wait与sleep区别、synchronized与Lock差异,分析多线程集合安全问题及解决方案,还阐述了JDK8新特性、JMM与volatile、CAS与原子引用等知识,适合Java面试备考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

感谢狂神的分享:B站视频地址

1. 概述

1)环境准备

使用JDK8版本

  1. 在idea的三个地方,确保使用 Java8 版本
    在这里插入图片描述
  2. 导入插件
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
    <scope>provided</scope>
</dependency>

2)java线程

  1. java默认开启几个线程?
    两个:main、GC
  2. java可以开启线程吗?
    不可以,需要通过native方法 private native void start0(); 调用C++底层操作硬件开启线程
  3. java代码获取CPU核数
public static void main(String[] args) {
    System.out.println(Runtime.getRuntime().availableProcessors());
}

在这里插入图片描述

  1. 线程的状态有哪些?
public enum State {
    NEW,    // 新生
    RUNNABLE,    // 运行
    BLOCKED,    // 阻塞
    WAITING,    // 无限等待
    TIMED_WAITING,    // 限时等待
    TERMINATED;    // 终止
}

java线程 有6中状态,电脑中 进程 有5中状态:操作系统中进程的五种状态与JAVA中线程的六种状态

3)wait 与 sleep 的区别

  • 来自不同的类:
    wait --> Object; sleep --> Thread
  • wait 释放锁资源,sleep 不释放
  • wait 必须在同步代码块中,sleep 可以用在任何地方

2. Lock 锁

2.1 synchronized 与 Lock

不带锁

/**
* 真正的多线程开发:
* 线程就是一个资源类,没有任何附属的操作
*/
public class SaleTicket {
   public static void main(String[] args) {
       Ticket ticket = new Ticket();
       //Runnable接口为函数式接口
       new Thread(()->{
           for (int i = 0; i < 40; i++) {
               ticket.sale();
           }
       },"a").start();
       new Thread(()->{
           for (int i = 0; i < 40; i++) {
               ticket.sale();
           }
       },"b").start();
       new Thread(()->{
           for (int i = 0; i < 40; i++) {
               ticket.sale();
           }
       },"c").start();
   }
}
//资源类oop编程
class Ticket {
   //属性,方法
   private int number = 50;
   //买票的方式
   public void sale() {
       if (number>0) {
           System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
       }
   }
}

输出顺序是乱的:

a卖出了8票,剩余:7
b卖出了28票,剩余:27
c卖出了25票,剩余:24
b卖出了7票,剩余:6

加了 synchronized 锁后,顺序正常

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

使用 Lock 锁,也能实现相同功能

公平锁:线程执行顺序先来后到,不可以插队;
非公平锁:线程执行的时候,可以插队
在这里插入图片描述

class Ticket {

    private int number = 50;
    //使用lock锁的方式,实现同步代码
    Lock lock = new ReentrantLock();

    public synchronized void  sale() {

        lock.lock();    //加锁
        try {  // try中是业务代码
            if (number>0) {
                System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  // 释放锁
        }
    }
}

Sychronized和lock的区别

在这里插入图片描述

  1. synchronized 内置的java关键字,Lock锁是一个java类
  2. synchronized 无法判断获取锁的状态,Lock锁可以判断是否获取到了锁
  3. synchronized 会自动释放锁,lock必须手动释放锁,否则会发生 死锁
  4. synchronized 线程一(获得锁,阻塞),线程二(一直等待);Lock锁就不一定会等待下去.
  5. synchronized 可重入锁、不可中断、非公平;Lock,可重入锁、可以中断锁、可自己设置是否公平
  6. synchronized 适合锁少量的代码的同步问题,Lock适合锁大量的代码同步问题。

2.2 生产者&消费者&虚假唤醒

虚假唤醒

虚假唤醒:线程可以在没有被通知、中断或者超时的情况下被唤醒。
【原因:】

  1. 当Object.wait()这个方法运行时,当前的线程会进入等待状态,并自动释放锁。当被其他线程唤醒时,它会在wait()之后的地方继续开始运行
  2. 当Object.notifyAll运行时,会唤醒所有处于等待状态的线程同时进行抢夺锁,对于不应被唤醒的线程而言,便是 虚假唤醒

参考博客:虚假唤醒及避免

参考博客:java多线程虚假唤醒及解决方法

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.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();*/
    }
}
/** 分成三步:等待 业务 通知
 * 资源类的作用:当 num <= 0,则 +1; 当 num >= 0,则 -1;
 */
class Data{
    private int num = 0;
    public synchronized void increment() throws InterruptedException {
        if (num > 0){
            //等待
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+" ----> "+num);
        //通知其他线程,我加一完毕了
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (num <= 0){
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+" ----> "+num);
        //通知其他线程,我减一完毕
        this.notifyAll();
    }
}

输出结果正常:

A ----> 1
B ----> 0
A ----> 1
B ----> 0
A ----> 1
B ----> 0
A ----> 1
B ----> 0

当启动线程C(注释掉的那段代码),则出现“虚假唤醒”现象。输出混乱出现负数:

A ----> 1
C ----> 0
B ----> -1
C ----> -2
A ----> -1
A ----> 0
A ----> 1
C ----> 0

问题的关键点在于:程序仅使用if对product做了一次判断,我们应该使用while循环去判断。即wait()要在while循环中。
使用 while 代替 if 判断,可以防止线程被"虚假唤醒"
在这里插入图片描述
代码改造及输出结果如下:
在这里插入图片描述

juc 版

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

/** 分成三步:等待 业务 通知
 * 资源类的作用:当 num <= 0,则 +1; 当 num >= 0,则 -1;
 */
class Data3{
    private int num = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void increment() {
        lock.lock();
        try {
            while (num > 0){ condition.await(); }
            num++;
            System.out.println(Thread.currentThread().getName()+" ----> "+num);
            //通知其他线程,我加一完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() {
        lock.lock();
        try {
            while (num <= 0){ condition.await(); }
            num--;
            System.out.println(Thread.currentThread().getName()+" ----> "+num);
            //通知其他线程,我加一完毕了
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

输出结果正常,但是顺序随机:

A ----> 1
B ----> 0
A ----> 1
C ----> 0
A ----> 1
C ----> 0

使用Lock锁唤醒指定状态的线程

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

/** 
 * 使用 condition 类,实现唤醒指定的线程:顺序执行
 */
class Data3{
    private int num = 1;
    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();
    public void printA() {
        lock.lock();
        try {
            while (num != 1){ condition1.await(); }
            num = 2;
            System.out.println(Thread.currentThread().getName()+" ----> "+"AAAAAAAAAAAAAA");
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (num != 2){ condition2.await(); }
            num = 3;
            System.out.println(Thread.currentThread().getName()+" ----> "+"BBBBBBBBBBBBBB");
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            while (num != 3){ condition3.await(); }
            num = 1;
            System.out.println(Thread.currentThread().getName()+" ----> "+"CCCCCCCCCCCCCC");
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

输出结果:

A ----> AAAAAAAAAAAAAA
B ----> BBBBBBBBBBBBBB
C ----> CCCCCCCCCCCCCC
A ----> AAAAAAAAAAAAAA
B ----> BBBBBBBBBBBBBB
C ----> CCCCCCCCCCCCCC

2.3 通过八锁理解锁

就是关于锁的8个问题,彻底理解什么是锁,锁的是什么。

  1. 发短信的方法,无论是否延迟,都会先执行。
/**
 * 一、发短信方法不延迟,输出顺序为: 1.发短信 2.打电话
 * 二、发短信方法延迟4秒,输出顺序:1.发短信 2.打电话
 */
public class EightLock01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }

}

class Phone{
    //synchronized锁的对象 是方法的调用者
    //两个方法用的是同一个锁,谁先拿到谁先执行
    public synchronized void sendSms() {
        // 发短信的方法,延迟4秒
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

一个对象的普通方法与同步方法;两个对象的同步方法:

/**
 * 三、新增非同步方法 sayHello 并替换掉 call 方法,输出顺序: 1. hello  2.发短信
 * 四、创建两个实例分别调用同步方法,输出结果: 1.打电话  2.发短信
 */
public class EightLock01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone{
    //synchronized锁的对象 是方法的调用者
    //两个方法用的是同一个锁,谁先拿到谁先执行
    public synchronized void sendSms() {
        // 发短信的方法,延迟4秒
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
    public void sayHello() {   //非同步方法 Hello
        System.out.println("hello");
    }
}

两个静态同步方法,一个&两个对象:

/**
 * 五、两个静态同步方法,一个对象;输出顺序: 1.发短信 2.打电话
 * 六、两个静态同步方法,两个对象;输出顺序: 1.发短信 2.打电话
 * 原因:sync
 */
public class EightLock01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
//synchronized锁的对象是方法的调用者
//static 静态方法 类一加载就有了,两个方法用的是同一个锁
class Phone{
    public static synchronized void sendSms() {
        // 发短信的方法,延迟4秒
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call() {
        System.out.println("打电话");
    }
}

一个静态同步方法,一个普通同步方法;一个or两个对象

/**
 * 七、一个对象的静态同步方法与普通同步方法:1.打电话  2.发短信
 * 八、两个对象,输出结果:1.打电话  2.发短信
 */
public class EightLock01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}

class Phone{
    public synchronized void sendSms() {
        // 发短信的方法,延迟4秒
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call() {
        System.out.println("打电话");
    }
}

【结论】synchronized 作用在普通同步方法上锁的是 对象,作用在静态同步方法上锁的

3. 多线程下的集合安全

3.1 CopyOnWriteArrayList

并发情况下,ArrayList并不安全,可能会出现ConcurrentModificationException异常:

public class ListTest {
    public static void main(String[] args) {
        // 单线程时,ArrayList没问题
        List<String> strList = new ArrayList<String>();
        for (int i = 0; i < 10; i++) {
            strList.add(UUID.randomUUID().toString().substring(0,5));
        }
        System.out.println(strList);

        // 多线程时,有并发修改异常 ConcurrentModificationException
        /*for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                strList.add(UUID.randomUUID().toString().substring(0,5));
            },String.valueOf(i)).start();
        }
        System.out.println(strList);*/

        /** 想要变得安全,调整如下:
         *  1. 使用线程安全类 Vector 代替 ArrayList
         *      List<String> strList1 = new Vector<String>();
         *  2. 使用集合工具类,变得线程安全
         *      List<String> strList2 = Collections.synchronizedList(strList);
         *  3. 使用 CopyOnWriteArrayList
         *      CopyOnWriteArrayList<String> strList3 = new CopyOnWriteArrayList<>();
         */
        /** CopyOnWrite 写入时复制,计算机程序设计领域的一种优化策略
         * 多个线程调用list的时候,读取时是固定的,写入时就可能被覆盖。
         *      那么就需要CopyOnWrite先复制出来一份,写入完成时再放回去
         */
//        List<String> safeList = new Vector<String>();
//        List<String> safeList = Collections.synchronizedList(strList);
        CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                safeList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(safeList);
            },String.valueOf(i)).start();
        }
    }
}

【总结】COW(写入时复制),计算机程序设计领域的一种优化策略。多个线程调用list的时候,读取时是固定的,写入时就可能被覆盖。那么就需要CopyOnWrite先复制出来一份,写入完成时再放回去,避免写入时数据被覆盖,造成数据问题。

CopyOnWriteArrayList 比 Vector牛在哪里

在这里插入图片描述

  • Vector的add()是同步方法(同步方法自动加锁解锁),效率会低很多
  • CopyOnWriteArrayList是先复制一份数组,写入完成后再set回去。效率高出很多

3.2 CopyOnWriteArraySet

HashSet也不是线程安全类,解决方案有下面两种:

public class SetTest {
    public static void main(String[] args) {
        //HashSet多线程时,有并发修改异常 ConcurrentModificationException
        /*Set<String> hashSet = new HashSet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                hashSet.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(hashSet);
            },String.valueOf(i)).start();
        }*/

        /**解决方法:
         *  1. 使用集合工具类,变得线程安全
         *      Set<String> safeSet = Collections.synchronizedSet(new HashSet<>());
         *  2. 使用线程安全类 CopyOnWriteArraySet
         *      Set<String> safeSet = new CopyOnWriteArraySet<>();
         */
//        Set<String> safeSet = Collections.synchronizedSet(new HashSet<>());
        Set<String> safeSet = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(()->{
                safeSet.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(safeSet);
            },String.valueOf(i)).start();
        }
    }
}

HashSet 源码解析
在这里插入图片描述

3.3 ConcurentHashMap

HashMap 线程不安全,解决方法有下面联众

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

        //HashMap多线程时,有并发修改异常 ConcurrentModificationException
        Map<String, Object> hashMap = new HashMap<>();
        /*for (int i = 1; i <= 30; i++) {
            String key = String.valueOf(i);
            new Thread(()->{
                hashMap.put(key,UUID.randomUUID().toString().substring(0,5));
                System.out.println(hashMap);
            },String.valueOf(i)).start();
        }*/

        /** 解决方法:
         *  1. 使用Collections工具类:
         *      Map<String, Object> safeMap = Collections.synchronizedMap(new HashMap<>());
         *  2. 使用线程安全类
         *      Map<String, Object> safeMap = new ConcurrentHashMap<>();
         */
        // Map<String, Object> safeMap = Collections.synchronizedMap(new HashMap<>());
        Map<String, Object> safeMap = new ConcurrentHashMap<>();
        for (int i = 1; i <= 30; i++) {
            String key = String.valueOf(i);
            new Thread(()->{
                safeMap.put(key,UUID.randomUUID().toString().substring(0,5));
                System.out.println(safeMap);
            },String.valueOf(i)).start();
        }
    }
}

4. Callable

在这里插入图片描述
继承Callable接口,实现多个
在这里插入图片描述

public class CallableTest {
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask,"A").start();
        try {
            String result = futureTask.get();  // 获取Callable的返回结果
            System.out.println(result);     // 输出:this is my call
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 自定义类继承 Callable 接口,并返回值
class MyCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "this is my call";
    }
}

注意:同一个 FutureTask 对象,即使启动多个线程,也只调用一次Callable中的call()方法;如果多个 FutureTask 对象,则调用多次。
在这里插入图片描述

5. 常用工具类

5.1 CountDownLatch

public static void main(String[] args) throws InterruptedException {
    /** 计数器清零,才会向下执行。
     *  1. 初始化的时候,设置数量为 6
     *  2. 每次执行 countDown() 方法,数量 -1
     *  3. await()方法作为分割间,数量清零之后,才会执行下面的代码
     */
    CountDownLatch countDownLatch = new CountDownLatch(6);
    for (int i = 0; i < 6; i++) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"  ---->  go out");
            countDownLatch.countDown();
        },String.valueOf(i)).start();
    }
    System.out.println(Math.random());	//在await()方法之前,会插入到线程中间输出
    countDownLatch.await(); //等待计数器归零,才会向下执行

    System.out.println("执行 await() 之后的方法");
}

减法计数器,输出结果如下:
在这里插入图片描述

5.2 CyclicBarrier

一种同步辅助工具,允许一组线程彼此等待到达一个共同的障碍点。CyclicBarriers 在涉及固定大小的线程组的程序中非常有用,这些线程必须偶尔相互等待。该屏障被称为循环屏障,因为它可以在等待线程释放后重新使用

public static void main(String[] args) {
    /** 当计数器增加到设定的值,才会执行特定的代码
     *   1. 创建CyclicBarrier 对象,设定初始值和要执行的代码
     *   2. 在某个地方,使计数器增加
     *   3. 达到预设值,执行特定的代码
     *      例子:集齐七颗龙珠,召唤神龙
     */
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
        System.out.println("召唤神龙成功");
    });
    for (int i = 1; i <= 7; i++) {
        int temp = i;
        //lambda不能直接拿到for循环中的i
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "   收集--->" + temp);
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

输出结果如下:
在这里插入图片描述
【总结】CyclicBarrier 可以设置最终达到的数值(构造方法中),还能创建线程。CyclicBarrier 下的 await() 会等待其数值达到设定值,才会执行设定值旁边的Runnable线程,然后才是 await() 后面的代码。

5.3 Semaphore

参考博客:Semaphore用法
参考博客:Semaphore详解

计数信号灯。从概念上讲,信号量维护一组许可。如果需要,每个acquire()都会阻塞,直到获得许可证,然后再获取它。每个release()都会添加一个许可证,可能会释放一个阻塞的收单机构。但是,没有使用实际许可对象;信号机只对可用的数字进行计数,并相应地进行操作

public static void main(String[] args) {
    Semaphore semaphore = new Semaphore(2);// 每次可执行的线程数
    for (int i = 1; i <= 6; i++) {
        new Thread(()->{
            try {
                semaphore.acquire();	
                System.out.println(Thread.currentThread().getName() + " 抢到了车位");
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + " 离开了车位");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                semaphore.release();//释放
            }
        },String.valueOf(i)).start();
    }
}

输出结果如下:
在这里插入图片描述

6. ReadWriteLock

写锁writeLock().lock() 不允许插队;读锁 readLock().lock() 允许插队:

public class ReadWriteLockTest {
    //想要实现: 写入的时候,线程串行;读取的时候,线程并行
    public static void main(String[] args) {
        MyCatchLock myCatch = new MyCatchLock();
        //写入
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCatch.put(temp+"",temp+"");
            },String.valueOf(i)).start();
        }
        //读取
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                myCatch.get(temp+"");
            },String.valueOf(i)).start();
        }
    }
}

class MyCatch {
    private volatile Map<String,Object> map = new HashMap<>(0);
    //存
    public void put(String key,Object value) {
        System.out.println(Thread.currentThread().getName()+"写入"+value);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName()+"写入成功");
    }
    //取
    public void get(String key) {
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取成功");
    }
}
//加锁的
class MyCatchLock {
    private volatile Map<String,Object> map = new HashMap<>(0);
    //读写锁更加细粒度的控制
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock lock1 = new ReentrantLock();

    //写入时,只希望同时有一个线程写
    public void put(String key,Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+" 写入 ------> "+value);
            map.put(key, value);
            System.out.println(" ------------ "+Thread.currentThread().getName()+"写入成功 ");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
    //取读时,所有的人都可以读
    public void get(String key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"读取开始 ============");
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName()+"============ 读取成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
}

在这里插入图片描述

7. 队列

BolckingQueue

在这里插入图片描述
继承关系如下所示:
在这里插入图片描述

7.1 ArrayBlockingQueue

操作 ArrayBlockingQueue的四组API:

操作有返回值,抛出异常有返回值,不抛异常阻塞等待超时等待
添加boolean add(E e)boolean offer(E e)void put(E e)offer(E e, long timeout, TimeUnit unit)
移除E remove()E poll()E take()E poll(long timeout, TimeUnit unit)
获取队首元素E element()E peek()--
public class ArrayBlockingQueueTest {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test1() {
        //抛出异常
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        /*
         *如果队列已满,再调用 add() 方法,就会抛出异常 Queue full
         */
        //System.out.println(blockingQueue.add("d"));
        System.out.println("-------------------------");

        System.out.println(blockingQueue.element());    //获取队首元素并返回
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println("-------------------------");
        /*
         * 如果队列已空,再调用 remove() 、 element() ,
         * 就会抛出 java.util.NoSuchElementException异常
         */
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.element());
    }

    public static void test2() {
        //不抛出异常
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);//队列大小
        /** offer(E e) 向队列添加一个元素
         *   1. 如果添加成功,返回 true
         *   2. 如果队列已满、添加失败,返回 false (不抛异常)
         */
        System.out.println(blockingQueue.offer("a"));   // true
        System.out.println(blockingQueue.offer("b"));   // true
        System.out.println(blockingQueue.offer("c"));   // true
        System.out.println(blockingQueue.offer("d"));   // false

        System.out.println("-------------------------");
        System.out.println(blockingQueue.peek());//获取队首元素  输出 a

        System.out.println(blockingQueue.poll());   // a
        System.out.println(blockingQueue.poll());   // b
        System.out.println(blockingQueue.poll());   // c
        /* 队列已空
         *  1. poll() 获取并移除队首元素,返回 null
         *  2. peek() 获取队首元素,返回 null
         */
        System.out.println(blockingQueue.poll());   // null
        System.out.println(blockingQueue.peek());   // null

    }

    public static void test3() throws InterruptedException {
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);//队列大小
        /** 阻塞等待(一直等待)
         *  1. 队列已满,继续 put(E e),则队列一直等待
         *  2. 队列已空,继续 take() ,则队列一直等待 [需要捕获异常]
         */
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        //blockingQueue.put("d");
        System.out.println("-------------------------");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        //System.out.println(blockingQueue.take());
    }

    public static void test4() throws InterruptedException {
        /** 限时等待
         *  1. 队列已满,继续放入元素 offer(E e, long timeout, TimeUnit unit),则限时等待,返回 false
         *  2. 队列已空,继续获取元素 E poll() ,则限时等待,返回 null
         */
        ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("b", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("c", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));//这里会等两秒,超时就不管了直接跳过
        System.out.println("-------------------------");
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS));
    }
}

7.2 SychronizedQueue

同步队列】:没有容量,进去一个元素,必须等待取出来之后,才能往里面再放一个元素

public class SynchronousQueueTest {
    public static void main(String[] args) {
        SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"  put --->  1");
                synchronousQueue.put("1");
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"  put --->  2");
                synchronousQueue.put("2");
                TimeUnit.SECONDS.sleep(3);
                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();
    }
}

输出结果:
:有时候输出没按照下面的顺序,是因为(取出方法里面的输出,要拼接字符串比较慢,导致的)

t1  put --->  1
t2 取出------> 1
t1  put --->  2
t2 取出------> 2
t1  put --->  3
t2 取出------> 3

8. 线程池

  • 线程池的好处:降低资源消耗;提高响应速度;方便管理(控制最大线程数等)
  • 线程池:三大方法、七大参数、四大拒绝策略

8.1 三大方法

  • 阿里巴巴开发手册,不能使用 Executors 创建线程池:
    在这里插入图片描述
  • 使用Executors创建线程池的三大方法:
    • Executors.newSingleThreadExecutor(); //单个线程
    • Executors.newFixedThreadPool(5); //创建一个固定大小得线程池
    • Executors.newCachedThreadPool(); //可伸缩,线程数可变
public class ExecutorsTest {
    public static void main(String[] args) {
//        ExecutorService executorService = Executors.newSingleThreadExecutor();
//        ExecutorService executorService = Executors.newCachedThreadPool();
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        try {
            for (int i = 0; i < 100; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"      OK");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池用完,需要关闭
            executorService.shutdown();
        }
    }
}

三种创建线程池的方式,执行线程的时候,区别如下:
在这里插入图片描述

源码解析

在这里插入图片描述

8.2 七大参数

通过 Executors 工具类,创建线程池的三个方法,底层实现都是 new ThreadPoolExecutor() 方法,只是传参不同。再点击一层:
在这里插入图片描述
【举例子】去银行办理业务:

  1. 到银行之后,先去常开的窗口办理(初始化线程池的大小 corePoolSize)
  2. 常开窗口满了之后,会在候客区等待(阻塞队列 BlockingQueue)
  3. 候客区也满了,就会启用不常开的窗口(最大线程数 maixmumPoolSize)
  4. 常开窗口,多长时间不办理业务会关闭(keepAliveTime TimeUnit)
  5. 拒绝执行策略:四种,后续讲解
    在这里插入图片描述

8.3 四大拒绝策略

在这里插入图片描述

public class ThreadExecutorPoolTest {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,      //核心池大小: 初始化线程数
                5,  	//最大池大小: 最大线程数
                3,     //超出空闲时间,除了默认线程数,其他都释放
                TimeUnit.SECONDS,    //时间单位
                new LinkedBlockingDeque<>(3),  //提供等待区的队列
                Executors.defaultThreadFactory(),       //一般不会变,默认线程模式

                // ------------------------ 四种拒绝执行策略 : ------------------------
                //【默认】终止策略:最大线程数 + 等待队列都满了,还有线程创建,抛出异常 RejectedExecutionException
                //new ThreadPoolExecutor.AbortPolicy());

                /* 呼叫者运行策略: 最大线程数 + 等待队列都满了,新创建的线程,由调用改方法的线程执行
                 *哪来的回哪去(main处理)
                 */
                //new ThreadPoolExecutor.CallerRunsPolicy());

                //放弃策略: 会丢掉多余的任务,不执行、也不会抛出异常
                //new ThreadPoolExecutor.DiscardPolicy());

                //放弃最旧策略: 最大线程数 + 等待队列都满了,新创建的线程,会和最新创建的线程竞争
                new ThreadPoolExecutor.DiscardOldestPolicy());
        try {
            for (int i = 0; i < 9; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "    ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }
}

8.4 最大线程数如何定义

  1. CPU密集型 :几核,最大线程数(maixmumPoolSize)就是几,可以充分利用cpu
  2. IO 密集型 :判断程序中十分消耗IO的线程(num),最大线程数设置为两倍(2*num);或者可用核心数(计算资源)的两倍。
/** Runtime.getRuntime().availableProcessors(): 获取的是cpu核心线程数,也就是计算资源(不是物理内核)
 *  1. cpu密集型计算推荐设置线程池核心线程数为N,也就是和cpu的线程数相同,可以尽可能低避免线程间上下文切换。
 *  2. io密集型计算推荐设置线程池核心线程数为2N,但是这个数一般根据业务压测出来的,如果不涉及业务就使用推荐。
 */
int availableProcessors = Runtime.getRuntime().availableProcessors();

9. JDK8新特性

9.1 四大函数式接口

Function<T , R>

在这里插入图片描述

public class FunctionTest {
    public static void main(String[] args) {
        // 函数式接口
        Function<String, String> myFunction = new Function<String, String>(){
            @Override
            public String apply(String s) {
                return "hello, "+ s;
            }
        };
        //使用 leambda 表达式简化
        Function<String, String> myFunction2 = ((str)->{
            return "hello, "+ str +",to";
        });

        System.out.println(myFunction.apply("word!!"));  // 输出: hello, word!!
        System.out.println(myFunction2.apply("word!!")); // 输出: hello, word!!,to
    }
}

Predicate 断定型接口

在这里插入图片描述

public class PredicateTest {
    public static void main(String[] args) {
        Predicate<String> myPredicate = new Predicate<String>(){
            @Override
            public boolean test(String str) { // 判断字符串是否为空
                return str.isEmpty();
            }
        };
        // leambda 表达式简化
        Predicate<String> myPredicate2 = ((str)->{
            return str.isEmpty();
        });
        
        System.out.println(myPredicate.test("hello"));  //输出:  false
        System.out.println(myPredicate2.test(""));      //输出:  true
    }
}

Supplier 供给型接口

在这里插入图片描述

public class SupplierTest {
    public static void main(String[] args) {
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "hello , supplier!!!";
            }
        };
        // leambda表达式简化:
        Supplier<String> supplier2 = (()->{
            return "hi , supplier!!!";
        });

        System.out.println(supplier.get());     //输出: hello , supplier!!!
        System.out.println(supplier2.get());    //输出: hi , supplier!!!
    }
}

Consumer 消费型接口

在这里插入图片描述

public class ConsumerTest {
    public static void main(String[] args) {
        Consumer<String> myConsumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println("hello:  " + s);
            }
        };
        Consumer<String> myConsumer2 = ((str)->{
            System.out.println("hi:  " + str);
        });

        myConsumer.accept("法外狂徒-张三");   // 输出:  hello : 法外狂徒-张三
        myConsumer2.accept("法外狂徒-张三");  // 输出: hi :  法外狂徒-张三
    }
}

9.2 Stream流式计算

【推荐博客】:Stream流的常用方法
【推荐博客】:java8 Stream

public class StreamTest {
    public static void main(String[] args) {
        /*
            1、ID 必须是偶数
            2、龄必须大于23岁
            3、用户名转为大写字母
            4、用户名字母倒着排序
            5、只输出一个用户!
         */
        User u1 = new User(1, "a", 21);
        User u2 = new User(2, "b", 22);
        User u3 = new User(3, "c", 23);
        User u4 = new User(4, "d", 24);
        User u5 = new User(5, "e", 25);
        User u6 = new User(6, "f", 26);
        List<User> userList = Arrays.asList(u1, u2, u3, u4, u5, u6);
        userList.stream()
                .filter(u -> u.getId() % 2 == 0)  //id 为偶数
                .filter(u -> {return u.getAge() > 23;})     //年龄 > 23
                .map(u -> {return u.getName().toUpperCase();})     //用户名转大写字母
                .sorted((uu1,uu2)->{return uu2.compareTo(uu1);})   // 倒序
                .limit(2)       // 只输出 2 个
                .forEach(System.out::println);
    }
}

class User{
    private int id;
    private String name;
    private int age;
    // 省略 get/set 方法
}

9.3 ForkJoin

推荐博客:ForkJoinPool线程池的使用及原理

推荐博客:ForkJoinPool的使用及原理

在这里插入图片描述
ForkJoinPool的继承关系:
在这里插入图片描述

  • 通过 ForkJoinPool.execute(ForkJoinTask<?> task) 分段执行任务
  • 不需要返回值,传入 RecursiveAction
  • 需要返回值,传入 RecursiveTask
    在这里插入图片描述
    现在需要计算1~10_0000_0000的总和,所以继承 RecursiveTask 类
    在这里插入图片描述
    RecursiveTask 实现类如下:
public class MyRecursiveTask extends RecursiveTask<Long> {
    private Long start;     // 起始值
    private Long end;       // 终止值

    //临界值:分成多个任务,每次执行这么多
    private Long temp = 10000L;

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

    /**
     * 下面算法类似递归:
     *  判断长度是否超过临界值:如超过,则分割再次判断。。
     */
    @Override
    protected Long compute() {
        if ((end - start) > temp) {
            Long sum = 0L;
            for (long i = start; i < end; i++) {
                sum += i;
            }
            System.out.println(sum);
            return sum;
        } else {
            long middle = (start + end) / 2;
            MyRecursiveTask test1 = new MyRecursiveTask(start, middle);
            test1.fork();//拆分值,把任务压到线程队列
            MyRecursiveTask test2 = new MyRecursiveTask(middle+1, end);
            test2.fork();
            return test1.join() + test2.join();
        }
    }
}

测试类代码:分别测试 普通算法、ForkJoin、Stream并行流 的计算速度

public class ForkjoinTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test2();
    }

    /*
     *  普通算法 :
     *  sum = 499999999500000000  ----->  时间: 5863
     */
    public static void test1() {
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i < 10_0000_0000; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum = " + sum + "  ----->  时间: " + (end - start));
    }

    /*
     *  使用 FockJoin
     *  sum = 499999999500000000  ----->  时间: 3055
     */
    public static void test2() throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> demo18 = new MyRecursiveTask(1L, 10_0000_0000L);

        /*forkJoinPool.execute(demo18);//执行任务(无返回值)*/
        ForkJoinTask<Long> submit = forkJoinPool.submit(demo18);//提交任务(有返回值)

        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("sum = " + sum + "  ----->  时间: " + (end - start));
    }

    /**
     * Stream并行流
     * sum = 500000000500000000  ----->  时间: 179
     */
    public static void test3(){
        long start = System.currentTimeMillis();
        long sum = LongStream.rangeClosed(1L, 10_0000_0000L).parallel().reduce(0, Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("sum = " + sum + "  ----->  时间: " + (end - start));
    }
}

9.4 异步回调

  • FutureTest,分为:有返回值、无返回值:
public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /** runAsync(Runnable runnable)  无返回值
         *  1. 调用的时候,延迟3秒 打印信息
         *  2. 调用 get() 方法,执行线程
         */
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " ——————》 runAsync => 无返回值");
        });
        //completableFuture1.get();   // 获取阻塞结果。如果不调用,线程不执行
        System.out.println("this Thread is: ----> " + Thread.currentThread().getName());

        /** supplyAsync(Supplier<U> supplier) 有返回值
         *  1. 参数为 供给型接口
         *  2. 如果执行成功,返回正确的值 ; 否则,抛出异常
         */
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + " ——————》 supplyAsync => 有返回值");
            //int res = 1/0;
            return 1024;
        });
        /*
            执行成功: t --> 线程中的返回值 ; u --> null
            执行异常: t --> null        ; u --> 异常信息
         */
        System.out.println(completableFuture2.whenComplete((t, u) -> { //whenComplete成功回调
            System.out.println("t =>   " + t);
            System.out.println("u =>   " + u);
        }).exceptionally((e) -> {       //exceptionally 失败回调
            System.out.println(e.getMessage());
            int i = 233;
            return i;
        }).get());
    }
}
  • 输出结果:
    在这里插入图片描述

10 JMM & volatile

10.1 JMM

参考博客:JMM概述

参考文章:浅谈JMM和并发三大特性

JMM (Java Memory Mode) ,即Java内存模型。就是一种符合内存模型的机制及规范,屏蔽了各种硬件和操作系统的访问差异,保证了Java程序在各种平台下,对内存的访问都能达到一致的效果。

JMM 规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中,保存了该线程中使用到的变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步

JMM关于变量同步的约定

  1. 线程解锁前,必须把工作内存立刻刷回主存。
  2. 线程加锁前,必须读取主存中的最新值到工作内存中!
  3. 加锁和解锁是同一把锁!
    在这里插入图片描述

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

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

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操作之前,必须把此变量同步回主内存

JMM对这八种操作规则和 对volatile的一些特殊规则 就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

10.2 volatile

可见性

  • 加上volatile,线程能感知到主存中的变化
  • 不加volatile,线程无法感知
public class VolatileTest01 {
    //加了volatile可以保证可见性,不加进入死循环
    /*
     *   线程A读取主存中的 num=0 , 复制到工作内存中。直到 num != 0,才会跳出死循环
     *   1. 如果不加 volatile,主存中修改了 num = 1, 线程A感知不到,一直循环等待
     *   2. 加上 volatile,num = 1之后,线程A 感知到并跳出循环。
     */
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        //线程A: 如果num == 0,则线程陷入死循环,直至 num 的值改变
        new Thread(()->{
            while (num == 0) {

            }
            System.out.println(Thread.currentThread().getName() + " ------> 运行结束");
        },"线程A").start();
        // main 线程沉睡 1s ,修改值 num = 1
        TimeUnit.SECONDS.sleep(1);
        num = 1;
        System.out.println(Thread.currentThread().getName() + "线程 ======> " +num);
    }
}

不保证原子性

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

定义变量的时候,即使加上 volatile 关键字,也无法保证 针对变量的操作,是原子性的

public class VolatileTest02 {
    //定义变量,使用 volatile 关键字
    private volatile static int num=0;
    // 变量自增
    public  static void add(){
        num++;
    }
    public static void main(String[] args) {
        // 20个线程,每个自增 1000,理论上值应该为 2_0000
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            /* 如果线程数量 > 2,主线程就处于礼让状态
             *   - java默认至少两个线程:一是 main 线程,二是 GC线程
             *   - 当线程状态只剩两个,就说明其他线程执行完毕
             */
            Thread.yield();
        }
        System.out.println(num);   // 执行几次,结果分别是 19659 、19497、19366
    }
}

如下所示,num++ 被编译成class文件再反编译(javap -c xxx.class)回来,变成了三条指令。可以用atomic 包下的原子类,代替 int 或 Integer:
在这里插入图片描述
解决方法:

  • 对 add 方法,使用 synchronizelock 加锁
  • 使用原子类替换基本类型,比如 AtomicInteger
public class VolatileTest021 {
    // 使用AtomicInteger,代替 int 或 Integer
    private static AtomicInteger num = new AtomicInteger(0);
    public  static void add(){
        num.incrementAndGet();
    }
    public static void main(String[] args) {
        // 20个线程,每个自增 1000,理论上值应该为 2_0000
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(num);   // 执行几次结果都是 20000
    }
}

防止指令重排

参考博客:指令重排
参考博客:Java中的指令重排

指令重排 :程序指令的执行顺序,有可能和代码的顺序 不一致,这个过程就称之为指令重排。

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

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

happens-before规则

参考博客:happens-before规则
参考博客:happens-before规则及内存屏障

1)如果ActionAActionB属于同一个线程,那么就说明ActionA happens-before ActionB2)如果ActionA是unlock操作,而ActionB是lock操作,那么ActionA happens-before ActionB3)如果A是对volatile变量的写操作,ActionB是对同一个变量的读操作,那么ActionA happens-before ActionB4)线程的启动Action happens-before 该线程上的其他动作。
5)线程中任何Action都 happens-before 任何其他线程检测到该线程已经结束、Thread.join调用成功返回,Thread.isAlive返回false6)一个线程调用另一个线程的interrupt一定发生在另一个线程中断之前。
7)一个对象的构造函数结束一定发生在兑现finalizer之前。
8ActionA发生在ActionB之前,ActionB发生在ActionC之前,则ActionA一定发生在ActionC之前。
ActionA happends-before ActionB,记作hb(ActionAActionB)。

volatile可以避免指令重排:操作系统内存在内存屏障。
作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
在这里插入图片描述

11. CAS & 原子引用

11.1 CAS

CAScompareAndSwap (比较并交换)
以 AtomicInteger 为例,解释 CAS:

public static void main(String[] args) {
    //CAS   compareAndSet  :比较并交换
    AtomicInteger atomicInteger = new AtomicInteger(2020);
    /**
     * public final boolean compareAndSet(int expect, int update)
     *   - expect 期待 , update 更新
     *   - 如果期望达到了,那么就更新,否则不更新,CAS是CPU的并发原语
     */
    //如果期望值是2020,则更新为2021
    System.out.println(atomicInteger.compareAndSet(2020,2021)); //更新成功,输出:true
    System.out.println(atomicInteger.get());    // 输出: 2021
    //上一步更新为2021,下面代码更新失败
    System.out.println(atomicInteger.compareAndSet(2020,2023)); //更新失败,输出:false
    System.out.println(atomicInteger.get());    //输出: 2021
}

查看 AtomicInteger 自增1 的源码,如下所示:
在这里插入图片描述
在这里插入图片描述
compareAndSwapInt源码如下,是个native方法:
在这里插入图片描述

11.2 原子引用

ABA 问题

在这里插入图片描述

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

    //线程2 (捣乱的线程,修改值并修改回来)
    System.out.println(atomicInteger.compareAndSet(2020,2021)); //更新成功,输出:true
    System.out.println(atomicInteger.get());    // 输出: 2021
    System.out.println(atomicInteger.compareAndSet(2021,2020)); //更新成功,输出:true
    System.out.println(atomicInteger.get());    // 输出: 2020

    //线程1 (想要执行的线程)
    System.out.println(atomicInteger.compareAndSet(2020,2023)); //更新失败,输出:false
    System.out.println(atomicInteger.get());    //输出: 2021
}

虽然最终线程2确实执行成功,可但是我们不希望数据被人动过了还不知道。

使用 AtomicStampedReference 原子类 带有版本号时间戳,可以每次记录加一类似于乐观锁

public class ABATest2 {

    /*  AtomicStampedReference(V initialRef, int initialStamp)
     *  - initialRef 初始引用
     *  - initialRef 初始版本号
     */
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(10, 1);

    public static void main(String[] args) {
        /*
         * 先修改引用的值,再修改回来,但是版本号变化了
         */
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();//获取当前最新版本号
            System.out.println("a1  --版本号--->  "+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("---------------------------------------------");

            System.out.println("a2  --修改成功?--->  "+atomicStampedReference.compareAndSet(10, 12,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a2  --版本号--->  " + atomicStampedReference.getStamp());
            System.out.println("a2  --引用的值--->  "+atomicStampedReference.getReference());

            System.out.println("---------------------------------------------");

            System.out.println("a3  --修改成功?--->  "+atomicStampedReference.compareAndSet(12, 10,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a3  --版本号--->  " + atomicStampedReference.getStamp());
            System.out.println("a3  --引用的值--->  "+atomicStampedReference.getReference());
        },"a").start();


        /*
         *乐观锁原理相同
         * 如果版本号从读取开始,是否变化
         *  - 如果没变化,则修改引用的值
         *  - 如果变化,修改失败
         */
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println("b1  --版本号---》  "+stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("****************************************");
            
            System.out.println("b2  --修改成功?---》  " + atomicStampedReference.compareAndSet(10,18, stamp, stamp + 1));
            System.out.println("b2  --版本号---》  " + atomicStampedReference.getStamp());
            System.out.println("b2  --引用的值---》  "+atomicStampedReference.getReference());
        },"b").start();
    }
}

输出结果:

a1  --版本号--->  1
b1  --版本号---1
---------------------------------------------
a2  --修改成功?--->  true
a2  --版本号--->  2
a2  --引用的值--->  12
---------------------------------------------
a3  --修改成功?--->  true
a3  --版本号--->  3
a3  --引用的值--->  10
****************************************
b2  --修改成功?---false
b2  --版本号---3
b2  --引用的值---10

12 锁

12.1 公平、非公平锁

公平锁:线程执行顺序先来后到,不可以插队;
非公平锁:线程执行的时候,可以插队(默认
在这里插入图片描述

12.2 可重入锁

可重入锁,又叫递归锁:拿到外面的锁后,自动获取里面的锁
在这里插入图片描述

  • synchronized 版
public class Lock01 {
    public static void main(String[] args) {
        MobilePhone phone = new MobilePhone();
        // 线程A和线程B,锁的是同一个实例对象
        new Thread(()->{ phone.sms(); },"A").start();
        new Thread(()->{ phone.sms(); },"B").start();
    }
}

class MobilePhone {
    public synchronized void sms() {
        System.out.println(Thread.currentThread().getName() + "  ------>  sms");
        // 发完短信,沉睡两秒
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        call();
    }
    public synchronized void call() {
        System.out.println(Thread.currentThread().getName() + "  ======>  call");
    }
}

输出结果:
在这里插入图片描述

  • lock 版(输出结果与synchronized版相同)
public class Lock02 {
    public static void main(String[] args) {
        MobilePhone phone = new MobilePhone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }
}
class MobilePhone2 {
    Lock lock = new ReentrantLock();
    // 每个方法,加锁、解锁必须配对
    public void sms() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "  ------>  sms");
            TimeUnit.SECONDS.sleep(2);
            call();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void call() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "  ======>  call");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

12.3 自旋锁

参考博客:java中的自旋锁

参考博客:Java自旋锁的实现

在这里插入图片描述

利用CSA方式,实现自旋锁

public class SpinLockDemo {
    // 原子引用, 泛型 Thread 默认为空
    AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();

    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+" ------>  myLock");
        /* 自旋锁
         * 如果期望值(当前引用)为空,设置为当前线程,并跳出循环;
         * 如果期望值(当前引用)不为空,设置失败,并一直循环
         * -- 翻译: 如果当前线程不为空,CAS失败;因为取反,所以一直执行
         */
        Thread thread1 = atomicReference.get();
        while (!atomicReference.compareAndSet(null,thread)){

        }
    }
    /*解锁: 如果期望值(当前引用)为当前线程,CSA成功:将当前引用置位 null
            将当前引用置为 null 后, mylock 就可跳出 while 循环
     */
    public void myUnLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " ======>  myUnLock");
        atomicReference.compareAndSet(thread, null);
    }
}

利用两个线程测试:

public class SpinLockTest {
    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLockDemo.myUnLock();
            }
        },"A").start();

        // 沉睡 1 秒,保证 A 先拿到自旋锁
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinLockDemo.myUnLock();
            }
        },"B").start();
    }
}

输出结果
在这里插入图片描述
优点:
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快。

缺点:

如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

12.4 死锁

死锁代码举例:

public class DeadLockTest {
    public static void main(String[] args) {
        String strA = "意大利炮";
        String strB = "三发炮弹";

        new Thread(new MyThread(strA,strB)).start();
        new Thread(new MyThread(strB,strA)).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() {
        // 锁住lockA ,等待 lockB
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"   拿到:"+lockA+"   等待:"+lockB);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"   拿到:"+lockB+"   等待:"+lockA);
            }
        }
    }
}

在这里插入图片描述

死锁排查方法

jps -l 命令:定位进程号
在这里插入图片描述
jstack 进程号 命令:找到死锁问题
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值