1创建多线程的四种方式

Java中使用Thread类代表线程,所以不管使用什么方式创建多线程本质上都是创建新的Thread对象,然后再调用start()方法启动线程
所不同的是:打算放在新线程中要执行的任务如何封装

1.1继承Thread


1.2实现Runnable接口


1.3实现Callable接口(需借助FutureTask)


1.4使用线程池


2多线程生命周期

2.1背记

线程状态的管理通常是由操作系统和编程语言的运行时环境共同完成的。
Java中,可以通过Thread类提供的方法来控制和管理线程的状态,例如使用interrupt()方法来中断线程的阻塞状态,或者使用join()方法等待线程终止等

2.2理解

2.2.1源码中定义的线程状态


2.2.2新建(NEW )

线程对象刚刚创建,但未启动(start)
Thread thread = new Thread();// 只要线程new出来
System.out.println("线程的名字"+thread.getName()+"线程的状态:"+thread.getState()); 示例代码

2.2.3可运行(RUNNABLE )

线程已被启动,可以被调度或正在被调度;也可以说此时线程在等待CPU时间片
Thread thread = new Thread(()->{
  System.out.println("1111");
  System.out.println("线程的状态"+Thread.currentThread().getState());
});
thread.start();
System.out.println("线程的状态"+thread.getState()); 示例代码

2.2.4锁阻塞(BLOCKED )

当前线程要获取的锁对象正在被其他线程占用,此时该线程处于Blocked状态
Object o = new Object();
Thread threadA = new Thread(() -> {
    synchronized (o) {
        System.out.println("11111");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}, "A");
threadA.start();
try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    e.printStackTrace();
}
Thread threadB = new Thread(() -> {
    synchronized (o) {
    }
}, "B");
threadB.start();
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("线程" + threadB.getName() + "状态" + threadB.getState()); 示例代码

2.2.5等待阻塞(WAITING )

