创建方式:newFixedThreadPool (固定数目线程的线程池)、newCachedThreadPool(可缓存线程的线程池)、newSingleThreadExecutor(单线程的线程池)、newScheduledThreadPool(定时及周期执行的线程池)、new ThreadPoolExecutor() (自定义的方式创建)
## 线程池七大参数
- corePoolSize(核心线程数)
线程池当中线程数最基本上的数量:只有当工作任务队列满了才会有新的线程被创建出来,此时线程数才会大于该值
- maximumPoolSize(最大线程数)
线程池中允许的最大线程数:当前任务队列满了并且小于该值的时候线程才会被创建,否则交给拒绝策略
- 最大线程的存活时间:如果当前线程空闲且线程数量大于核心数则线程销毁的超时时间
- unit 时间单位
- 阻塞队列:当核心线程满后,后面来的任务都进入阻塞队列
- 线程工厂:用于生产线程
- 任务拒绝策略:阻塞队列满后,拒绝任务,有四种策略(1)抛异常(2)丢弃任务不抛异常(3)打回任务(4)尝试与最老的线程竞争
## 线程池的好处
1⃣️降低资源消耗
2⃣️提高响应速度
3⃣️使线程便于管理
a.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务!
b.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
d.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者"我不能再接受任务了"
run和start方法的区别?
1. 定义位置不同
run()方法是Thread类中的一个普通方法,它是线程中实际运行的代码,线程的代码逻辑主要就是在run()方法中实现的。
start()方法是Thread类中的一个启动方法,它会启动一个新的线程,并在新的线程中调用run()方法。
2. 执行方式不同
直接调用run()方法,会像普通方法一样在当前线程中顺序执行run()方法的内容,这并不会启动一个新的线程。
调用start()方法会创建一个新的线程,并在新的线程中并行执行run()方法的内容。
3. 线程状态不同
当我们调用start()方法启动一个新线程时,该线程会进入就绪状态,等待JVM调度它和其他线程的执行顺序。而当我们直接调用run()方法时,则会在当前线程中执行,不会产生新的线程。
1. RUNNING:线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。
2. SHUTDOWN:不接收新任务,但能处理已排队的任务。调用线程池的 shutdown() 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。
3. STOP:不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由(RUNNING 或 SHUTDOWN ) 转变为 STOP 状态。
4. TIDYING:
SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated() 方法。线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。
线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就会由 SHUTDOWN 转变为 TIDYING 状态。
线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为 TIDYING 状态。
5. TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED 状态。
线程池execute和submit方法的区别?
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 创建拒绝策略
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池,设置参数
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 60; // 线程空闲时间
TimeUnit unit = TimeUnit.SECONDS; // 时间单位
int queueCapacity = 100; // 任务队列大小
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
unit,
new LinkedBlockingQueue<>(queueCapacity),
threadFactory,
rejectedExecutionHandler
);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
final int taskId = i; // 任务ID(仅用于示例)
executor.execute(new Runnable() {
public void run() {
System.out.println("Task " + taskId + " is executing by " +
Thread.currentThread().getName());
// 执行任务的具体逻辑
// ...
}
});
}
// 关闭线程池
executor.shutdown();
}
}
守护线程(daemon thread)是在计算机程序中运行的一种特殊线程。它的主要特点是当所有非守护线程结束时,守护线程会自动退出,而不会等待任务的完成。
守护线程通常被用于执行一些后台任务,如垃圾回收、日志记录等。它们在程序运行过程中默默地执行任务,不会阻塞主线程或其他非守护线程的执行。
与普通线程不同,守护线程的生命周期并不影响整个程序的生命周期。当所有非守护线程结束时,守护线程会被强制退出,无论它的任务是否完成。
需要注意的是,守护线程不能用于执行一些重要的任务,因为它们可能随时被强制退出。此外,守护线程也无法捕获或处理异常。
thread1.setDaemon(true); //设置守护线程
结束线程有以下三种方法: (1)设置退出标志,使线程正常退出。 (2)使用interrupt()方法中断线程。 (3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
线程分为1-10级,其中10最高,默认值为5,优先级的高低不代表线程优先执行,JVM不一定采纳,需要看CPU的情况,一般情况下优先级高的先执行。
remove
方法删除变量,否则可能会造成内存泄露的问题## Thread、ThreadLocal、ThreadLocalMap的关系
Thread 与 ThreadLocalMap 是 has a 的关系。初始时,Thread 中的 threadLocals 为空,只有在当前线程中创建了 ThreadLocal 变量并且设置了变量值,才会创建 ThreadLocalMap 实例。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal 类中定义了静态内部类 ThreadLocalMap,把它当作伪 Map 即可,就是存储key-value 键值对
ThreadLocalMap 类中又定义了 Entry 静态内部类,该类定义了一个 Entry 类型的数组 table。为什么要自定义一个 Entry 呢,因为现有不满足要定制,Entry 的 k 为 ThreaLocal 实例,v 为变量值
使用注意:
原理:
线程写Volatile变量的过程:
线程读Volatile变量的过程:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
## 问题:volatile 能够保证线程安全问题吗?为什么?
不能,volatile 只能保证可见性和顺序性,不能保证原子性。
Java线程具有以下几种基本状态:
start()
方法以开始其执行。此时,线程已分配内存空间并已完成初始化,但它还没有开始执行代码。start()
方法,正处于就绪状态,它已经准备好运行。wait()
方法、等待 I/O 操作完成等原因而暂时无法继续执行。wait()
方法且设置了超时参数的情况下。(无限等待状态(植物人状态):线程调用了wait()方法并且没有传参数 → 自己醒不过来,只能调用notify()方法来唤醒无限等待状态)此外,还有两种特殊状态:
notifyAll()
或 join()
方法的执行结果。run()
方法已经执行完毕,或者被中断或异常退出,导致线程进入死亡状态。在这种状态下,线程不能再作为独立执行的线程对待。/*顾客去买包子,包子可能没有要现做,
但是不知道要多久才能做好,因为不知道顾客要等待的时间,所以调用了wait()方法,此时顾客属于无限等待状态
包子做好了,老板通知顾客包子好了,notify()一下*/
public class WaitAndNotify {
public static void main(String[] args) {
//锁对象必须是唯一的
final Object obj = new Object();
//创建一个顾客线程
new Thread(){
@Override
public void run() {
while (true){
//老板和顾客线程要用同步代码块包裹,保证只能实行其中的一个
synchronized (obj){
System.out.println("顾客说要1个素馅的1个肉馅的包子");
//无限等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后的代码
System.out.println("包子已经做好了,开吃");
System.out.println("______________________________");
}
}
}
}.start();
//创建一个老板线程
new Thread(){
@Override
public void run() {
while(true){
//花了3S做包子
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//老板和顾客线程要用同步代码块包裹,保证只能实行其中的一个
synchronized (obj){
System.out.println("老板花3S做好了包子");
obj.notify();
}
}
}
}.start();
}
}
notify()是随机唤醒一个线程,notifyAll()唤醒所有线程,都是让线程变为就绪状态
sleep
直接作用于 Thread
类,不需要与 synchronized
关键字一起使用。 wait
则是 Object
类的方法,通常需要配合 synchronized
使用以确保正确性。sleep
会自动在指定的时间后唤醒线程,如果未设置超时时间,则会无限期地等待下去。 wait
不一定需要传递超时时间参数。如果不传递任何参数,表示永久休眠;若传递超时时间,则在超时后唤醒。sleep
不会释放任何锁资源。 wait
会释放所持锁资源,以便其他线程能够访问同步控制块或方法。sleep
通常用于使整个应用程序暂停执行,而不是特定的同步控制块。 wait
更适合于在特定同步控制块内部暂停线程,以便其他线程有机会处理该控制块的资源。CAS是一种无锁算法 (乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
缺点:
1.可能cas 会一直失败,然后自旋
2.如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。
AQS的核心思想是:通过一个volatile修饰的int属性state代表同步状态,例如0是无锁状态,1是上锁状态。多线程竞争资源时,通过CAS的方式来修改state,例如从0修改为1,修改成功的线程即为资源竞争成功的线程,将其设为exclusiveOwnerThread,也称【工作线程】,资源竞争失败的线程会被放入一个FIFO的队列中并挂起休眠,当exclusiveOwnerThread线程释放资源后,会从队列中唤醒线程继续工作,循环往复。
在Java中,synchronized是一种关键字,用于实现线程同步。它可以用于方法或代码块,用于保证同一时间只有一个线程可以执行被synchronized修饰的代码。
synchronized的锁机制有两种使用方式:
synchronized锁是基于对象的,每个对象都有一个关联的锁。当多个线程同时访问某个对象的同步方法或同步代码块时,它们会竞争该对象的锁。
1.如果synchronized锁加在实例方法上,则默认使用的是this锁
2.如果synchronized锁加载静态方法上,则默认使用的是 类名.class 锁(Java反射技术中说到一个class文件只会在jvm中存在一份)
另外,要注意避免过多地使用synchronized,因为过多的同步操作可能会导致性能下降。在某些情况下,可以考虑使用更灵活的并发工具,如Lock和Condition接口
对象头:
自旋:线程会一直循环检查该锁是否被释放,直到获取到该锁为止。这个循环等待的过程被称为自旋
1)偏向锁
只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程。引入了偏向锁目的是来尽可能减少无竞争情况下的同步操作开销。当一个线程访问同步块并获取对象的锁时,会将锁的标记记录在线程的栈帧中,并将对象头中的Thread ID设置为当前线程的ID。此后,当这个线程再次请求相同对象的锁时,虚拟机会使用已经记录的锁标记,而不需要再次进入同步块。
偏向锁(Biased Locking)就是为了在无竞争的情况下减少同步操作的开销。它通过记录线程ID来避免对锁的加锁和解锁操作,提高了单线程访问同步代码块时的性能。
2)轻量级锁(自旋锁)
一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋。虚拟机会将对象的Mark Word复制到线程的栈帧中作为锁记录,并尝试使用CAS(Compare and Set)操作尝试获取锁。如果CAS成功,则表示线程获取了轻量级锁,并继续执行同步块。如果CAS失败,说明有竞争,虚拟机会通过自旋(spinning)等待其他线程释放锁
轻量级锁是为了减少线程切换的开销。它使用CAS(Compare and Set)操作来尝试获取锁,如果成功则可以继续执行同步块,无需线程切换;如果失败,则会进行自旋操作等待锁的释放。自旋操作避免了线程挂起和切换的开销,提高了多线程竞争时的性能。
使用对象头中的一部分位来存储线程ID和锁标记,不需要额外的内存存储锁的状态。相对于传统的重量级锁,它能够节省内存消耗。
3)重量级锁
如果自旋等待不成功,虚拟机会将轻量级锁升级为重量级锁。在这种状态下,虚拟机会将线程阻塞,并使用操作系统的互斥量来实现锁的释放和获取。
需要注意的是,锁的升级是逐级升级的过程,而不会存在降级。换句话说,一旦锁升级到更高级别,就不会回到低级别。
## 1、适应性自旋
解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
## 2、锁粗化(Lock Coarsening)
锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
## 3、锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:
public static void main(String[] args) {
SynchronizedTest02 test02 = new SynchronizedTest02();
//启动预热
for (int i = 0; i < 10000; i++) {
i++;
}
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
test02.append("abc", "def");
}
虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。
锁 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。 同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。 同步块执行速度较长。 |
synchronized 是 JVM 的内置锁,基于 Monitor 机制实现。每一个对象都有一个与之关联的监视器 (Monitor),这个监视器充当了一种互斥锁的角色。当一个线程想要访问某个对象的 synchronized 代码块,首先需要获取该对象的 Monitor。如果该 Monitor 已经被其他线程持有,则当前线程将会被阻塞,直至 Monitor 变为可用状态。当线程完成 synchronized 块的代码执行后,它会释放 Monitor,并把 Monitor 返还给对象池,这样其他线程才能获取 Monitor 并进入 synchronized 代码块。
每个Java对象都有一个与之关联的Monitor。这个Monitor的实现是在JVM的内部完成的,它采用了一些底层的同步原语,用以实现线程间的等待和唤醒机制,这也是为什么等待(wait)和通知(notify)方法是属于Object类的原因。这两个方法实际上是通过操纵与对象关联的Monitor,以完成线程的等待和唤醒操作,从而实现线程之间的同步。
(1)synchronized是关键字,lock是一个类,synchronize是在JVM层面实现的,发生异常后jvm会释放锁。lock是JDK代码实现的,需要手动释放,在finally块中释放,容易死锁。
(2) 用法不一样:synchronize可以用在代码块上,方法上。lock只能写在代码里,不能直接修改方法。synchronized在发生异常时会自动释放锁,lock需要手动释放锁
(3)synchronized是非公平锁、可重入锁(每个线程获取锁的顺序不是按照线程访问锁的先后顺序获取)不可中断锁,lock是可公平锁、可中断锁、可重入锁。
(4)synchronized适用于少量同步,lock适用于大量同步。
(5)锁状态是否可以判断:synchronized 不可以,lock可以。
## Synchronized
优点:实现简单,语义清晰,便于JVM堆栈跟踪,加锁解锁过程由JVM自动控制,提供了多种优化方案,使用更广泛
缺点:悲观的排他锁,不能进行高级功能
## Lock
优点:可定时的、可轮询的与可中断的锁获取操作,提供了读写锁、公平锁和非公平锁
缺点:需手动释放锁unlock,不适合JVM进行堆栈跟踪
## 可重入锁
可重入锁和不可重入锁的最大区别在于,可重入锁允许同一个线程在获得锁之后再次获得该锁,而不可重入锁不允许。
如果一个线程已经获得锁,那么在该线程释放该锁之前,它可以再次获得该锁而不会被阻塞。
实现原理:
每次获得锁时,计数器加1,每次释放锁时,计数器减1。只有当计数器为0时,其他线程才有机会获得该锁。
## ReentrantLock (用于替代synchronized)
ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
## 公平锁
按先来后到的顺序获取锁
## 读写锁 ReentrantReadWriteLock (乐观锁)
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。
使用synchronized关键字:synchronized关键字可以将某些代码块或方法设为同步代码,确保同一时刻只有一个线程可以访问。这种方式需要注意锁的粒度,使得锁住的代码块尽可能的短,以避免影响程序性能。
使用Volatile关键字:Volatile关键字可以用于修饰变量,确保多线程之间的可见性,即当一个线程修改了共享变量的值,其他线程会立即查询最新的值。
使用Lock对象:Lock是JDK提供的同步机制,Lock提供的Lock()和Unlock()方法可以在同一个时刻,只允许一个线程进入执行Lock()和Unlock()方法之间的代码块,其他线程必须等待。
使用原子类:Java提供了很多原子类,包括AtomicInteger、AtomicLong和AtomicBoolean等等,这些类可以保证特定操作的原子性,避免多线程同时访问一个共享资源所造成的数据安全问题。(atomic是通过CAS实现的)
使用ThreadLocal类:ThreadLocal类可以在多线程中为每个线程创建一个独立的实例,避免多线程对同一资源的争夺,从而保证了数据安全性。
Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。
所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的
## 栈帧的压入
局部变量表 主要存放了编译期可知的各种数据类型、对象引用。
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接 主要服务一个方法需要调用其他方法的场景。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接
## 栈帧的弹出
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。