JVM基础知识

JVM探究

请你谈谈对JVM的理解? java8虚拟机和之前的变化更新
什么是OOM(内存溢出)? 什么是栈溢出(stackOverFlowError)? 怎么分析?
JVM常用的调优参数有哪些?
内存快照如何抓取? 怎么分析Domp文件 ? 知道吗?
谈谈JVM中,类加载器你的认识?

1.jvm的位置

2.jvm的体系结构

3.类加载器

4.双亲委派机制

虚拟机自带的加载器

启动类 (根) 加载器

扩展类加载器

应用程序加载器

app—>扩展类(ext) ----> (启动类)根加载器(rt) —>

1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成, 一直向上委托, 直到启动类加载器(根加载器rt)
3.启动类加载器检查是否能够加载当前这个类, 能够加载就结束了, 使用当前加载器,否则抛出异常,通知子类加载器进行加载.
4.重复步骤3.

1.1 什么是双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

1.2 类加载器的类别

BootstrapClassLoader(启动类加载器)

c++编写,加载java核心库 java.*,构造ExtClassLoaderAppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)

java编写,加载扩展库,如classpath中的jrejavax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)

java`编写,加载程序所在的目录,如`user.dir`所在的位置的`class

CustomClassLoader(用户自定义类加载器)

java编写,用户自定义的类加载器,可加载指定路径的class文件

1.3 源码分析

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

1.4 委派机制的流程图

1.5 双亲委派机制的作用

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全

5.沙箱安全机制

6.Native*

Native 关键字 调用 本地方法栈----> JNI (java Native Interface本地方法接口) 调用外部语言程序

7.pc寄存器

8.方法区

Method Area 方法区

方法区是被所有线程共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

静态变量(static),常量(final),类信息(构造方法,接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关.

static ,final, Class , 常量池

9.栈

栈: 先进后出,后进先出

队列: 先进先出 (FIFO)

喝多了吐就是栈,吃多了拉就是队列

栈:栈内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题

一旦线程结束,栈就Over

栈:8大基本类型+ 对象引用+实例的方法

栈 + 堆 +方法区:交互关系

栈堆方法区交互

在内存中画出一个对象的实例化过程

10.三种JVM

11.堆

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的.

类加载器读取了类文件后,一般会把什么东西放到堆中? 类, 方法 , 常量 , 变量 , ~,保存我们所有引用类型的真实对象;

堆内存中还要细分为三个区域:

新生区 (伊甸园区) Young/New

养老区 old

永久区 Perm jdk8之后叫做 “元空间”

GC垃圾回收, 主要在伊甸园区和养老区~

假设内存满了,OOM内存不够了! java.lang.OutOfMemoryError: java heap space

1.新生区 (满了 轻GC)

  • 类 : 诞生和成长的地方 , 甚至死亡;

  • 伊甸园, 所有的对象都是在伊甸园区new出来的

  • 幸存区(0,1)

  • 经研究99%的对象都是临时对象

2.老年区 (满了 重GC)

永久区

这个区域常驻内存的. 用来存放jdk自身携带的Class对象.Interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭vm虚拟就会释放这个区域的内存~.

  1. jdk1.6之前 :永久代,常量池是在方法区;

  2. jdk1.7 :永久代,但是慢慢退化了, 去永久代,常量池在堆中;

  3. jdk1.8之后:无永久代, 常量池在元空间;

  1. 元空间:逻辑上存在,物理上不存在

堆内存调优

默认情况下: 分配的总内存 是电脑内存的1/4,而初始化的内存: 1/64

OOM:堆内存溢出

  • 尝试扩大堆内存看结果
    -Xms1024m -Xmx1024m -xx:+PrintGCDetails

  • 分析内存,看一下哪个地方出现了问题 (专业工具)

在一个项目中,突然出现了OOM故障,那么该如何排除~研究为什么出错

  • 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler

  • Dubug,一行行分析代码

    MAT,Jprogiler作用:

jprofiler使用

-Xms 设置初始化内存分配大小 1/64

-Xmx 设置最大分配内存,默认1/4

-XX:+HeapDumpOnOutOfMemoryError oomDump

12.GC:垃圾回收

GC:作用区域 堆+方法区

JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~

新生代
幸存区(form , to)
老年区
GC两种:轻GC(普通GC),重GC(全局GC)

GC题目

JVM的内存模型和分区~详细到每个区放什么?
堆里面的分区有哪些? Eden,form,to,老年区,说说他们的特点?
GC的算法有哪些? 标记清除法 , 标记压缩 , 复制算法, 引用计数器, 怎么用的?
轻GC和重GC分别在什么时候发生?

13.常用算法

  1. 引用计数法

  1. 复制算法

好处:没有内存的碎片
坏处:浪费了内存空间:多了一半空间永远是空 to.假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候:新生区;

  1. 标记清除算法

优点:不需要额外的空间
缺点:两次扫描,严重浪费时间,会产生内存碎片

  1. 标记压缩(优化标记清除)

三部曲:标记,清除.压缩
如何选择四种算法:
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)

