Skip to content

Java

饿汉式单例模式

java
public class Singleton {
	//定义一个私有的静态的 Singleton 变量,并进行初始化赋值(创建一个对象给变量赋值)
    private static Singleton singleton = new Singleton();
    //私有空参数构造方法,不让用户直接创建对象
    private Singleton(){}
    //定义一个公共的静态方法,返回 Singleton 对象
    public static Singleton getInstance(){
        return singleton;
    }
}

懒汉式

就是在使用时创建一个单例

image-20230818112542339

List面试题

数组(Array)是一种用连续的内存空间存储相同数据类型数据的线性数据结构。

ArrayList底层的实现原理是什么?

  • ArrayList底层是用动态的数组实现的
  • ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
  • ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
  • ArrayList在添加数据的时候
    • 确保数组已使用长度(size)加1之后足够存下下一个数据
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
    • 返回添加成功布尔值。

ArrayList list=new ArrayList(10)中的list扩容几次

该语句只是声明和实例了一个ArrayList,指定了容量为10,未扩容

面试官再问:

  • 用Arrays.asList转List后,如果修改了数组内容,list受影响吗List
  • 用toArray转数组后,如果修改了List内容,数组受影响吗

再答:

  • Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
  • list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层是它是进行了数组的拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响

ArrayList和LinkedList的区别是什么?

  1. 底层数据结构 ArrayList是动态数组的数据结构实现LinkedList是双向链表的数据结构实现

  2. 操作数据效率

  • ArrayList按照下标查询的时间复杂度O(1)【内存是连续的,根据寻址公式】,LinkedList不支持下标查询
  • 查找((未知索引): ArrayList需要遍历,链表也需要链表,时间复杂度都是O(n)
  • 新增和删除 ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)LinkedList头尾节点增删时间复杂度是O1),其他都需要遍历链表,时间复杂度是O(n)
  1. 内存空间占用 ArrayList底层是数组,内存连续,节省内存 LinkedList是双向链表需要存储数据,和两个指针,更占用内存

  2. 线程安全 ArrayList和LinkedList都不是线程安全的 如果需要保证线程安全,

    有两种方案:●在方法内使用,局部变量则是线程安全的●使用线程安全的ArrayList和LinkedList

LinkedList

HashMap实现原理

HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树

  1. 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。 a.如果key相同,则覆盖原始值; b.如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中。

HashMap的Push方法

第一次添加数据的流程

image-20230818172327419

完整的添加流程

image-20230818172522649

  1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
  2. 根据键值key计算hash值得到数组索引
  3. 判断table[i]==null,条件成立,直接新建节点添加
  4. 如果table[i]!=null ,不成立 4.1判断table[i]的首个元素是否和key一样,如果相同直接覆盖value 4.2判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对 4.3遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
  5. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容。

HashMap扩容流程

image-20230818174309733

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度*0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用e.hash & (newCap - 1)计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加入
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为O,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

HashMap的寻址算法

线程

线程创建方式

  • 继承Thread方法。

线程与进程的区别

进程是操作系统调度和分配资源的最小单位

线程是CPU调度和执行的最小单位。

程序由指令数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。

在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU 执行一个进程之内可以分为一到多个线程。

image-20230819144948550

二者对比

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

并行和并发区别

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

单核cpu

  • 单核CPU下线程实际还是串行执行的
  • 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒))分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。
  • 总结为一句话就是:微观串行,宏观并行

一般会将这种线程轮流使用CPU的做法称为并发(concurrent)

现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
  • 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

创建线程的方式有哪些?

一共有4种方式创建线程

  • 继承Thread类
  • 实现runnable接口
  • 实现Callable接口
  • 线程池创建线程
java
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("My Thread...run...");
    }

    public static void main(String[] args) {
//        ! / 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();//调用start方法启动线程
        t1.start();
        t2.start();
    }
}

第二种

java
public class MyRunable implements Runnable{
    @Override
    public void run() {
        System.out.println("ruanble");
    }


    public static void main(String[] args) {
        MyRunable able1 =new MyRunable();
//        MyRunable able2 =new MyRunable();


        Thread t1= new Thread(able1);
        Thread t2 =new Thread(able1);
        
        t1.start();
        t2.start();
//        able1.run();
//        able2.run();
    }
}

