Java并发编程(多线程)
创建多线程的四种方式
Java中使用Thread类代表线程,所以不管使用什么方式创建多线程本质上都是创建新的Thread对象,然后再调用start()方法启动线程
所不同的是:打算放在新线程中要执行的任务如何封装
继承Thread类
- 定义一个类继承自 Thread 类,并重写 run() 方法。
- 创建该类的对象,并调用其 start() 方法启动新线程。
实现Runnable接口
- 定义一个类并实现 Runnable 接口的 run() 方法。
- 创建该类的对象,并将其作为目标传递给一个 Thread 类的实例。
- 调用 Thread 实例的 start() 方法启动新线程。
实现Callable接口(需借助FutureTask)
- 定义一个类并实现 Callable 接口的 call() 方法。
- 创建一个 FutureTask 对象,将 Callable 实例作为参数传入。
- 将 FutureTask 对象作为目标传递给一个 Thread 类的实例,并启动线程。
- 通过 FutureTask 对象的 get() 方法获取异步计算的结果。
使用线程池
- 创建一个执行器服务,例如 Executors.newFixedThreadPool(int nThreads) 。
- 提交实现了 Runnable 或 Callable 接口的任务到执行器服务。
- 执行器服务会自动分配线程来执行这些任务。
多线程生命周期
背记
- 新建(New):线程被创建后,尚未启动(未调用start()方法)的状态。
- 就绪(Runnable):当线程对象的start()方法被调用后,线程进入就绪状态,此时线程已经准备好执行,等待CPU调度。
- 运行(Running):当CPU开始调度处于就绪状态的线程时,线程进入运行状态,开始执行其任务。
- 阻塞(Blocked):线程在运行过程中遇到同步锁但申请锁失败,会进入阻塞状态,此时需要获取到锁之后才会继续执行。
- 等待(Waiting):线程在执行过程中可能会进入等待状态,例如调用了wait()方法,等待其他线程的通知、唤醒,才能继续执行。
- 计时等待(Timed Waiting):与等待状态类似,但在此状态下的线程有一个预定的等待时间,超时后自动返回到就绪状态。
- 终止(Terminated):线程完成任务或者因为异常而结束执行,进入终止状态。
线程状态的管理通常是由操作系统和编程语言的运行时环境共同完成的。
在Java中,可以通过Thread类提供的方法来控制和管理线程的状态,例如使用interrupt()方法来中断线程的阻塞状态,或者使用join()方法等待线程终止等
理解
源码中定义的线程状态
State
新建(NEW )
线程对象刚刚创建,但未启动(start)
|
|
可运行(RUNNABLE )
线程已被启动,可以被调度或正在被调度;也可以说此时线程在等待CPU时间片
|
|
锁阻塞(BLOCKED )
当前线程要获取的锁对象正在被其他线程占用,此时该线程处于Blocked状态
|
|
等待阻塞(WAITING )
当前线程遇到了wait(),join()等方法
|
|
限时等待(TIMED_WAITING )
当前线程调用了sleep(时间),wait(时间),join(时间)等方法.
|
|
终止(TERMINATED )
线程正常结束或异常提前退出.
什么是线程池,线程池有哪些?
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间、实现了线程对象的复用,提高的代码执行效率
在 JDK 的 java.util.concurrent.Executors 中提供了多种生成线程池的静态方法。
- ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
- ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
- ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
- ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
需要由线程对象执行特定任务时,调用他们的 execute 方法即可。
注意:实际开发时严格禁止使用上述方法创建线程池!!!因为它们内部设置的各项参数非常不合理,存在OOM等重大风险
ThreadPoolExecutor对象有哪些参数?都有什么作用?怎么设定核心线程数和最大线程数?拒绝策略有哪些? [重点]
7个参数的作用
corePoolSize
核心线程数,在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false)
- allowCoreThreadTimeOut为false:核心线程会一直存活,哪怕是一直空闲着
- allowCoreThreadTimeOut为true:核心线程空闲时间超过keepAliveTime时会被回收
maximumPoolSize
最大线程数,线程池能容纳的最大线程数,当线程池中的线程达到最大且等待队列已满时,添加新任务将会触发拒绝策略
keepAliveTime
线程的最大空闲时间
- 非核心空闲超过这个时间将被回收
- 核心线程超过这个时间是否回收受allowCoreThreadTimeOut影响
unit
keepAliveTime的时间单位
workQueue
任务队列,常用有三种队列,即SynchronousQueue、LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
threadFactory
线程工厂,ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制,默认直接新建线程。
RejectedExecutionHandler
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。
线程池大小设置
最佳实践
把corePoolSize和maximumPoolSize设置成相同的数值,避免非核心线程创建又销毁,销毁又创建
具体数值设置
首先需要分析项目中线程池负责的任务是哪种类型
CPU密集型
主要执行计算任务,响应时间很快,CPU一直在运行。这种任务的CPU利用率很高,那么线程数的配置应该根据CPU核心数来决定。
CPU核心数等于最大同时执行线程数
假如CPU核心数为4,那么服务器最多能同时执行4个线程,过多的线程会导致上下文切换反而使得效率降低
此时线程池的最大线程数可以配置为CPU核心数+1
I/O密集型
主要进行I/O操作,执行I/O操作时间长,在I/O过程中CPU处于空闲状态导致CPU利用率不高
这种情况可以增加线程池中线程数量的大小,具体增加多少可以结合线程的等待时长来判断,等待时间越长,线程数可以相对越多
一般可以配置CPU核心数的两倍
补充:其实我们平时写的常规业务都是I/O密集型。前端发送过来一个请求,Java代码需要计算的不多,大部分时间是在等待Redis、MySQL、ElasticSearch通过网络传输返回结果
常见线程安全的并发容器有哪些?
- Set集合:
CopyOnWriteArraySet
(写时复制技术) - List集合:
CopyOnWriteArraySet
(写时复制技术) - Map集合:
ConcurrentHashMap
- JDK1.7:采用了锁分段(Segment)技术来提高并发性能,每个段(Segment)相当于一个独立的哈希表,并且每个段都有自己的锁,这样可以减少锁的竞争
- JDK1.8:设计进行了优化,不再使用分段锁(Segment),而是采用了CAS(Compare and Swap)操作和synchronized关键字相结合的方式来实现线程安全,它使用了更细粒度的锁机制,即在链表或红黑树的节点上使用synchronized锁,同时对于一些非竞争性的操作则采用CAS来保证原子性,从而进一步提高了并发性能
Atomic原子类了解多少?原理是什么?
概述
Java中的java.util.concurrent.atomic包提供了一组原子类,用于在多线程环境中执行原子操作,而无需使用显式的锁。
分别说明
AtomicInteger、AtomicLong、AtomicReference
这些类使用compareAndSet(CAS)操作实现原子性。
CAS是一种乐观锁定机制,它尝试原子地将一个值更新为新值,但只有在当前值等于预期值时才成功;否则,它会重新尝试。
CAS操作是由处理器提供的原子性操作指令支持的。
AtomicBoolean
AtomicBoolean类使用compareAndSet实现。
compareAndSet的实现通常依赖于底层处理器的CAS指令。
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
这些类提供了对数组元素的原子性访问。
它们也使用CAS操作,但应用于数组的特定位置。
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
这些类提供了对对象字段的原子性更新。
它们使用了反射和CAS操作来实现。
synchronized底层实现是什么?Lock底层是什么?有什么区别?
7.1 synchronized原理
同步方法
方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。
JVM可以从方法常量池中的方法表结构(method_info Structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
同步代码块
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。
当JVM执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;
当执行monitorexit指令时,锁计数器-1;
当锁计数器为0时,该锁就被释放了。
如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
Lock原理
Lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
Lock获取锁的过程
本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
Lock释放锁的过程
修改状态值,调整等待链表。Lock大量使用CAS+自旋。因此根据CAS特性,Lock建议使用在低锁冲突的情况下。
Lock与synchronized的区别
Lock的加锁和解锁都是由Java代码配合native方法(调用操作系统的相关方法)实现的,而synchronized的加锁和解锁的过程是由JVM管理的。
阻塞机制
- 当一个线程使用synchronized获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。
- Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的条件下提供一种退出的机制。
锁占有模式
- synchronized对线程的同步仅提供独占模式,
- Lock既可以提供独占模式,也可以提供共享模式
条件队列
一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronized只有一路条件队列
同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以及设置等待时限等方式退出条件队列。
总结
synchronized | Lock |
---|---|
关键字 | 接口/类 |
自动加锁和释放锁 | 需要手动调用unlock()方法释放锁 |
JVM层面的锁 | API层面的锁 |
非公平锁 | 可以选择公平或者非公平锁 |
锁是一个对象,并且锁的信息保存在了对象中 | 代码中通过int类型的state标识 |
有一个锁升级的过程 | 无 |