内存整齐度:复制算法=标记压缩算法>标记清除算法

内存利用率:标记压缩算法=标记清除算法>复制算法

难道没有一个最优的算法吗?

答案:没有,没有最好的算法,只有最合适的算法---->GC:分带收集算法

年轻代:

存活率低
复制算法
老年代:

区域大:存活率
标记清除(内存碎片不是太多)+标记压缩混合实现

14.JMM

1.什么是JMM?

JMM :(java Memory Model 缩写)java内存模型

2.他是干什么的?

解决共享对象可见性的问题:volilate

3.他该如何学习

JMM :抽象的概念,理论

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

参考地址 周洋的面试题

https://blog.csdn.net/lookfordream_/category_7718088.html

15 volilate

1、概念

volatile关键字是为了保证线程安全,线程安全意味着一个方法或类实例可以被多个线程同时使用,而不会出现任何问题。它提供的功能主要有两点:

  • 保证可见性

  • 禁止指令重排序

  • 不保证原子性

不使用synchronized解决原子性可以使用java.util.concurrent.automic下的原子类AtomicInteger实现

使用下面的CAS实现

16 CAS底层原理

1、概念

CAS的全称是Compare-And-Swap,它是CPU并发原语

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。

2、代码使用

首先调用AtomicInteger创建了一个实例, 并初始化为5

// 创建一个原子类
AtomicInteger atomicInteger = new AtomicInteger(5);

然后调用CAS方法,企图更新成2019,这里有两个参数,一个是5,表示期望值,第二个就是我们要更新的值

atomicInteger.compareAndSet(5, 2019)

然后再次使用了一个方法,同样将值改成1024

atomicInteger.compareAndSet(5, 1024)

3、CAS底层原理

这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。
Unsafe类 + CAS思想: 也就是自旋,自我旋转

在本地方法栈去Native关键字调用c++的方法执行内存方法

底层汇编
Unsafe类中的compareAndSwapInt是一个本地方法,该方法的实现位于unsafe.cpp中

先想办法拿到变量value在内存中的地址
通过Atomic::cmpxchg实现比较替换,其中参数X是即将更新的值,参数e是原内存的值

4、CAS 缺点

CAS不加锁,保证一次性,但是需要多次比较

循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性

5、 总结

CAS
CAS是compareAndSwap,比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止

CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否者什么都不做

17、原子类AtomicInteger的ABA问题

1、连环套路

从AtomicInteger引出下面的问题

CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题

CAS导致ABA问题
CAS算法实现了一个重要的前提,需要取出内存中某时刻的数据,并在当下时刻比较并替换,那么这个时间差会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的

ABA问题
CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过
原子引用

原子引用其实和原子包装类是差不多的概念,就是将一个java类,用原子引用类进行包装起来,那么这个类就具备了原子性。

/**
 * 原子引用
 * @author:  
 * @create: 2020-03-11-22:12
 */

class User {
    String userName;
    int age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User z3 = new User("z3", 22);

        User l4 = new User("l4", 25);

        // 创建原子引用包装类
        AtomicReference<User> atomicReference = new AtomicReference<>();

        // 现在主物理内存的共享变量,为z3
        atomicReference.set(z3);

        // 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());

        // 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
    }
}
 

2、解决ABA问题