第三种

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

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return "ok ";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyCallable对象
        MyCallable mc = new MyCallable();
        // 创建FutureTask
        FutureTask<String> ft = new FutureTask<String>(mc); //创建Thread对象
        Thread t1 = new Thread(ft);
        Thread t2 = new Thread(ft);//  使用start方法启动线程

        t1.start();
//        t2.start();
        //调用代码的get方法获取执行结果
        String result = ft.get();
         //输出
        System.out.println(result);
    }
}

最后一种

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

public class MyExecu implements  Runnable{
    @Override
    public void run() {
        System.out.println("开心");
    }


    public static void main(String[] args) {
        ExecutorService threadpool= Executors.newFixedThreadPool(3);
        threadpool.submit(new MyExecu());
        threadpool.shutdown();
    }
}

Runnable和Callable之间区别

  1. Runnable接口run方法没有返回值
  2. Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  3. Callable接口的call)方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

run()和start()有什么区别?

  • start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。
  • run():封装了要被线程执行的代码,可以被调用多次。

线程有哪些状态

一共6种状态

调用了start方法,线程才进入就绪阶段。

//新生 NEW, //运行​ RUNNABLE, //阻塞​ BLOCKED, //等待,死死地等​ WAITING, //超时等待​ TIMED_WAITING, //终止​ TERMINATED;

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
    • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态

image-20230819161420449

下面是代码

java
public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called {@code Object.wait()}
         * on an object is waiting for another thread to call
         * {@code Object.notify()} or {@code Object.notifyAll()} on
         * that object. A thread that has called {@code Thread.join()}
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

举个例子,正在买票

java
//多线程模拟售票问题

import java.util.concurrent.TimeUnit;

//资源类
class Ticket {
    private int num = 100;//总共100张票

    public void sell() throws InterruptedException {
        while (num > 0) {
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + (num--));
            //为了创造一些异常,让线程到此处sleep阻塞一下1
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

public class Windows {
    public static void main(String[] args) {
        //创建三个线程,线程操纵资源类
        Ticket ticket = new Ticket();
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                try {
                    ticket.sell();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "售票员" + String.valueOf(i)).start();
        }
    }
}

我们可以看到一下情况,出现重复票号

image-20230819160659100

如何解决此类,多线程操作同一数据的问题?

解决办法①synchronized关键字②lock锁

Synchronized关键字

java
class Ticket {
    private int num = 100;//总共100张票

    public synchronized void sell() throws InterruptedException {//此时用synchronized修饰方法sell(),其同步监视器是:this(当前类的对象),由于使用的是
        while (num > 0) {
            System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + (num--));//为了创造一些异常,让线程到此处sleep阻塞一下
            TimeUnit.MILLISECONDS.sleep(10);
        }
    }
}

Lock锁

java
class Ticket {
    private int num = 100;//总共100张票
    Lock lock = new ReentrantLock();//ReentrantLock:lock接口的实现类之一

    public void sell() throws InterruptedException {
        try {
            lock.lock();
            while (num > 0) {
                System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + (num--));//为了创造一些异常,让线程到此处sleep阻塞一下
                TimeUnit.MILLISECONDS.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

新建线程,如何保证顺序执行

可以使用线程中的join方法解决

join() 等待线程运行结束

notify()和notifyAll()有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个wait线程

java中wait和sleep方法的不同?

共同点 wait(), wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

不同点

  1. 方法归属不同 sleep(long)是Thread的静态方法 而wait(),wait(long)都是Object的成员方法,每个对象都有

  2. 醒来时机不同 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来 wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去

    它们都可以被打断唤醒

  3. 锁特性不同(重点) wait方法的调用必须先获取wait对象的锁,而sleep 则无此限制 wait方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃cpu,但你们还可以用)而sleep如果在synchronized 代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)

如何停止一个正在运行的线程?

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep,wait,join )的线程,线程会抛出InterruptedException异常
    • 打断正常的线程,可以根据打断状态来标记是否退出线程

Synchronized 关键字的底层原理

请添加图片描述