当前线程遇到了wait(),join()等方法
Object o = new Object();
Thread thread = new Thread(() -> {
    synchronized (o) {
        try {
            for (int i = 1; i < 10; i++) {
                System.out.println("i---" + i);
                if (i == 5) {
                    o.wait();
                    // 使用对象的wait方法时 必要要有一个对象和synchronized
                    // 如若不结合synchronized 那么就会出现一个监视器对象状态异常 IllegalMonitorStateException。
                    // 任何一个对象中都有一个ObjectMonitor对象。监视器锁。管程技术。
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();
Thread.sleep(2000);
System.out.println(thread.getState());
Thread thread1 = new Thread(() -> {
    // o.notify();//使用notify或者notifyAll()都要结合synchronized使用,不然就会出现监视器异常IllegalMonitorStateException
    synchronized (o) {
        System.out.println("do some thing");
        o.notify();
    }
});
thread1.start();
System.out.println("end"); 示例代码

2.2.6限时等待(TIMED_WAITING )

当前线程调用了sleep(时间),wait(时间),join(时间)等方法
Object o = new Object();
Thread thread = new Thread(() -> {
    synchronized (o) {
        try {
            // 线程调用wait(5000)方法
            o.wait(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread.start();
// 线程调用sleep(5000)方法
Thread.sleep(5000);
// 线程调用join(5000)方法
thread.join(5000);
System.out.println(thread.getState()); 示例代码

2.2.7终止(TERMINATED )

线程正常结束或异常提前退出
Thread thread = new Thread(() -> {
    int i = 0;
    System.out.println("1111");
    try {
        i = 10 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println(i);
});
thread.start();
Thread.sleep(100);
System.out.println(thread.getState()); 示例代码

3什么是线程池,线程池有哪些?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间、实现了线程对象的复用,提高的代码执行效率
在 JDK 的 java.util.concurrent.Executors 中提供了多种生成线程池的静态方法。
需要由线程对象执行特定任务时,调用他们的 execute 方法即可。
注意:实际开发时严格禁止使用上述方法创建线程池!!!因为它们内部设置的各项参数非常不合理,存在OOM等重大风险

3.1newCachedThreadPool()

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:

3.2newFixedThreadPool()

创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型的线程池。在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

3.3newScheduleThreadPool()

创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。

4种线程池底层全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。

4ThreadPoolExecutor对象有哪些参数?都有什么作用?怎么设定核心线程数和最大线程数?拒绝策略有哪些? [重点]

4.17个参数的作用

4.1.1corePoolSize

核心线程数,在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false)

4.1.2maximumPoolSize

最大线程数,线程池能容纳的最大线程数,当线程池中的线程达到最大且等待队列已满时,添加新任务将会触发拒绝策略

4.1.3keepAliveTime

线程的最大空闲时间

4.1.4unit

keepAliveTime的时间单位

4.1.5workQueue

任务队列,常用有三种队列,即SynchronousQueue、LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。

4.1.6threadFactory

线程工厂,ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制,默认直接新建线程。

4.1.7RejectedExecutionHandler

也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandlerrejectedExecution法。默认是抛出一个运行时异常。

4.2线程池大小设置

4.2.1最佳实践

corePoolSizemaximumPoolSize设置成相同的数值,避免非核心线程创建又销毁,销毁又创建

4.2.2具体数值设置

首先需要分析项目中线程池负责的任务是哪种类型

4.2.2.1CPU密集型

主要执行计算任务,响应时间很快,CPU一直在运行。这种任务的CPU利用率很高,那么线程数的配置应该根据CPU核心数来决定。
CPU核心数等于最大同时执行线程数
假如CPU核心数为4,那么服务器最多能同时执行4个线程,过多的线程会导致上下文切换反而使得效率降低
此时线程池的最大线程数可以配置为CPU核心数+1

4.2.2.2I/O密集型

主要进行I/O操作,执行I/O操作时间长,在I/O过程中CPU处于空闲状态导致CPU利用率不高
这种情况可以增加线程池中线程数量的大小,具体增加多少可以结合线程的等待时长来判断,等待时间越长,线程数可以相对越多
一般可以配置CPU核心数的两倍
补充:其实我们平时写的常规业务都是I/O密集型。前端发送过来一个请求,Java代码需要计算的不多,大部分时间是在等待Redis、MySQL、ElasticSearch通过网络传输返回结果

5常见线程安全的并发容器有哪些?


6Atomic原子类了解多少?原理是什么?

6.1概述

Java中的java.util.concurrent.atomic包提供了一组原子类,用于在多线程环境中执行原子操作,而无需使用显式的锁。
这些原子类使用特殊的CPU指令来确保操作的原子性,从而避免了使用锁带来的性能开销。
这些原子类的实现依赖于底层硬件架构提供的原子操作指令。
通常,这些指令在现代处理器上是硬件级别的支持,确保对内存的读写是原子的。
这使得在不使用锁的情况下,可以在多线程环境中执行某些操作,而不会导致竞态条件(race conditions)。

6.2分别说明

以下是一些常见的java.util.concurrent.atomic包中的原子类以及它们的一些实现原理:

6.2.1AtomicInteger、AtomicLong、AtomicReference

这些类使用compareAndSet(CAS)操作实现原子性。
CAS是一种乐观锁定机制,它尝试原子地将一个值更新为新值,但只有在当前值等于预期值时才成功;否则,它会重新尝试。
CAS操作是由处理器提供的原子性操作指令支持的。

6.2.2AtomicBoolean

AtomicBoolean类使用compareAndSet实现。
compareAndSet的实现通常依赖于底层处理器的CAS指令。

6.2.3AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

这些类提供了对数组元素的原子性访问。
它们也使用CAS操作,但应用于数组的特定位置。

6.2.4AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

这些类提供了对对象字段的原子性更新。
它们使用了反射和CAS操作来实现。

7synchronized底层实现是什么?Lock底层是什么?有什么区别?

7.1synchronized原理

7.1.1同步方法

方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。
JVM可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

7.1.2同步代码块

代码块的同步是利用monitorentermonitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。
JVM执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;
当执行monitorexit指令时,锁计数器-1;
当锁计数器为0时,该锁就被释放了。
如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

7.2Lock原理

Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)

7.2.1Lock获取锁的过程

本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。

7.2.2Lock释放锁的过程

修改状态值,调整等待链表。Lock大量使用CAS+自旋。因此根据CAS特性,Lock建议使用在低锁冲突的情况下。

7.3Locksynchronized的区别

Lock的加锁和解锁都是由Java代码配合native方法(调用操作系统的相关方法)实现的,而synchronized的加锁和解锁的过程是由JVM管理的。

7.3.1阻塞机制


7.3.2锁占有模式


7.3.3条件队列

一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronized只有一路条件队列
同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以及设置等待时限等方式退出条件队列。

7.3.4总结


synchronized

Lock

关键字

接口/类

自动加锁和释放锁

需要手动调用unlock()方法释放锁

JVM层面的锁

API层面的锁

非公平锁

可以选择公平或者非公平锁

锁是一个对象,并且锁的信息保存在了对象中

代码中通过int类型的state标识

有一个锁升级的过程