新增一种机制,也就是修改版本号,类似于时间戳的概念

如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路

AtomicStampedReference
时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

/**
 * ABA问题的解决,AtomicStampedReference
 * @author:  
 * @create: 2020-03-12-15:34
 */
public class ABADemo {

    /**
     * 普通的原子引用包装类
     */
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    // 传递两个值,一个是初始值,一个是初始版本号
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        System.out.println("============以下是ABA问题的产生==========");

        new Thread(() -> {
            // 把100 改成 101 然后在改成100,也就是ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保证t1线程,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 101 然后在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());

        }, "t2").start();

        System.out.println("============以下是ABA问题的解决==========");

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t3一秒钟
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 传入4个值,期望值,更新值,期望版本号,更新版本号
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());

        }, "t3").start();

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);

            System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp());

            System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());


        }, "t4").start();

    }
}

18、Collection线程不安全的举例

参考地址 https://blog.csdn.net/LookForDream_/article/details/105831364

ArrayList、HashSet、HashMap都是线程不安全的

多线程环境

为什么ArrayList是线程不安全的?因为进行写操作的时候,方法上为了保证并发性,没有添加sychronized修饰,所以并发性写的时候,就会出现问题。

解决方案

方案一:Vector

第一种方法,就是不用ArrayList这种不安全的List实现类。而采用Vector,线程安全的
关于Vector如何实现线程安全的,而方法上加了锁,即synchronized

这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,并导致并发性下降。

方案二:Collections.sychronized()

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

方案三:采用JUC里面的方法

CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想

查看底层add方法源码

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

19、Java 的锁

公平锁和非公平锁

概念

公平锁

是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列

非公平锁

是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)

如何创建

并发包中ReentrantLock的创建可以指定析构函数的boolean类型来得到公平锁或者非公平锁,默认是非公平锁

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

两者区别

公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列中的第一个,就占用锁,否者就会加入到等待队列中,以后安装FIFO的规则从队列中取到自己

非公平锁: 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

题外话

Java ReenttrantLock通过构造函数指定该锁是否公平,默认是非公平锁,因为非公平锁的优点在于吞吐量比公平锁大,对于synchronized而言,也是一种非公平锁.

可重入锁和递归锁ReentrantLock

概念

可重入锁就是递归锁
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁

也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块

ReentrantLock / Synchronized 就是一个典型的可重入锁.
可重入锁就是,在一个method1方法中加入一把锁,方法2也加锁了,那么他们拥有的是同一把锁.
也就是说我们只需要进入method1后,那么它也能直接进入method2方法,因为他们所拥有的锁,是同一把。

作用

可重入锁的最大作用就是避免死锁

自旋锁

概念

自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

原来提到的比较并交换,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。

优缺点

优点:循环比较获取直到成功为止,没有类似于wait的阻塞

缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源

独占锁(写锁) / 共享锁(读锁) / 互斥锁

概念

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可以被多个线程锁持有

对ReentrantReadWriteLock其读锁是共享,其写锁是独占

写的时候只能一个人写,但是读的时候,可以多个人同时读

为什么会有写锁和读锁

原来我们使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读

多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行,
但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写

读-读:能共存
读-写:不能共存
写-写:不能共存

20、CountDownLatch_CyclicBarrier_Semaphore

CountDownLatch

概念

让一些线程阻塞直到另一些线程完成一系列操作才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程就会被阻塞。其它线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会被阻塞),当计数器的值变成零时,因调用await方法被阻塞的线程会被唤醒,继续执行

场景

现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的

package com.moxi.interview.study.thread;

import java.util.concurrent.CountDownLatch;

/**
 * @author: 
 * @create: 2020-04-15-19:03
 */
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
    }
}
 

CyclicBarrier

概念

和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0,加到某个值的时候就执行

CyclicBarrier的字面意思就是可循环(cyclic)使用的屏障(Barrier)。它要求做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await方法

案例

集齐7个龙珠,召唤神龙的Demo,我们需要首先创建CyclicBarrier

/**
 * CyclicBarrier循环屏障
 *
 * @author: 轻狂书生FS
 * @create: 2020-04-16-14:40
 */
public class CyclicBarrierDemo {