特性

  • 原子性:synchronized保证语句块内操作是原子的

    • 同步方法 ACC_SYNCHRONIZED 这是一个同步标识,对应的 16 进制值是 0x0020 这 10 个线程进入这个方法时,都会判断是否有此标识,然后开始竞争 Monitor 对象。
    • 同步代码  monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法 的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。  monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。
  • 可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现) 那么为什么添加 synchronized 也能保证变量的可见性呢? 因为:

    1. 线程解锁前,必须把共享变量的最新值刷新到主内存中。
    2. 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存 中重新读取最新的值。
    3. volatile 的可见性都是通过内存屏障(Memnory Barrier)来实现的。
    4. synchronized 靠操作系统内核的Mutex Lock(互斥锁)实现,相当于 JMM 中的 lock、unlock。退出代码块时刷新变量到主内存。
  • 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”) as-if-serial,保证不管编译器和处理器为了性能优化会如何进行指令重排序,

    都需要保证单线程下的运行结果的正确性。也就是常说的:如果在本线程内观察, 所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序 的。

    这里有一段双重检验锁(Double-checked Locking)的经典案例:

    java
    public class SingletonDoubleCheckLock {
    
    private SingletonDoubleCheckLock(){}
    
    private volatile static SingletonDoubleCheckLock instance;
    
    public SingletonDoubleCheckLock getInstance(){
        if (null == instance){
            synchronized (SingletonDoubleCheckLock.class){
                if (null == instance){
                    instance = new SingletonDoubleCheckLock();
                }
            }
        }
        return instance;
    }

    为什么,synchronized 也有可见性的特点,还需要 volatile 关键字? 因为,synchronized 的有序性,不是 volatile 的防止指令重排序。那如果不加 volatile 关键字可能导致的结果,就是第一个线程在初始化初始化对象,设置 instance 指向内存地址时。第二个线程进入时,有指令重排。在判断 if (instance == null) 时就会有出错的可能,因为这会可能 instance 可能还没有初始化成功。

  • 重入性:synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁 的临界资源,这种情况称为可重入锁🔒。 那么我们就写一个例子,来证明这样的情况。

    java
    public class A {
        public synchronized void doA(){
            System.out.println("父类方法:A.doA() ThreadId:" + Thread.currentThread().getId());
        }
    }
    
    public class RetryTest extends A {
        public static void main(String[] args) {
            RetryTest retryTest = new RetryTest();
            retryTest.doA();
        }
    
        public synchronized void doA(){
            System.out.println("子类方法:RetryTest.doA() ThreadId:" + Thread.currentThread().getId());
            doB();
        }
    
        private synchronized void doB(){
            super.doA();
            System.out.println("子类方法:RetryTest.doB() ThreadId:" + Thread.currentThread().getId());
        }
    }

作用对象

synchronized主要有三种使用方式:修饰普通同步方法、修饰静态同步方法、修饰同步方法块。

为什么synchronized最终都是作用在对象上呢? 因为对象在堆中除了除了有字段属性外,还有固定的对象头,通过对象头最终可以得知这个对象是否被加过锁,以及持有锁的线程是谁。

修饰静态同步方法

synchronized放在实例方法上的代码:

java
public class App {

    public synchronized static void test() {
    }
}

然后使用命令对App.java进行编译,如果有中文,建议加上encoding这个参数

bash
javac  -encoding UTF-8 App.java

然后使用命令对App.class进行反编译

bash
javap -verbose App.class

得到结果

txt
Classfile /D:/code/k1/src/App.class
  Last modified 2023年8月20日; size 226 bytes
  SHA-256 checksum 72bf05e007573b470392bebd914f47289ccfc6fbc7fae174bd75af8ace5101ca
  Compiled from "App.java"
public class App
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // App
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // App
   #8 = Utf8               App
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               test
  #12 = Utf8               SourceFile
  #13 = Utf8               App.java
{
  public App();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static synchronized void test();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "App.java"

从反编译的结果来看,我们可以看到test()方法中多了一个标识符。JVM就是根据该ACC_SYNCHRONIZED标识符来实现方法的同步:

当方法被执行时,JVM调用指令会去检查方法上是否设置了ACC_SYNCHRONIZED标识符,如果设置了ACC_SYNCHRONIZED标识符,则会获取锁对象的monitor对象,线程执行完方法体后,又会释放锁对象的monitor对象。在此期间,其他线程无法获得锁对象的monitor对象

修饰普通同步方法

java
public class Test {

    public synchronized static void test(){
    }
}

反编译结果

txt
Classfile /D:/code/k1/src/App.class
  Last modified 2023年8月20日; size 226 bytes
  SHA-256 checksum 72bf05e007573b470392bebd914f47289ccfc6fbc7fae174bd75af8ace5101ca
  Compiled from "App.java"
public class App
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // App
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // App
   #8 = Utf8               App
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               test
  #12 = Utf8               SourceFile
  #13 = Utf8               App.java
{
  public App();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static synchronized void test();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "App.java"

我们可以看到跟放在实例方法相同,也是test()方法上会多一个标识符。可以得出synchronized放在实例方法上和放在类方法上的实现原理相同,都是ACC_SYNCHRONIZED标识符去实现的。只是它们锁住的对象不同

修饰同步方法块

java
public class App {

    public void test(){
        synchronized (this) {
        }
    }
}

反编译结果

txt
Classfile /D:/code/k1/src/App.class
  Last modified 2023年8月20日; size 332 bytes
  SHA-256 checksum b5ff6cf1ecf9107ad1f8cdc41fa71e218684498fb58c13412ee5b68b612a3dd6
  Compiled from "App.java"
public class App
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #7                          // App
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // App
   #8 = Utf8               App
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               test
  #12 = Utf8               StackMapTable
  #13 = Class              #14            // java/lang/Throwable
  #14 = Utf8               java/lang/Throwable
  #15 = Utf8               SourceFile
  #16 = Utf8               App.java
{
  public App();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 4: 0
        line 5: 4
        line 6: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class App, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "App.java"

我们可以看到test()字节码指令中会有两个monitorentermonitorexit指令,

(1) monitorenter: monitorenter指令表示获取锁对象的monitor对象,这是monitor对象中的count并会加+1,如果monitor已经被其他线程所获取,该线程会被阻塞住,直到count=0,再重新尝试获取monitor对象

(2) monitorexit: monitorexit与monitorenter是相对的指令,表示进入和退出。执行monitorexit指令表示该线程释放锁对象的monitor对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象

总结一下

monitorenter:代表 监视器入口,获取锁; monitorexit:代表监视器出口,释放锁; monitorexit:第二次monitorexit,代表 发生异常,释放锁;

从synchronized放置的位置不同可以得出,synchronized用来修饰方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。而用来修饰代码块时,是通过monitorenter和monitorexit指令来完成

在Java中,每个对象里面隐式的存在一个叫monitor(对象监视器)的对象,这个对象源码是采用C++实现的,下面来看一下Monitor对象的源码:

c++
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当monitor对象被线程持有时,Monitor对象中的count就会进行+1,当线程释放monitor对象时,count又会进行-1操作。用count来表示monitor对象是否被持有

对象

在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。

请添加图片描述

  • 对象头:HotSpot虚拟机的对象头分为两部分信息,第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄等,这部分数据的长度在32位和64位的虚拟机中分别为32位和64位。官方称为Mark Word。另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间。

先简单介绍下对象头的形式,JVM中对象头的方式有以下两种(以32位JVM为例):

  • 普通对象: 请添加图片描述
  • 数组对象: 请添加图片描述

Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。 为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

在这里插入图片描述

在这里插入图片描述

  • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
  • bias_lock:对象是否启动偏向锁标记,只占1个二进制位。为1时表示对象启动偏向锁,为0时表示对象没有偏向锁。
  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中锁记录的指针。
  • ptr_to_heavyweight_monitor:指向monitor对象(也称为管程或监视器锁)的起始地址,每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor对象可以与对象一起创建销毁或当前线程试图获取对象锁时自动生,但当一个monitor被某个线程持有后,它便处于锁定状态。

Klass Word(Klass Pointer)

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

每个Class的属性指针(即静态变量) 每个对象的属性指针(即对象变量) 普通对象数组的每个元素指针 当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

数组长度

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

流程

在这里插入图片描述

流程

  • cxq、EntryList都是先进后出队列FILO
  • 争抢锁失败的线程会进入cxq
  • 获取锁的线程调用wait后进入waitSet
  • waitSet中被notify唤醒的线程会进入cxq
  • 持有锁的线程释放锁后
    • 唤醒EntryList最后入队的线程
    • 如果EntryList没有节点,则会将cxq的节点移动过来,再唤醒最后入队的线程

在这里插入图片描述

为什么用cxq和EntryList两个队列存放线程

为了降低cxq尾部并发竞争

假如只有cxq先进后出队列,队列尾部面临的操作有

  • 增加新的竞争锁失败的线程
  • 尾部线程被唤醒
  • 增加从waitSet中被notify的线程

拆分两个队列后,唤醒操作发生在EntryList上

锁升级

锁解决了数据的安全性,但是同样带来了性能的下降,hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。

所以基于这样一个概率,synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁,锁的状态根据竞争激烈的程度从低到高不断升级。

无锁->偏向锁->轻量级锁->重量级锁

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

偏向锁

偏向锁是JDK1.6中引用的优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

为什么要有延迟偏向: 虚拟机启动过程中,许多后台线程可能会争抢锁,导致对象头的锁状态从偏向锁撤销,再升级到 轻量级锁或重量级锁。

  • 无锁时在MarkWord存储hashcode。
  • 偏向锁无位置存储hashcode。
  • 轻量级锁在栈的锁记录中记录hashcode
  • 重量级锁在Monitor中记录hashcode

对象可偏向或已偏向时,调用hashcode会使对象无法偏向。

  • 可偏向时,调hashcode,偏向锁撤销,并只能升级为轻量级锁
  • 已偏向时,调hashcode,偏向锁撤销并升级为重量级锁

几种情况:

  • 创建对象默认无锁->synchronized加锁->无锁变轻量级锁
  • 创建对象默认偏向锁->调用hashcode->撤销偏向锁变为无锁->synchronized加锁->无锁变轻量级锁
  • 创建对象默认偏向锁->synchronized加锁->调用hashcode->偏向锁撤销变重量级锁

偏向锁状态,执行

  • notify会升级为轻量级锁
  • wait会升级为重量级锁

锁升级流程

无锁->

轻量级锁

轻量级锁也是在JDK1.6中引入的新型锁机制。它不是用来替换重量级锁的,它的本意是在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

加锁流程

  1. 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
  2. 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
  4. 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

解锁过程

  1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
  2. 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
  3. 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

重量级锁

指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

优点缺点适用场景
偏向锁加解锁无额外消耗,单线程下加锁基本无消耗线程竞争产生额外的锁撤销成本单线程访问同步代码块
轻量级锁竞争线程不阻塞,基于cas自旋在用户态实现长时间锁自旋获取不到锁还是会锁膨胀,并且消耗CPU同步块执行速度快、锁竞争不激烈
重量级锁锁不自旋,不消耗cpu线程阻塞,用户态转内核态效率低追求吞吐量、竞争激烈、同步代码块执行时间长

image-20230820101553776

image-20230820105617282

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

JMM

  • JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

CAS

CAS是Compare And Swap的缩写(比较在交换),在无锁状态下保证线程操作数据的原子性。解决多线程并行情况下使用锁造成性能损耗的一种机制。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。流程如下图所示。

img

如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

CAS好处

  • 因为没有加锁,所以线程不会陷入阻塞,效率较高
  • 如果竞争激烈,更试频繁发生,效率会受影响

底层实现原理

image-20230820160306673

CAS的问题

①.CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。

②.CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。

CAS是一种基于锁的操作,而且是乐观锁

在java中锁分为乐观锁悲观锁

  • 悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。
  • 而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

image-20230820160859389

volatile理解

作用

在并发编程中,多个线程可能同时访问同一个变量。如果这个变量不是Volatile类型的,那么一个线程对它的修改可能不会立即被其他线程看到,因为其他线程可能还在使用它们自己的缓存拷贝。这就会导致线程间的数据不一致。

Volatile关键字可以解决这个问题。当一个变量被定义为Volatile类型时,任何对它的修改都会aaaaaaaaqaQ a q,而不是等到线程结束或者锁被释放。同时,当其他线程需要访问这个变量时,它们会从主内存中读取最新的值。这样就保证了线程间数据的一致性。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证线程间的可见性
  2. 禁止进行指令重排序

举个例子

java
public class KK {

    static boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            System.out.println(Thread.currentThread().getName() + ": modify stop to true...");
        }, "t1").start();

        new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": " + stop);
        }, "t2").start();

        new Thread(() -> {
            int i = 0;
            while (!stop) {
                i++;
            }
            System.out.println("stopped... c:" + i);
        }, "t3").start();
    }
}