    public static void main(String[] args) {
        /**
         * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });

        for (int i = 0; i < 7; i++) {
            final Integer tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");

                try {
                    // 先到的被阻塞,等全部线程完成后,才能执行方法
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}
 

Semaphore:信号量

概念

信号量主要用于两个目的:

  • 一个是用于共享资源的互斥使用
  • 另一个用于并发线程数的控制

代码

我们模拟一个抢车位的场景,假设一共有6个车,3个停车位
那么我们首先需要定义信号量为3,也就是3个停车位

/**
 * 信号量Demo
 * @author: 轻狂书生FS
 * @create: 2020-03-16-15:01
 */
public class SemaphoreDemo {

    public static void main(String[] args) {

        /**
         * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
         */
        Semaphore semaphore = new Semaphore(3, false);

        // 模拟6部车
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    // 代表一辆车,已经占用了该车位
                    semaphore.acquire(); // 抢占

                    System.out.println(Thread.currentThread().getName() + "\t 抢到车位");

                    // 每个车停3秒
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "\t 离开车位");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放停车位
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}
 

21、阻塞队列

概念

队列

队列就可以想成是一个数组,从一头进入,一头出去,排队买饭

阻塞队列

BlockingQueue 阻塞队列,排队拥堵,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:

线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当蛋糕店的柜子空的时候,无法从柜子里面获取蛋糕
当阻塞队列是满时,从队列中添加元素的操作将会被阻塞
当蛋糕店的柜子满的时候,无法继续向柜子里面添加蛋糕了
也就是说 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其它线程往空的队列插入新的元素
同理,试图往已经满的阻塞队列中添加新元素的线程,直到其它线程往满的队列中移除一个或多个元素,或者完全清空队列后,使队列重新变得空闲起来,并后续新增

为什么要用?

去海底捞吃饭,大厅满了,需要进候厅等待,但是这些等待的客户能够对商家带来利润,因此我们非常欢迎他们阻塞
在多线程领域:所谓的阻塞,在某些清空下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动唤醒

为什么需要BlockingQueue

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都帮你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己取控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

BlockingQueue阻塞队列是属于一个接口,底下有七个实现类

ArrayBlockQueue:由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
有界,但是界限非常大,相当于无界,可以当成无界
PriorityBlockQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
生产一个,消费一个,不存储元素,不消费不生产
LinkedTransferQueue:由链表结构组成的无界阻塞队列
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
这里需要掌握的是:ArrayBlockQueue、LinkedBlockingQueue、SynchronousQueue

BlockingQueue核心方法

SynchronousQueue

SynchronousQueue没有容量,与其他BlockingQueue不同,SynchronousQueue是一个不存储的BlockingQueue,每一个put操作必须等待一个take操作,否者不能继续添加元素

阻塞队列的用处

生产者消费者模式

一个初始值为0的变量,两个线程对其交替操作,一个加1,一个减1,来5轮

关于多线程的操作,我们需要记住下面几句

  • 线程 操作 资源类
  • 判断 干活 通知
  • 防止虚假唤醒机制
    我们下面实现一个简单的生产者消费者模式,首先有资源类ShareData

我们能够发现,线程t3,在进行ABA操作后,版本号变更成了3,而线程t4在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样

22、synchronized和lock的区别

synchronize和Lock有什么区别?用新的Lock有什么好处?请举例说明

1、原始构成

  • synchronized是关键字属于JVM的层面

    使用monitorenter

    monitorexit

  • Lock是具体类(java.until.concurrent.Locks.Lock)是api层面的锁

2、使用方法

  • synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
  • ReentrantLock 则需要用户去手动释放锁,若没有主动释放锁,就有可能导致死锁现象

3、等待是否可中断

  • synchronized不可中断,除非抛出异常或者正常运行结束
  • ReentrantLock可以中断 1、设置超时方法 tryLock(Long timeout, TimeUnit unit) 2、LockInterUptibly ()放到代码块中,调用interrrupt 方法可中断

4、加锁是否公平锁

  • synchronzed 非公平锁
  • ReentrantLock 两者都可以,默认公平锁,构造参数传参 true false

5、锁绑定多个条件Condition

  • synchronized 没有
  • ReentrantLock 用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程

23、HashMap底层实现原理

底层是基于数组+链表+红黑色实现

它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

HashMap 是基于 hashing 的原理

我们使用 put(key, value) 存储对象到 HashMap 中,使用 get(key) 从 HashMap 中获取对象。当我们给 put() 方法传递键和值时,我们先对键调用 hashCode() 方法,计算并返回的 hashCode 是用于找到 Map 数组的 bucket 位置来储存 Node 对象。

这里关键点在于指出,HashMap 是在 bucket 中储存键对象和值对象,作为Map.Node 。

Node[] table = new Node[16]; // 散列桶初始化,table 第一次put的时候创建
class Node {
    hash; //hash值
    key; //键
    value; //值
    node next; //用于指向链表的下一层(产生冲突,用拉链法)
}

以下是具体的 put 过程(JDK1.8)

1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)

2.如果散列表为空时,调用resize()初始化散列表

3.如果没有发生碰撞,直接添加元素到散列表中去

4.如果发生了碰撞(hashCode值相同),进行三种判断

4.1:若key地址相同或者equals后内容相同,则替换旧值

4.2:如果是红黑树结构,就调用树的插入方法

4.3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。链表长度低于6,就把红黑树转回链表 使用尾插法

5.如果桶满了大于阀值(容量16*加载因子0.75),则resize进行扩容(扩容2倍后重排)

以下是具体 get 过程

对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。

http://www.codeinn.net/misctech/1718.html

https://www.cnblogs.com/zengcongcong/p/11295349.html

24、ConcurrentHashMap

JDK 1.7 是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