结果

image-20230820162839321

我们可以看到线程一直在运动并没有结束。但是线程2确实变成了true。

image-20230820163248199

java中volatile的作用:1、Java提供了volatile关键字来保证可见性;2、保证有序性,代码为【context = loadContext();inited = true;】;3、提供double check。

AQS

全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

synchronizedAQS
关键字,c++语言实现java语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock阻塞式锁

  • Semaphore信号量

  • CountDownLatch倒计时锁

  • 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的

  • AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程

  • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是O(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源

ReentrantLock

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

原理

image-20230820171913963

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

为什么ReentrantLock比synchronized性能高

ReentrantLock基于AQS实现。AQS在Java代码层面管理阻塞队列,synchronized由jvm层面管理阻塞队列。AQS使用CAS较多,阻塞队列操作逻辑比jvm实现的好,因此性能高一些。

和ReentrantLock的对比

区别synchronizedReentrantLock
修饰位置不同静态方法、普通方法、代码块代码块
自动与非自动释放锁自动释放锁手动释放锁
锁类型不同非公平锁默认非公平锁,也可以创建公平锁
响应中断不同不可以响应中断能够响应中断
底层实现不同基于JVM的Monitor基于AQS
阻塞后线程状态不同阻塞进入BLOCKED状态阻塞进入WAITING状态
同步队列实现方式不同类似栈,有两个,先进后出队列且只有一个,先进先出

死锁产生条件

  1. 互斥条件:一个资源每次只能被一个进程使用;请求与保持条件:
  2. 持有并等待条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  4. 环路条件:若干进程之间形成一种头尾相接的循环等待资源关系

image-20230820210146098

ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合

image-20230820210352868

在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表采用CAS + Synchronized来保证并发安全进行实现 image-20230820210511673

put操作:

  • 根据key计算出Hash值
  • 判断是否需要初始化
  • 定位到某个索引位置,判断首节点,如果为null,尝试使用cas的方式添加节点,如果首节点的hash = MOVED = -1,说明其他线程正在扩容,该节点也一起参与扩容,如果上面的条件都不满足,就使用synchronized锁住首节点,判断是链表还是红黑树,遍历插入
  • 当链表长度大于8时,数组扩容或者转化为红黑树

get操作:

  • 计算出key的Hash值,定位到索引位置
  • 如果首节点就是要get的结点,直接返回
  • 如果是链表结构,遍历链表
  • 如果是红黑树,在红黑树中查询

get过程不需要加锁,因为 Node 的元素 value 和指针next 是用volatile 修饰的,每次查询的都是最新值

ConcurrentHashMap与HashMap、HashTable的区别

ConcurrentHashMap 、HashTable不允许值为null,值为null时抛出异常,HashMap允许值为null HashTable所有方法都加锁synchronized,为线程安全的。HashMap不保证线程安全。ConcurrentHashMap采用了分段锁,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。 ConcurrentHashMap为弱一致性,在get方法时有可能获得过时的数据。

线程池

一、线程池是什么 (1).线程池是理解为一个池,里面有好多个线程,如何想使用线程则去线程池里取一个线程使用,使用后归还即可。 二、为什么使用线程池。 (1).减少系统资源开销,因为频繁创建和销毁线程是有一定系统资源开销的。 (2).方便管理线程。

Java实现的方式

(1)Executor是线程池的顶级接口例如execute()方法。 (2)ExecutorService是对Executor接口扩展例如submit()方法。 (3)AbstractExecutorService是抽象类运用模板方法设计模式实现一部分功能例如submit()方法。 (4)ThreadPoolExecutor是普通线程池类,包含基本线程池操作方法实现例如execute()方法。 线程池接口到类结构图

线程池的执行原理

七个核心参数

  • corePoolSize: 线程池核心线程个数

  • workQueue:用于保存等待执行任务的阻塞队列

  • maximunPoolSize: 线程池最大线程数量。最大线程数目=核心线程+救急线程的最大数目

  • ThreadFactory: 创建线程的工厂

  • RejectedExecutionHandler: 队列满,并且线程达到最大线程数量的时候,对新任务的处理策略

    1.AbortPolicy:直接抛出异常,默认策略;

    2.CallerRunsPolicy:用调用者所在的线程来执行任务;

    3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

    4.DiscardPolicy:直接丢弃任务;

  • keeyAliveTime: 空闲线程存活时间,救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • TimeUnit: 救急线程的生存时间单位,如秒、毫秒等

常见线程池

  1. new SingleThreadExecutor()

    池里只有一条线程,如果线程因为异常而停止,会自动新建一个线程补充

    • 核心线程数和最大线程数都是1
    • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
    • 适用于按照顺序执行的任务
  2. new FixedThreadPool()

    创建一个核心线程数跟最大线程数相同的线程池,线程池数量大小不变,如果有任务放入队列,等待空闲线程。

    • 核心线程数与最大线程数一样,没有救急线程
    • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
    • 适用于任务量已知,相对耗时的任务
  3. new CachedThreadPool()

    线程池是创建一个核心线程数为0,最大线程为Inter.MAX_VALUE的线程池,线程池数量不确定,有空闲线程则优先使用,没用则创建新的线程处理任务,处理完放入线程池。

    适合任务数比较密集,但每个任务执行时间较短的情况

  4. new ScheduledThreadPool()

    创建一个没有最大线程数限制的可以定时执行线程池,还有创建一个只有单个线程的可以定时执行线程池(Executors.newSingleThreadScheduledExecutor())

工作流程

  1. 线程在有任务的时候会创建核心的线程数corePoolSize
  2. 当线程满了(有任务但是线程被使用完)不会立即扩容,而是放到阻塞队列中,当阻塞队列满了之后才会继续创建线程。
  3. 如果队列满了,线程数达到最大线程数则会执行拒绝策略。
  4. 当线程数大于核心线程数事,超过KeepAliveTime(闲置时间),线程会被回收,最终会保持corePoolSize个线程。

线程池流程图

常见问题

(1)Executors类已经提供创建常见线程池方法使用方便,但是常见这些线程池是存在一些弊端的,比如: (2)newFixedThreadPool固定长度线程池适用于流量平稳的场景,newFixedThreadPool内部是LinkedBlockingQueue做任务队列,队列是无界队列没有限制,如果流量不平稳会使LinkedBlockingQueue突然暴增长到达一定程度会是内存溢出。(3)newCachedThreadPool缓存线程池适用流量高场景,newCachedThreadPool内部是SynchronousQueue做任务队列但是最大线程数是Integer的最大值的,任务队列内不存储数据,是一个阻塞的队列,如果流量高很适合使用newCachedThreadPool,但是如果流量太高线程数就会多,线程数高就会有可能超过cpu的负载,哪就会存在机器卡顿或者直接卡死。 (4)newSingleThreadExecutor单线程池使用于流量小的场景,newSingleThreadExecutor内部是LinkedBlockingQueue做任务队列而且还是单线程,队列是无界队列没有限制,如果流量大会使LinkedBlockingQueue突然暴增长单线程处理有限到达一定程度会是内存溢出。 (5)newScheduledThreadPool延时线程池适用于流量小的延时任务的场景,newScheduledThreadPool内部是DelayedWorkQueue做任务队列而且线程数是Integer的最大值的,队列是无界队列延时时间没有到达流量大会使DelayedWorkQueue突然暴增长到达一定程度会是内存溢出。 (6)使用无界队列时maximumPoolSize和keepAliveTime将是一个无效参数,因为无界队列一直都装不满所以不会创建新线程。 (7)使用无界队列时defaultHandler将是一个无效参数,因为无界队列一直都装不满所以不会执行拒绝策略。

常见的阻塞队列

workQueue -当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。 2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。 3.DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

image-20230820215942907

如何确定核心线程数

CPU密集型

一般来说:计算型代码,Bitmap转换,Gson转换等。

获得cpu的核数,不同的硬件不一样,设置核数的的线程数量。核心线程数打下设置为N+1

IO密集型

一般来说:文件读写,DB读写,网路请求

IO非常消耗资源,所有我们需要计算大型的IO程序任务有多少个。

一般来说,线程池最大值 >大型任务的数量即可。一般设置大型任务的数量*2+1

image-20230820220515471

image-20230820221850477

线程池的使用场景

CountDownLatch (闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await()用来等待计数归零
  • countDown()用来让计数减一

Semaphore [ 'scme, for]信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量

使用场景:

通常用于那些资源有明确访问数量限制的场景,常用于限流。

image-20230821143722933

对ThreadLoad的理解

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享。

使用场景

使用场景:全局参数传递、解决线程安全问题 使用案例: PageHelper、MDC类、@Transactional的数据库连接

java
public class Asd {


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



    public static void main(String[] args) {

        new Thread(()->{
            String name = Thread.currentThread().getName();
            threadLocal.set("itcasrt");
            print(name);
            System.out.println(name+" -after remove:" + threadLocal.get());
        },"t1").start();


        new Thread(()->{
            String name = Thread.currentThread().getName();
            threadLocal.set("kk");
            print(name);

            System.out.println(name+" -after remove:" + threadLocal.get());
        },"t2").start();



    }

    static  void print(String str){
        System.out.println(str+":"+threadLocal.get());
        threadLocal.remove();
    }
}

结果

java
t1:itcasrt
t2:kk
t1 -after remove:null
t2 -after remove:null

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程间互不干扰。一个线程可以创建多个ThreadLocal对象,将其存到当前线程的ThreadLocalMap里。ThreadLocalMap底层数据结构是一个Entry数组,它的Entry是继承WeakReference的,key是ThreadLocal对象(弱引用)

● 为什么ThreadLocalMap中把ThreadLocal对象存储为Key时使用的是弱引用

一般来说使用ThreadLocal时会有两个引用指向ThreadLocal对象,一个是创建ThreadLocal对象时的显式的引用,还有一个就是ThreadLocalMap对ThreadLocal对象的弱引用,当我们不再使用ThreadLocal时,显式引用不再指向ThreadLocal对象。这时只有ThreadLocalMap对ThreadLocal对象的弱引用存在。

● 假设ThreadLocalMap对ThreadLocal的引用是强引用 由于ThreadLocalMap是属于线程的,而我们创建多线程时一般是使用线程池进行创建,线程池中的部分线程在任务结束后是不会关闭的,那么这部分线程中的ThreadLocalMap将会一直持有对ThreadLocal对象的强引用,导致ThreadLocal对象无法被垃圾回收,从而造成内存泄漏。

● 设置成弱引用之后

下一次垃圾回收时,无论内存空间是否足够,只被弱引用指向的对象都会被直接回收。所以将ThreadLocalMap对ThreadLocal对象的引用设置成弱引用,就能避免ThreadLocal对象无法回收导致内存泄漏的问题。但是ThreadLocalMap对value的引用是强引用,所以value部分还是有内存泄漏的可能。所以ThreadLocal类定义了expungeStaleEntry方法用于清理key为null的value。expungeStaleEntry在remove中方法中调用。

② 使用ThreadLocal时要注意什么?比如说内存泄漏?

ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key会导致内存泄漏。

可能发送内存泄漏的地方: 1、大量地(静态)初始化ThreadLocal实例,初始化之后不再调用get()、set()、remove()方法。 2、初始化了大量的ThreadLocal,这些ThreadLocal中存放了容量大的Value,并且使用了这些ThreadLocal实例的线程一直处于活跃的状态。

最佳实践:

每次使用完ThreadLocal实例,都调用它的remove()方法,清除Entry中的数据

用remove()方法最佳时机是线程运行结束之前的finally代码块中调用

同时尽量避免使用ThreadLocal存储大对象

Java对象中的四种引用类型:强引用软引用弱引用虚引用

  • 强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收。

    java
    User user= new User();
  • 弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。

    java
    User user = new User(y:
    WeakReference weakReference = new WeakReference(user);

参考资料

java中如何将一个方法加锁 java实现锁几种方式_ctaxnews的技术博客_51CTO博客

Synchronized的底层实现原理(原理解析,面试必备)_synchronized底层实现原理_生活,没那么矫情的博客-CSDN博客

Synchronized的底层实现原理(看这篇就够了) - 江南大才子 - 博客园 (cnblogs.com)