   /**
   * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。
     */
   final Segment<K,V>[] segments;

  transient Set<K> keySet;
   transient Set<Map.Entry<K,V>> entrySet;

和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

Put

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. 最后会解除在 1 中所获取当前 Segment 的锁。

get 逻辑比较简单:

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

JDK1.8 后的更改

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。

那就是查询遍历链表效率太低。

因此 1.8 做了一些数据结构上的调整。

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

Put

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

Get

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

25、ThreadLocal

https://www.cnblogs.com/fsmly/p/11020641.html

线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每个线程创建一个单独的变量副本,每个线程都可以改变自己的变量副本而不影响其它线程所对应的副本。

1、ThreadLocal简介

  多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

img

2、ThreadLocal简单使用

 下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

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

下面是运行后的结果:

img

3、ThreadLocal的实现原理

  下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为nullimgimg,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的

img

1、set方法源码

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}

如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示

1 void createMap(Thread t, T firstValue) {
2     t.threadLocals = new ThreadLocalMap(this, firstValue);
3 }

 createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

 2、get方法源码

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

 3、remove方法的实现

  remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

img

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

4、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

1、基础概念

  首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章(JVM相关)

①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

img

2、分析ThreadLocalMap内部实现

  上面我们知道ThreadLocalMap内部实际上是一个Entry数组img,我们先看看Entry的这个内部类

/**
 * 是继承自WeakReference的一个类,该类中实际存放的key是
 * 指向ThreadLocal的弱引用和与之对应的value值(该value值
 * 就是通过ThreadLocal的set方法传递过来的值)
 * 由于是弱引用,当get方法返回null的时候意味着坑能引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** value就是和ThreadLocal绑定的 */
    Object value;

    //k:ThreadLocal的引用,被传递给WeakReference的构造方法
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}

//Reference构造方法
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

  考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

  总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

26、线程池

1、前言

获取多线程的方法,我们都知道有三种,还有一种是实现Callable接口

  • 实现Runnable接口
  • 实现Callable接口
  • 实例化Thread类
  • 使用线程池获取

2、Callable接口

Callable接口,是一种让线程执行完成后,能够返回结果的
在说到Callable接口的时候,我们不得不提到Runnable接口

/**
 * 实现Runnable接口
 */
class MyThread implements Runnable { 
    @Override
    public void run() {

    }
}

我们知道,实现Runnable接口的时候,需要重写run方法,也就是线程在启动的时候,会自动调用的方法

同理,我们实现Callable接口,也需要实现call方法,但是这个时候我们还需要有返回值,这个Callable接口的应用场景一般就在于批处理业务,比如转账的时候,需要给一会返回结果的状态码回来,代表本次操作成功还是失败

/**
 * Callable有返回值
 * 批量处理的时候,需要带返回值的接口(例如支付失败的时候,需要返回错误状态)
 *
 */
class MyThread2 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("come in Callable");
        return 1024;
    }
}

最后我们需要做的就是通过Thread线程, 将MyThread2实现Callable接口的类包装起来

这里需要用到的是FutureTask类,他实现了Runnable接口,并且还需要传递一个实现Callable接口的类作为构造函数

//  FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
// 这里通过了FutureTask接触了Callable接口
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());

然后在用Thread进行实例化,传入实现Runnabnle接口的FutureTask的类

Thread t1 = new Thread(futureTask, "aaa");
t1.start();

后通过 futureTask.get() 获取到返回值

// 输出FutureTask的返回值
System.out.println("result FutureTask " + futureTask.get());

这就相当于原来我们的方式是main方法一条龙之心,后面在引入Callable后,对于执行比较久的线程,可以单独新开一个线程进行执行,最后在进行汇总输出

最后需要注意的是 要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致阻塞,直到计算完成

img
也就是说 futureTask.get() 需要放在最后执行,这样不会导致主线程阻塞

也可以使用下面算法,使用类似于自旋锁的方式来进行判断是否运行完毕

// 判断futureTask是否计算完成
while(!futureTask.isDone()) {

}

3、注意

多个线程执行 一个FutureTask的时候,只会计算一次

FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());

// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask, "BBB").start();

如果我们要两个线程同时计算任务的话,那么需要这样写,需要定义两个futureTask

FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread2());

// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();

new Thread(futureTask2, "BBB").start();

4、ThreadPoolExecutor

为什么用线程池

线程池做的主要工作就是控制运行的线程的数量,处理过程中,将任务放入到队列中,然后线程创建后,启动这些任务,如果线程数量超过了最大数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用、控制最大并发数、管理线程

线程池中的任务是放入到阻塞队列中的

线程池的好处

多核处理的好处是:省略的上下文的切换开销

原来我们实例化对象的时候,是使用 new关键字进行创建,到了Spring后,我们学了IOC依赖注入,发现Spring帮我们将对象已经加载到了Spring容器中,只需要通过@Autowrite注解,就能够自动注入,从而使用

因此使用多线程有下列的好处

1、降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
2、提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
3、提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

架构说明

Java中线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(代表工具类),ExecutorService,ThreadPoolExecutor这几个类。

img

创建线程池

1、Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池
执行长期的任务,性能好很多
创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待
2、Executors.newSingleThreadExecutor:创建一个只有1个线程的 单线程池
一个任务一个任务执行的场景
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行

3、Executors.newCacheThreadPool(); 创建一个可扩容的线程池
执行很多短期异步的小程序或者负载教轻的服务器
创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
具体使用,首先我们需要使用Executors工具类,进行创建线程池,这里创建了一个拥有5个线程的线程池

具体使用,首先我们需要使用Executors工具类,进行创建线程池,这里创建了一个拥有5个线程的线程池

// 一池5个处理线程(用池化技术,一定要记得关闭)
ExecutorService threadPool = Executors.newFixedThreadPool(5); 
// 创建一个只有一个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor(); 
// 创建一个拥有N个线程的线程池,根据调度创建合适的线程
ExecutorService threadPool = Executors.newCacheThreadPool(); 

然后我们执行下面的的应用场景

##模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
我们需要使用 threadPool.execute执行业务,execute需要传入一个实现了Runnable接口的线程
    
threadPool.execute(() -> {
	System.out.println(Thread.currentThread().getName() + "\t 给用户办理业务");
});
然后我们使用完毕后关闭线程池
 threadPool.shutdown();

/**
 * 第四种获取 / 使用 Java多线程的方式,通过线程池
 * @author: 轻狂书生FS
 * @create: 2020-04-17-15:59
 */
public class MyThreadPoolDemo {
    public static void main(String[] args) {

        // Array  Arrays(辅助工具类)
        // Collection Collections(辅助工具类)
        // Executor Executors(辅助工具类)


        // 一池5个处理线程(用池化技术,一定要记得关闭)
        ExecutorService threadPool = Executors.newFixedThreadPool(5);

        // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
        try {

            // 循环十次,模拟业务办理,让5个线程处理这10个请求
            for (int i = 0; i < 10; i++) {
                final int tempInt = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

最后结果:
pool-1-thread-1	 给用户:0 办理业务
pool-1-thread-5	 给用户:4 办理业务
pool-1-thread-1	 给用户:5 办理业务
pool-1-thread-4	 给用户:3 办理业务
pool-1-thread-2	 给用户:1 办理业务
pool-1-thread-3	 给用户:2 办理业务
pool-1-thread-2	 给用户:9 办理业务
pool-1-thread-4	 给用户:8 办理业务
pool-1-thread-1	 给用户:7 办理业务
pool-1-thread-5	 给用户:6 办理业务
我们能够看到,一共有5个线程,在给10个用户办理业务

5、底层实现

我们通过查看源码,点击了Executors.newSingleThreadExecutor 和 Executors.newFixedThreadPool能够发现底层都是使用了ThreadPoolExecutor

img
我们可以看到线程池的内部,还使用到了LinkedBlockingQueue 链表阻塞队列
同时在查看Executors.newCacheThreadPool 看到底层用的是 SynchronousBlockingQueue阻塞队列
最后查看一下,完整的三个创建线程的方法

我们可以看到线程池的内部,还使用到了LinkedBlockingQueue 链表阻塞队列
同时在查看Executors.newCacheThreadPool 看到底层用的是 SynchronousBlockingQueue阻塞队列
最后查看一下,完整的三个创建线程的方法
img

线程池的重要参数

线程池在创建的时候,一共有7大参数

corePoolSize:核心线程数,线程池中的常驻核心线程数
在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1相当有扩容后的线程数,这个线程池能容纳的最多线程数
keepAliveTime:多余的空闲线程存活时间
当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
unit:keepAliveTime的单位
workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
LinkedBlockingQueue:链表阻塞队列
SynchronousBlockingQueue:同步阻塞队列
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的Runnable的策略

当营业窗口和阻塞队列中都满了时候,就需要设置拒绝策略

在这里插入图片描述

拒绝策略

以下所有拒绝策略都实现了RejectedExecutionHandler接口

AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

线程池底层工作原理

线程池运行架构图

在这里插入图片描述

文字说明

1、在创建了线程池后,等待提交过来的任务请求

2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断

​ 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
​ 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
​ 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
​ 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
3、当一个线程完成任务时,它会从队列中取下一个任务来执行

4、当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:

​ 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
​ 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
以顾客去银行办理业务为例,谈谈线程池的底层工作原理

最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理
后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待
后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题
假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略
临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数

为什么不用默认创建的线程池?

线程池创建的方法有:固定数的,单一的,可变的,那么在实际开发中,应该使用哪个?

我们一个都不用,在生产环境中是使用自己自定义的

为什么不用Executors中JDK提供的?

根据阿里巴巴手册:并发控制这章

线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
线程池不允许使用Executors去创建,而是通过ThreadToolExecutors的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象弊端如下:
FixedThreadPool和SingleThreadPool:
运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
CacheThreadPool和ScheduledThreadPool
运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

手写线程池

采用默认拒绝策略
从上面我们知道,因为默认的Executors创建的线程池,底层都是使用LinkBlockingQueue作为阻塞队列的,而LinkBlockingQueue虽然是有界的,但是它的界限是 Integer.MAX_VALUE 大概有20多亿,可以相当是无界的了,因此我们要使用ThreadPoolExecutor自己手动创建线程池,然后指定阻塞队列的大小

下面我们创建了一个 核心线程数为2,最大线程数为5,并且阻塞队列数为3的线程池

// 手写线程池
       final Integer corePoolSize = 2;
       final Integer maximumPoolSize = 5;
       final Long keepAliveTime = 1L;

       // 自定义线程池,只改变了LinkBlockingQueue的队列大小
       ExecutorService executorService = new ThreadPoolExecutor(
               corePoolSize,
               maximumPoolSize,
               keepAliveTime,
               TimeUnit.SECONDS,
               new LinkedBlockingQueue<>(3),
               Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy());

然后使用for循环,模拟10个用户来进行请求

    // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
        try {

            // 循环十次,模拟业务办理,让5个线程处理这10个请求
            for (int i = 0; i < 10; i++) {
                final int tempInt = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        } 

但是在用户执行到第九个的时候,触发了异常,程序中断

pool-1-thread-1	 给用户:0 办理业务
pool-1-thread-4	 给用户:6 办理业务
pool-1-thread-3	 给用户:5 办理业务
pool-1-thread-2	 给用户:1 办理业务
pool-1-thread-2	 给用户:4 办理业务
pool-1-thread-5	 给用户:7 办理业务
pool-1-thread-4	 给用户:2 办理业务
pool-1-thread-3	 给用户:3 办理业务
java.util.concurrent.RejectedExecutionException: Task com.moxi.interview.study.thread.MyThreadPoolDemo$$Lambda$1/1747585824@4dd8dc3 rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 5, active threads = 3, queued tasks = 0, completed tasks = 5]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.moxi.interview.study.thread.MyThreadPoolDemo.main(MyThreadPoolDemo.java:34)

这是因为触发了拒绝策略,而我们设置的拒绝策略是默认的AbortPolicy,也就是抛异常的

触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会抛出异常从而报错退出。

1、拒绝策略是默认的AbortPolicy,也就是抛异常的 直接抛出异常

2、当我们更好其它的拒绝策略时,采用CallerRunsPolicy拒绝策略,也称为回退策略,就是把任务丢回原来的请求开启线程着,我们看运行结果

3、采用DiscardPolicy拒绝策略会,线程池会自动把后面的任务都直接丢弃,也不报异常,当任务无关紧要的时候,可以采用这个方式

4、采用DiscardOldestPolicy拒绝策略,这个策略和刚刚差不多,会把最久的队列中的任务替换掉

线程池的合理参数

生产环境中如何配置 corePoolSize 和 maximumPoolSize

这个是根据具体业务来配置的,分为CPU密集型和IO密集型

CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)

而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

CPU密集型任务配置尽可能少的线程数量:

一般公式:CPU核数 + 1个线程数

IO密集型
由于IO密集型任务线程并不是一直在执行任务,则可能多的线程,如 CPU核数 * 2

IO密集型,即该任务需要大量的IO操作,即大量的阻塞

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上

所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集时,大部分线程都被阻塞,故需要多配置线程数:

参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右

例如:8核CPU:8/ (1 - 0.9) = 80个线程数

27、Hystrix

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,已提高分布式系统的弹性。

“ 断路器 ” 本身是一种开关装置,当某个服务单元发送故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

Hystrix重要概念:

1.服务降级:

服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
哪些情况会发出降级?

程序运行异常
超时
服务熔断触发服务降级
线程池 / 信号量打满也会导致服务降级
2.服务熔断

类似保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示

3.服务限流

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行

配置项目:

需要在启动类配置的注解为:

@EnableHystrix
业务代码为:
    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallBackMethod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    })
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfo_TimeOut(id);
    }

配置的文件为application.yml:

feign:
 hystrix:
   enabled: true

一般都是在消费端配置断路器,因为不用来回传值。

服务熔断

image-20210517105350856

image-20210517110144433

28、spring循环依赖

单例模式(singleton)的情况下spring使用三级缓存解决循环依赖的问题

非单例模式(原型模式 propryten)编译是会报错依赖异常


29、Spring5.0 SpringBoot2以上的AOP执行顺序

**正常情况 ** around-----before------afterreturnning------after---------around

异常情况 around------before--------afterthrowing-----after

30、AQS

抽象的队列式的同步器,使用volatile的stateFIFO的队列维护线程,将每一个线程维护成node节点,node节点结构waitstate、前后首尾节点,CAS自旋获取是否占用,默认有一个傀儡节点是头结点,由前一个节点通知后一个节点,并释放state。

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

关键字 :

volatile的 State 状态

FIFO的队列

node节点

CAS自旋

模板方法模式

posted @ 2021-04-26 08:55  风雨无阻415  阅读(130)  评论(0编辑  收藏  举报