1 线程
进程和线程(重点)
进程(process)
进程是资源分配的基本单位;
系统在运行的时候会为每个进程分配不同的内存空间和系统资源;
一个进程可以有多个线程;
一个进程不能直接访问另一个进程,由于进程间的内存隔离,进程间通信通常需要使用特殊的机制,如管道、信号、消息队列、共享内存或套接字等。
线程(thread)
线程是程序执行的基本单位。
线程在进程内部运行,共享进程的内存空间和资源。
线程有自己的程序计数器、寄存器、栈(包括虚拟机栈和本地方法栈),但共享进程的堆和方法区。
线程也被称为轻量级进程,因为创建线程的开销通常小于创建进程的开销。并且线程间的上下文切换开销通常小于进程上下文切换,因为线程共享进程的地址空间
查看进程线程的方法
在Windows环境下,可以通过任务管理器来查看进程和线程数,也可以用来杀死进程
查看进程
tasklist
杀死进程
taskkill /F /PID 14788
Linux环境下有关进程的指令
ps -ef 查看所有进程
ps -fT -p <PID> 查看某个进程(PID)的所有线程
kill 杀死进程
top 按大写H切换是否显示进程
top -H -p <PID> 查看某个进程(PID)的所有线程
线程常见方法
| 方法名 | static | 功能说明 | 注意 |
|---|---|---|---|
| start() | 创建一个新线程,并且这个新线程会执行线程对象中run() 方法定义的代码. | start方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException | |
| run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为 | |
| join() | 等待线程运行结束 | ||
| join(long n) | 等待线程运行结束,最多等待n毫秒 | ||
| getId() | 获取线程长整型的id | id唯一 | |
| getName() | 获取线程名 | ||
| setName(String) | 修改线程名 | ||
| getPriority() | 获取线程优先级 | ||
| setPriority(int) | 修改线程优先级 | Java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的机率 | |
| getState() | 获取线程状态 | Java中线程状态是用6个enum表示,分别为:NEW(新建), RUNNABLE(可运行), BLOCKED(阻塞), WAITING(等待), TIMED_WAITING(计时等待), TERMINATED(终止) | |
| isInterrupted() | static | 判断是否被打断 | 不会清除打断标记 |
| interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
| interrupt() | static | 打断线程 | 如果被打断线程正在sleep、wait、join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记 |
| isAlive() | static | 线程是否存活(还没有运行完毕) |
调用 start()方法时会执行run()方法,为什么不能直接调用run()方法
调用 start()方法会创建一个新线程,并且这个新线程会执行线程对象中run() 方法定义的代码。
直接调用 run() 方法并不会创建一个新的线程,而是在当前线程中同步执行 run() 方法中的代码。
线程的生命周期(重点)
包括:新建、就绪、运行、阻塞、等待、超时等待、终止。
新建
- 当使用new Thread()创建一个线程对象时,该线程处于新建状态。此时,线程对象已经被分配了内存,但还没有开始执行其任务。
就绪
- 当调用线程的start()方法后,线程进入就绪状态。该状态的线程等待CPU的调度,即等待CPU时间片。
运行
- 当线程获得CPU时间片并开始执行其run()方法中的代码时,此时线程处于运行状态。
阻塞:
- 线程在尝试执行某个同步代码块或方法时,如果无法获得必要的锁,则会进入阻塞状态。在这个状态下,线程会暂停执行,直到它获得所需的锁。
等待
- 线程可以通过调用Object.wait()方法或Thread.join()方法进入等待状态。在这个状态下,线程会暂停执行,直到它被其他线程调用Object.notify()或Object.notifyAll()方法显式地唤醒。
超时等待
- 线程通过调用带有超时参数的Thread.sleep(long millis)、Object.wait(long timeout)等方法进入超时等待状态。在这种状态下,线程会等待指定的时间或直到被唤醒,线程会自动返回就绪状态,等待CPU
终止
- 当线程的run()方法执行完毕或因为抛出未捕获的异常而终止时,线程进入终止状态。
sleep、wait、join、yield(重点)
sleep
- sleep() 方法是 Thread 类的静态本地方法,可以在任何地方直接调用。
- 让当前线程休眠指定的时间,不会释放任何锁。
- sleep() 方法在指定的时间到达后会自动唤醒,或者如果当前线程在等待期间被中断(调用interrupt()),则会抛出 InterruptedException。
- sleep()主要用于当前线程休眠。
wait
- wait() 方法是 Object 类的本地方法,只能在同步块或同步方法中调用。
- 调用 wait() 方法会释放当前线程持有的对象的监视器锁,允许其他线程进入该对象相关的同步方法或同步块,加⼊到等待队列中。
- wait() 方法在等待期间可以通过其他线程调用相同对象的 notify() 或 notifyAll() 方法来主动唤醒等待线程,或者如果调用了 wait(long timeout),则指定的时间后自动唤醒等待线程。
- wait()主要用于多⽤于多线程之间的通信。
join
- join()执⾏后线程进⼊阻塞状态,例如在线程B中调⽤线程A的join(),那线程B会进⼊到阻塞队
列,直到线程A结束或中断线程.。
yield
- yield()执⾏后线程直接进⼊就绪状态,⻢上释放了cpu的执⾏权,但是依然保留了cpu的执⾏资
格,所以有可能cpu下次进⾏线程调度还会让这个线程获取到执⾏权继续执⾏。
两者的主要区别在于是否释放对象锁以及适用场景的不同
sleep
//线程A在持有锁的情况下调用了sleep()沉睡5s,它不会释放任何锁,线程B尝试获取锁,在线程A持锁期间被阻塞, 线程A被主线程打断唤醒.
@Slf4j(topic = "t.Sync")
public class LockSleepInterrupt {
//同步锁
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
lock.lock();
try {
log.debug("获取锁,线程开始沉睡5s");
Thread.sleep(5000);
} catch (InterruptedException e) {
log.debug("线程被其他线程唤醒,继续执行任务");
} finally {
lock.unlock();
log.debug("释放锁");
}
}, "threadA");
Thread threadB = new Thread(() -> {
lock.lock();
try {
log.debug("获取锁");
} finally {
lock.unlock();
log.debug("释放锁");
}
}, "threadB");
threadA.start();
Thread.sleep(2000);
threadB.start();
log.debug("线程A状态:{}",threadA.getState());
threadA.interrupt();
}
//10:41:46.874 t.Sync [threadA] - 获取锁,线程开始沉睡5s
//10:41:48.872 t.Sync [main] - 线程A状态:TIMED_WAITING
//10:41:48.874 t.Sync [threadA] - 线程被其他线程唤醒,继续执行任务
//10:41:48.874 t.Sync [threadA] - 释放锁
//10:41:48.874 t.Sync [threadB] - 获取锁
//10:41:48.874 t.Sync [threadB] - 释放锁
}
wait
//定义任意对象作为锁对象,A线程获取锁后调用wait()方法进入等待状态,然后线程B获取锁,执行完任务后调用notify()或notifyAll()唤醒线程A
@Slf4j(topic = "t.Sync")
public class ObjectWaitNotify {
private static final Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (lock) {
log.debug("获取锁");
try {
//线程A进入等待状态,释放锁
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("被唤醒,重新获取锁");
log.debug("释放锁");
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (lock) {
log.debug("获取锁");
//唤醒A线程
lock.notify();
log.debug("释放锁");
}
}, "threadB");
threadA.start();
Thread.sleep(200);
threadB.start();
}
//11:21:39.025 t.Sync [threadA] - 获取锁
//11:21:39.224 t.Sync [threadB] - 获取锁
//11:21:39.224 t.Sync [threadB] - 释放锁
//11:21:39.224 t.Sync [threadA] - 被唤醒,重新获取锁
//11:21:39.224 t.Sync [threadA] - 释放锁
}
Java中怎么唤醒阻塞线程
**IO阻塞 **
- 当线程进行IO操作时,如果资源不可用,线程会被阻塞。通常不建议直接中止线程,因为这可能会导致资源泄露、数据损坏或其他未定义行为
sleep() 、join()、wait()等方法阻塞
- 对于 sleep() 和 join() 方法导致的阻塞,可以通过调用该线程的 interrupt() 方法来中断它。sleep()会抛出 InterruptedException,而 join() 会提前返回。
- 对于 wait() 方法导致的阻塞,需要使用 notify() 或 notifyAll() 方法来唤醒等待的线程。
创建线程的方式(重点)
- 继承Thread类
创建一个继承自Thread类的子类,并重写其run方法,该方法包含线程的执行逻辑。
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("Thread running");
}
public static void main(String[] args) {
// 创建线程对象
MyThread thread = new MyThread();
// 启动线程
thread.start();
}
}
- 实现Runnable接口
创建一个实现了Runnable接口的类,并实现其run方法。
通过实现Runnable接口创建线程是更加灵活的方法,因为Java不支持多重继承,而实现接口可以避免这个限制。
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("Runnable running");
}
public static void main(String[] args) {
// 创建Runnable对象
MyRunnable myRunnable = new MyRunnable();
// 创建线程对象,传入Runnable对象作为构造器参数
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
- 实现callable接口
创建一个实现了Callable接口的类,并实现其call方法。该方法包含线程的执行逻辑,并返回一个结果。
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程执行的逻辑
return "Callable result";
}
public static void main(String[] args) {
// 创建Callable任务对象
Callable<String> callable = new MyCallable();
// 使用ExecutorService创建线程池,提交Callable任务
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(callable);
try {
// 获取Callable任务的返回结果
String result = future.get();
System.out.println("Callable result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
executor.shutdown();
}
}
三种方式的区别
-
Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
-
Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
-
Callable: 与Runnable类似,但是Callable接口的call方法可以返回执行结果,并且可以抛出异常。
-
Thread 和 Runnable:执行任务,无返回值;异常处理有限。
-
Callable:返回执行结果,可抛出检查异常。
-
Callable接口扩展了Runnable接口,FutureTask类实现了RunnableFuture接口,使Callable任务可以在ExecutorService中兼容使用,以FutureTask形式提交给线程池执行。这样一来,Callable确实可以看作是一种更加功能强大的Runnable。
守护线程和非守护线程(重点)
守护线程
- 为所有⾮守护线程提供服务的线程。它们通常用于执行后台任务,如垃圾回收、定时器等。
- 通过调用Thread类的setDaemon(true)方法可以将线程设置为守护线程,但这一设置必须在线程启动之前进行,否则将抛出IllegalThreadStateException。
- 守护线程由于其随时可能被终止的特性,确实不适合执行重要的或需要持续进行的任务,如文件读写操作或需要完整执行逻辑的计算任务。
非守护线程(用户线程)
- 非守护线程是Java中线程的默认类型,它们负责执行程序的主体任务。
- JVM中所有的非守护线程都已完成执行或者程序通过调用System.exit()方法被显式地终止,否则JVM将继续运行。
- 通过Thread类或其子类创建的线程,未调用setDaemon(true)的话,默认就是非守护线程。
//守护线程模拟使用
@Slf4j(topic = "t.Sync")
public class GradeThread {
static class DaemonTask implements Runnable {
@Override
public void run() {
while (true) {
log.debug("守护线程在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
//创建一个线程
Thread daemonThread = new Thread(new DaemonTask());
//设置成守护线程
daemonThread.setDaemon(true);
daemonThread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束");
}
//14:26:26.663 t.Sync [Thread-0] - 守护线程在运行
//14:26:27.667 t.Sync [Thread-0] - 守护线程在运行
//14:26:28.668 t.Sync [Thread-0] - 守护线程在运行
//14:26:29.669 t.Sync [Thread-0] - 守护线程在运行
//14:26:30.669 t.Sync [Thread-0] - 守护线程在运行
//14:26:31.662 t.Sync [main] - 主线程结束
}
ThreadLocal(重点)
ThreadLocal为每个线程提供独立的变量副本,这些副本存储在当前线程的ThreadLocalMap 中。使得每个线程都可以独立地操作自己的变量副本,而不会影响其他线程的副本。这种方式避免了使用共享变量可能带来的竞态条件和同步问题,从而提升了程序的并发性能和可维护性。
底层实现
实现原理是通过在每个线程(Thread类)内部维护一个ThreadLocalMap,用于存储各个ThreadLocal对象与其对应的变量副本。当调用 get() 方法时,会根据当前线程获取相应的变量副本;而调用set() 方法则会设置当前线程的变量副本。
内存泄漏问题
如果在使用完 ThreadLocal 后没有及时调用 remove() 方法清除变量,可能会导致内存泄漏。这是因为 因为ThreadLocalMap的Entry对象持有ThreadLocal变量(key)的弱引用(Weak Reference),但持有value的强引用。即使ThreadLocal变量被垃圾回收了,Entry的key变为null,但value仍然不会被回收,除非显式调用remove()方法或者线程结束。
应用场景
经典的应⽤场景连接管理,即每个线程可以拥有自己的数据库连接或其他资源连接,这些连接在线程之间不共享,从而避免了同步和线程安全问题。此外,ThreadLocal还常用于事务管理、用户身份认证信息存储等场景,其中每个线程需要维护自己独立的上下文信息。
串⾏、并⾏、并发的区别(重点)
- 串⾏在时间上不可能发⽣重叠,前⼀个任务没完成,下⼀个任务就只能等着。
- 并⾏在时间上是重叠的,允许两个或多个任务在同一时间段内同时执行,且这些任务之间互不干扰。
- 并发允许多个任务同时开始执行,通过时间片轮转、抢占式调度或其他机制交替执行任务。
并发的三⼤特性(重点)
原子性
- 是指一个操作要么全部执行成功,要么全部不执行。
确保原子性可以通过使用 synchronized 关键字、Java 并发包中的锁(如 ReentrantLock)以及并发原子类(如 AtomicInteger、AtomicLong 等)来实现。
可见性
- 当多个线程访问同⼀个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
volatile 关键字和 synchronized块都可以确保可见性。
有序性
- 确保多线程环境下,程序执行的顺序按照代码的先后顺序来执行。
volatile本身就包含了禁⽌指令重排序的语义,⽽synchronized关键字是由“⼀个变量在同⼀时刻只允许⼀条线程对其进⾏lock操作”这条规则明确的。
怎么保证线程安全
线程安全通常指的是在多线程环境下,多个线程访问同一个对象或资源时,不会出现数据不一致或数据损坏的情况。
原子类
Java提供了一系列的原子类(如AtomicInteger、AtomicLong等),通过原子操作来保证对单个变量的操作是原子性的,即不可中断的。这些类利用CAS(Compare and Swap)机制来确保线程安全。
CAS机制它涉及三个操作数 —— 内存位置(V)、期望的原值(A)和新值(B)。当且仅当内存位置V的值等于预期原值A时,内存值 V才会被设置为新值B。
volatile关键字
volatile关键字用于声明变量,确保所有线程看到的该变量的值是最新的。它可以保证变量的可见性和禁止指令重排序优化,但不能保证复合操作的原子性。
锁机制
synchronized关键字是Java中最基本的锁机制,可以修饰方法或代码块,确保同一时间只有一个线程可以执行被synchronized保护的代码。Java并发包中的Lock接口提供了更灵活的锁机制,如可中断锁、定时锁等。
无状态设计
通过设计避免共享状态,每个线程处理的数据都是独立的,不涉及共享数据,自然也就避免了线程安全问题。
不可变设计
将共享变量设计为不可变对象,即创建后其状态不可修改。不可变对象在多线程环境下是线程安全的,例如Java中的String类就是一个不可变类。
ThreadLocal
ThreadLocal为每个线程提供变量的独立副本,从而避免了线程间对共享变量的访问冲突。但需要注意,ThreadLocal并不能解决所有线程安全问题,特别是需要跨线程共享数据时。
多线程上下文切换
多线程上下文切换是操作系统为了在多任务环境中实现并发执行而采取的一种技术。当CPU从一个线程切换到另一个线程时,它需要保存当前线程的执行上下文(如程序计数器、寄存器状态、内存管理等),以便将来能够恢复执行。然后,它会加载下一个线程的执行上下文,并继续执行。
线程上下文切换的原因:
- 时间片用完:CPU根据时间片轮转调度算法,在任务的时间片用完之后切换到下一个任务。
- IO阻塞:当线程等待IO操作时,它会被阻塞,CPU会切换到其他线程以充分利用资源。
- 锁竞争:当多个线程试图访问同一资源时,没有获得锁的线程会被挂起,直到锁被释放。
- 用户代码显式挂起:通过某些系统调用或同步机制,用户代码可以主动让出CPU时间。
- 硬件中断:硬件事件(如键盘输入、网络数据包到达等)会触发中断,导致CPU从当前线程切换到中断处理程序。
如何减少上下文切换:
- 无锁并发编程:通过避免使用锁来减少线程间的阻塞和上下文切换
- CAS算法:原子操作,用于实现无锁数据结构
- 使用最少线程:设置合理设置线程池的大小。
多线程的好处
减少程序响应时间
提高CPU利用率
创建和切换开销小
数据共享效率高
简化程序结构
线程池的实现(重点)
在Java中,ThreadPoolExecutor是线程池的核心实现类,它位于java.util.concurrent包下。通过配置不同的参数(如核心线程数、最大线程数、工作队列、空闲存活时间等),ThreadPoolExecutor能够灵活地管理线程池中的线程,以高效地执行并发任务。
线程池状态(重点)
-
RUNNING(运行)
线程池被一旦被创建,就处于运行状态。此时线程池可以接收新的任务,处理排队的任务。 -
SHUTDOWN(关闭)
调用shutdown()方法关闭线程池,线程池由运行状态变成关闭状态。
此时线程池不接收新任务,但是会处理队列中已经存在的任务。当队列为空时,所有线程都将尝试终止,但已经开始的任务将继续执行直到完成。 -
STOP(停止)
调用线程池的shutdownNow()方法的时候,线程池由((运行或者关闭 ) 状态变成 停止状态。
此时线程池不接收新任务,不处理队列中的任务,并且会中断正在执行的任务。 -
TIDYING(整理)
当所有的任务已终止,记录的”任务数量”为0,线程池会变为整理状态,等待其terminated()钩子函数被执行。 -
TERMINATED(终止)
当钩子函数terminated()被执行完成之后,线程池已完全终止。
线程池参数(重点)
-
corePoolSize 线程池的核心线程数
-
maximumPoolSize 线程池的最大线程数,包括核心线程数和非核心线程数
-
keepAliveTime 超出核⼼线程数之外的线程的空闲存活时间
-
unit 表示 keepAliveTime 的时间单位
-
workQueue ⽤来存放待执⾏任务的阻塞队列。当任务提交过来时,如果线程池中的线程数已经达到 corePoolSize,新任务会被放入阻塞队列中等待执行。
-
threadFactory 线程工厂,用于创建新线程。可以自定义线程工厂。
-
handler 任务拒绝策略:当阻塞队列已满,且线程池中的线程数达到最大线程数时,如何处理新提交的任务的策略。
常见的拒绝策略
- AbortPolicy(直接抛出异常)
- CallerRunsPolicy(调用者线程执行该任务)
- DiscardPolicy(丢弃任务)
- DiscardOldestPolicy(弃队列中等待最久的任务,然后尝试重新提交当前任务)
- 自定义拒绝策略
线程池的底层⼯作原理(重点)
ThreadPoolExecutor内部使用Worker类来表示工作线程,Worker类实现了Runnable接口,并持有要执行的任务。当Worker线程被启动时,它会不断地从工作队列中取出任务并执行。
线程池内部是通过队列+线程实现的,当新任务提交时
- 如果线程池中的线程数小于核心线程数,即使有空闲线程,也会创建新线程来执行任务。
- 如果线程池中的线程数等于核心线程数且阻塞队列未满,则将任务放入阻塞队列中。
- 如果阻塞队列已满但线程池中的线程数小于最大线程数,则创建新线程来执行任务。
- 如果阻塞队列已满且线程池中的线程数等于最大线程数,则根据拒绝策略处理新任务。
当线程池中的线程数量大于核心线程数时,如果某个线程的空闲时间超过了配置的空闲存活时间,则这个线程将被终止。这有助于动态调整线程池中的线程数量,以优化资源使用。
线程池中阻塞队列的作⽤
- 阻塞队列作为一个有限长度的缓冲区,用于存储待执行的任务。当队列达到其容量限制时,阻塞队列会阻止额外的任务加入。
- 阻塞队列通过内置的同步机制,当队列为空时,尝试从队列中获取任务的线程会被阻塞,从而释放CPU资源;当队列中有任务时,被阻塞的线程会被唤醒,并从队列中取出任务执行。
- 当没有任务可执行时,核心线程可以通过阻塞队列的take()方法被挂起,而不是被销毁或处于忙等状态
使用线程池的好处(重点)
-
降低资源消耗:提⾼线程利⽤率,降低创建和销毁线程的消耗
-
提高响应速度:当任务到达时,可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性:使⽤线程池可以统⼀分配调优监控线程。
-
提高系统的稳定性:限制线程的数量,可以确保系统不会因为线程过多而崩溃,提高了系统的稳定性。
创建线程池方式(重点)
通过Executors类的方法创建线程池
newFixedThreadPool()-创建一个固定大小的线程池
/*
*线程池中的线程数固定为5,所以最多可以有5个线程同时执行任务。
*超过5个的任务会进入等待队列,等待线程池中的线程变得空闲后再执行。
*/
public class FixedThreadPool {
public static void main(String[] args) {
//创建固定大小为5的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//使用线程池执行任务1
for (int i = 0; i < 5; i++) {
//给线程池添加任务
// fixedThreadPool.submit(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName() + "执行任务1");
// }
// });
//简化
fixedThreadPool.submit(() -> System.out.println(Thread.currentThread().getName() + "执行任务1"));
}
//使用线程池执行任务2
for (int i = 0; i < 8; i++) {
//给线程池添加任务
fixedThreadPool.submit(() -> System.out.println(Thread.currentThread().getName() + "执行任务2"));
}
//pool-1-thread-2执行任务1
//pool-1-thread-4执行任务1
//pool-1-thread-5执行任务1
//pool-1-thread-1执行任务1
//pool-1-thread-3执行任务1
//pool-1-thread-1执行任务2
//pool-1-thread-5执行任务2
//pool-1-thread-4执行任务2
//pool-1-thread-2执行任务2
//pool-1-thread-4执行任务2
//pool-1-thread-5执行任务2
//pool-1-thread-1执行任务2
//pool-1-thread-3执行任务2
}
}
newCachedThreadPool()-带缓存的线程池
适用于短时间有大量任务的场景,但有可能会占用更多的资源。线程的数量由cpu根据任务量而定。
/*
*线程的数量由cpu根据任务量而定。
*/
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//执行50个任务
for (int i = 0; i < 50; i++) {
int finalI = i;
cachedThreadPool.submit(()->{
System.out.println(finalI+"线程名"+Thread.currentThread().getName() + "执行任务");
});
}
//执行了两次:一次有18个线程,一次有19个线程;线程的数量由cpu根据任务量而定。
//线程名pool-1-thread-1执行任务
//4线程名pool-1-thread-5执行任务
//3线程名pool-1-thread-4执行任务
//2线程名pool-1-thread-3执行任务
//1线程名pool-1-thread-2执行任务
//7线程名pool-1-thread-8执行任务
//6线程名pool-1-thread-7执行任务
//5线程名pool-1-thread-6执行任务
//9线程名pool-1-thread-10执行任务
//8线程名pool-1-thread-9执行任务
//10线程名pool-1-thread-3执行任务
//17线程名pool-1-thread-5执行任务
//15线程名pool-1-thread-8执行任务
//13线程名pool-1-thread-6执行任务
//21线程名pool-1-thread-8执行任务
//14线程名pool-1-thread-7执行任务
//11线程名pool-1-thread-9执行任务
//25线程名pool-1-thread-7执行任务
//20线程名pool-1-thread-11执行任务
//23线程名pool-1-thread-3执行任务
//22线程名pool-1-thread-5执行任务
//12线程名pool-1-thread-10执行任务
//19线程名pool-1-thread-1执行任务
//16线程名pool-1-thread-2执行任务
//36线程名pool-1-thread-3执行任务
//18线程名pool-1-thread-4执行任务
//38线程名pool-1-thread-3执行任务
//32线程名pool-1-thread-14执行任务
//35线程名pool-1-thread-5执行任务
//28线程名pool-1-thread-13执行任务
//33线程名pool-1-thread-10执行任务
//24线程名pool-1-thread-12执行任务
//34线程名pool-1-thread-1执行任务
//31线程名pool-1-thread-9执行任务
//29线程名pool-1-thread-7执行任务
//30线程名pool-1-thread-11执行任务
//27线程名pool-1-thread-6执行任务
//26线程名pool-1-thread-8执行任务
//47线程名pool-1-thread-18执行任务
//49线程名pool-1-thread-19执行任务
//44线程名pool-1-thread-17执行任务
//40线程名pool-1-thread-16执行任务
//48线程名pool-1-thread-10执行任务
//46线程名pool-1-thread-13执行任务
//45线程名pool-1-thread-5执行任务
//37线程名pool-1-thread-15执行任务
//43线程名pool-1-thread-4执行任务
//42线程名pool-1-thread-3执行任务
//41线程名pool-1-thread-14执行任务
//39线程名pool-1-thread-2执行任务
}
}
newSingleThreadExecutor()-创建单个线程的线程池
这个线程池只有一个核心线程在工作,相当于单线程串行化执行所有任务。
如果唯一的线程因为异常而结束,SingleThreadExecutor 会创建一个新的线程来替换它。
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
int finalI = i;
singleThreadExecutor.submit(()->{
System.out.println(finalI+"线程名"+Thread.currentThread().getName() + "提交任务");
});
}
//0线程名pool-1-thread-1提交任务
//1线程名pool-1-thread-1提交任务
//2线程名pool-1-thread-1提交任务
//3线程名pool-1-thread-1提交任务
//4线程名pool-1-thread-1提交任务
}
}
newSingleThreadScheduledExecutor()-创建执行定时任务的单个线程的线程池
public class SingleThreadScheduledExecutor {
public static void main(String[] args) {
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
System.out.println("添加任务"+ LocalDateTime.now());
singleThreadScheduledExecutor.schedule(() -> {
System.out.println("执行任务"+LocalDateTime.now());
},3, TimeUnit.SECONDS);
}
//添加任务2024-06-25T11:18:02.899
//执行任务2024-06-25T11:18:05.931
}
newScheduledThreadPool()-创建执行定时任务的线程池
public class ScheduledThreadPool {
public static void main(String[] args) {
//指定线程个数5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
System.out.println("添加任务"+ LocalDateTime.now());
//执行任务
//once(scheduledThreadPool);
//many(scheduledThreadPool);
manyRete(scheduledThreadPool);
}
/**
* 执行一次的定时任务,3s后执行
* @param scheduledThreadPool
*/
private static void once(ScheduledExecutorService scheduledThreadPool) {
scheduledThreadPool.schedule(()->{
System.out.println("执行任务"+LocalDateTime.now());
},3, TimeUnit.SECONDS);
}
//添加任务2024-06-25T11:49:47.269
//执行任务2024-06-25T11:49:50.282
/**
* 执行多次的定时任务
* scheduleWithFixedDelay(任务,第一次任务执行延迟时间,一次任务执行结束和下一次任务执行开始之间的间隔时间,时间单位)
* @param scheduledThreadPool
*/
private static void many(ScheduledExecutorService scheduledThreadPool) {
scheduledThreadPool.scheduleWithFixedDelay(()->{
System.out.println("线程"+Thread.currentThread().getName()+"执行任务"+LocalDateTime.now());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//相当于第一次任务执行延迟3s,后面的任务执行延迟4s
},3,2, TimeUnit.SECONDS);
}
//添加任务2024-06-25T13:42:02.154
//线程pool-1-thread-1执行任务2024-06-25T13:42:05.167
//线程pool-1-thread-1执行任务2024-06-25T13:42:09.168
//线程pool-1-thread-2执行任务2024-06-25T13:42:13.169
//线程pool-1-thread-1执行任务2024-06-25T13:42:17.170
//线程pool-1-thread-3执行任务2024-06-25T13:42:21.171
//线程pool-1-thread-3执行任务2024-06-25T13:42:25.171
//线程pool-1-thread-3执行任务2024-06-25T13:42:29.172
//线程pool-1-thread-3执行任务2024-06-25T13:42:33.173
/**
* 执行多次的定时任务
* scheduleAtFixedRate(任务,第一次任务执行延迟时间,间隔时间,时间单位)
* 推迟3秒执行;上一次任务开始2s后,下一个任务开始执行
* @param scheduledThreadPool
*/
private static void manyRete(ScheduledExecutorService scheduledThreadPool) {
scheduledThreadPool.scheduleAtFixedRate(()->{
System.out.println("线程"+Thread.currentThread().getName()+"执行任务"+LocalDateTime.now());
try {
//设置任务执行时间
//执行时间大于间隔时间,以执行时间为准
//Thread.sleep(3000);
//间隔时间大于执行时间,以间隔时间为准
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
},3,2,TimeUnit.SECONDS);
}
//执行时间3s大于间隔时间2s,以执行时间为准
//添加任务2024-06-25T14:01:12.179
//线程pool-1-thread-1执行任务2024-06-25T14:01:15.194
//线程pool-1-thread-1执行任务2024-06-25T14:01:18.196
//线程pool-1-thread-2执行任务2024-06-25T14:01:21.197
//线程pool-1-thread-1执行任务2024-06-25T14:01:24.198
//线程pool-1-thread-3执行任务2024-06-25T14:01:27.198
//间隔时间2s大于执行时间0.1s,以间隔时间为准
//添加任务2024-06-25T14:15:37.415
//线程pool-1-thread-1执行任务2024-06-25T14:15:40.428
//线程pool-1-thread-1执行任务2024-06-25T14:15:42.426
//线程pool-1-thread-2执行任务2024-06-25T14:15:44.427
//线程pool-1-thread-1执行任务2024-06-25T14:15:46.426
//线程pool-1-thread-3执行任务2024-06-25T14:15:48.426
}
newWorkStealingPool()-创建了一个工作窃取线程池
每个线程都有一个任务队列存放任务,一般自己的本地队列采取LIFO(后进先出),窃取时采用FIFO(先进先出),一个从头开始执行,一个从尾部开始执行,由于窃取的动作十分快速,会大量降低这种冲突,也是一种优化方式。
使用后台线程来执行任务,根据当前设备的配置自动决定线程池中线程数目。主线程不会等待所有任务执行完毕,而是在提交完任务后就继续执行。
public class WorkStealingPool {
public static void main(String[] args) {
ExecutorService workStealingPool = Executors.newWorkStealingPool();
//获取当前系统CPU核心数
System.out.println("获取当前系统CPU核心数"+Runtime.getRuntime().availableProcessors());
for (int i = 0; i < 50; i++) {
int fromIndex=i;
workStealingPool.submit(()->{
System.out.println(fromIndex+"线程名"+Thread.currentThread().getName() + "执行任务");
});
}
//1.1使用workStealingPool.isTerminated()方法判断线程池中的任务是否全部执行完毕
// while (!workStealingPool.isTerminated()){
//
// }
// 1.2关闭线程池
workStealingPool.shutdown();
try {
// 等待线程池中的任务执行完毕,最多等待10s
if (!workStealingPool.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("等待超时,部分任务可能未执行完毕");
//强制关闭未执行完毕的任务
workStealingPool.shutdownNow();
}
} catch (InterruptedException e) {
System.out.println("等待被中断");
//强制关闭未执行完毕的任务
workStealingPool.shutdownNow();
}
System.out.println("所有任务执行完毕");
}
//获取当前系统CPU核心数12
//0线程名ForkJoinPool-1-worker-9执行任务
//1线程名ForkJoinPool-1-worker-2执行任务
//2线程名ForkJoinPool-1-worker-11执行任务
//3线程名ForkJoinPool-1-worker-9执行任务
//4线程名ForkJoinPool-1-worker-2执行任务
//6线程名ForkJoinPool-1-worker-11执行任务
//10线程名ForkJoinPool-1-worker-11执行任务
//11线程名ForkJoinPool-1-worker-13执行任务
//12线程名ForkJoinPool-1-worker-11执行任务
//14线程名ForkJoinPool-1-worker-6执行任务
//5线程名ForkJoinPool-1-worker-4执行任务
//17线程名ForkJoinPool-1-worker-8执行任务
//18线程名ForkJoinPool-1-worker-4执行任务
//19线程名ForkJoinPool-1-worker-1执行任务
//7线程名ForkJoinPool-1-worker-9执行任务
//16线程名ForkJoinPool-1-worker-6执行任务
//15线程名ForkJoinPool-1-worker-11执行任务
//13线程名ForkJoinPool-1-worker-13执行任务
//28线程名ForkJoinPool-1-worker-11执行任务
//27线程名ForkJoinPool-1-worker-12执行任务
//26线程名ForkJoinPool-1-worker-6执行任务
//24线程名ForkJoinPool-1-worker-3执行任务
//25线程名ForkJoinPool-1-worker-10执行任务
//23线程名ForkJoinPool-1-worker-9执行任务
//22线程名ForkJoinPool-1-worker-1执行任务
//9线程名ForkJoinPool-1-worker-15执行任务
//21线程名ForkJoinPool-1-worker-4执行任务
//8线程名ForkJoinPool-1-worker-2执行任务
//20线程名ForkJoinPool-1-worker-8执行任务
//39线程名ForkJoinPool-1-worker-2执行任务
//38线程名ForkJoinPool-1-worker-4执行任务
//37线程名ForkJoinPool-1-worker-15执行任务
//36线程名ForkJoinPool-1-worker-1执行任务
//35线程名ForkJoinPool-1-worker-9执行任务
//34线程名ForkJoinPool-1-worker-10执行任务
//33线程名ForkJoinPool-1-worker-3执行任务
//32线程名ForkJoinPool-1-worker-6执行任务
//31线程名ForkJoinPool-1-worker-12执行任务
//30线程名ForkJoinPool-1-worker-11执行任务
//29线程名ForkJoinPool-1-worker-13执行任务
//49线程名ForkJoinPool-1-worker-12执行任务
//48线程名ForkJoinPool-1-worker-6执行任务
//47线程名ForkJoinPool-1-worker-3执行任务
//46线程名ForkJoinPool-1-worker-10执行任务
//45线程名ForkJoinPool-1-worker-9执行任务
//44线程名ForkJoinPool-1-worker-1执行任务
//43线程名ForkJoinPool-1-worker-15执行任务
//42线程名ForkJoinPool-1-worker-4执行任务
//41线程名ForkJoinPool-1-worker-2执行任务
//40线程名ForkJoinPool-1-worker-8执行任务
//所有任务执行完毕
}
通过ThreadPoolExecutor类手动创建线程池
public class MyThreadPool {
public static void main(String[] args) {
//线程工厂 r-Runnable
ThreadFactory factory = r -> {
Thread thread = new Thread(r);
return thread;
};
//手动创建线程池
ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor(2, 4, 3, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2), factory,
//1.提示异常,拒绝执行多余的任务
// new ThreadPoolExecutor.AbortPolicy()
//2.忽略堵塞队列中最旧的任务
//new ThreadPoolExecutor.DiscardOldestPolicy()
//3.忽略最新的任务
//new ThreadPoolExecutor.DiscardPolicy()
//4.使用调用该线程池的线程来执行任务
//new ThreadPoolExecutor.CallerRunsPolicy()
//5.A自定义拒绝策略
(r, executor) -> System.out.println("自定义拒绝策略")
);
//任务
for (int i=0;i<7;i++){
int finalI=i;
threadPoolExecutor.submit(()->{
try {
Thread.sleep(finalI*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行任务"+finalI);
});
}
}
}
2.锁
互斥、同步、锁重入
互斥:当一个线程访问某个共享资源时,其他线程不能访问该资源,直到占用线程释放该资源。
同步:确保多个线程按照一定的顺序访问共享资源,以避免数据不一致或其他问题。
锁重入:指的是一个线程可以重复获取已经持有的锁,而不会因为自己已经持有该锁而被阻塞。
锁的四大特性
原子性:是指一个操作或一系列操作要么全部执行成功,要么全部不执行。
可见性:是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改后的值。
有序性:确保多线程环境下,程序执行的顺序按照代码的先后顺序来执行。
可重入性:允许一个线程在持有锁的情况下再次请求该锁,而不会因为自己已经持有锁而被阻塞。
公平锁与非公平锁
公平锁:按照线程请求锁的顺序来分配锁,先到先得。
非公平锁:多个线程在尝试获取锁时,会直接尝试获取,不会按照等待队列中等待的顺序获取锁。
synchronized和ReentrantLock默认都是非公平锁,但ReentrantLock可以设置为公平锁。
共享式与独占式锁
共享式锁:允许多个线程同时访问某个资源。
例如,ReentrantReadWriteLock读锁是共享锁,允许多个线程同时持有读锁。
独占式锁:确保同一时刻只有一个线程能够访问某个资源。
例如,synchronized和ReentrantLock的写锁都是独占锁。
悲观锁与乐观锁
悲观锁:总是假设最坏的情况,认为数据在大部分情况下都会发生并发冲突,因此在数据处理过程中会锁定数据。
Java中的synchronized和ReentrantLock都是悲观锁的实现。
乐观锁:认为数据在大部分情况下不会发生冲突,因此不会立即锁定数据,而是在更新时检查数据是否被其他线程修改过。如果数据在检查期间被其他线程修改过,则更新操作会失败,并需要重试。Java中的AtomicInteger和CAS操作就是乐观锁的代表。
注意:乐观锁需要配合volatile修饰变量保证线程的可见性
什么是死锁
死锁是指两个或多个线程在竞争有限资源时,每个线程都在等待其他线程释放它所需要的资源,从而形成一个循环等待的僵局,导致所有涉及的线程都无法继续执行。这种情况下的线程被称为死锁线程。
死锁发生的条件(重点)
-
互斥使用:⼀个资源每次只能被⼀个线程使⽤。如果一个线程正在使用某个资源,其他线程必须等待,直到该资源被释放。
-
请求和保持:一个线程至少持有一个资源,并正在等待获取其他线程持有的资源。
-
不可剥夺:⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺。
-
循环等待:若⼲线程形成头尾相接的循环等待资源关系。即线程T1持有线程T2需要的资源,线程T2持有线程T3需要的资源,…,线程Tn持有线程T1需要的资源,从而形成一个闭环。
如何避免死锁(重点)
前三个条件(互斥使用、请求和保持、不可剥夺)是锁或资源管理的固有属性,难以或不应该被直接打破。因此,为了避免死锁,主要的策略是打破第四个条件:循环等待。
循环等待
- 为资源分配一个唯一的编号,并规定线程必须按照编号的升序(或降序)请求资源。这样,线程在请求资源时就会按照固定的顺序进行,从而避免了循环等待的情况。这种方法被称为资源有序分配法或资源分级法
虽然打破其他条件在某些特殊情况下可能也是可行的,但是会引入其他问题。
请求和保持
- 要求线程一次性申请它所需要的所有资源,如果不能满足,则等待直到所有资源都可用。
- 导致资源的浪费和利用率降低
不可剥夺
- 允许一个线程在等待资源时,被剥夺已经持有的资源
- 导致数据的不一致性。
CAS
CAS(Compare-and-Swap 比较并交换)是一种实现无锁并发控制的技术,用于在多线程环境中实现无锁数据结构。CAS算法通过比较内存值和期望值来确定是否进行值的更新,这避免了使用传统的锁机制,从而提高了并发性能。
CAS算法涉及到三个操作数:
内存值 V
期望值A
新值 B
当且仅当内存值V等于期望值A时,内存值V才会被设置为新值B。
举例:Java的java.util.concurrent.atomic包中的类(如AtomicInteger、AtomicLong等)就使用了CAS.
注意:CAS的原子性由CPU硬件指令实现保证。
CAS存在的问题
ABA问题
CAS在检查变量值的时候,只会检查值是否发生了变化,而不会检查这个值中间是否曾经被修改过。这就是ABA问题(即一个变量从A到B再到A,CAS检查时发现它仍然是A,误认为它从来没有被修改过)。
解决思路
在变量前面添加版本号或者时间戳,每次变量更新的时候都更新版本号或时间戳。这样即使值从A到B再到A,但由于版本号或时间戳不同,CAS操作也会识别出这个变量实际上已经被修改过。
循环时间长开销大
CAS操作如果长时间不成功,会导致其一直循环执行,给CPU带来非常大的开销。
解决思路
可以设置重试次数限制或者引入退避机制,以减少CPU的开销。
只能保证一个共享变量的原子操作
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
解决思路
对于涉及多个共享变量的操作,可能需要使用更复杂的同步机制,或者使用互斥锁 。
AQS框架(重点)
- AQS是一个用于构建锁和同步器的框架。
- AQS使用了一个 volatile int 类型的 state 变量来表示锁的状态,一个指向当前持有锁的线程的引用,这个state 变量对所有线程可见,并通过 CAS操作来保证操作的原子性。
- 在可重⼊锁的场景下,state就⽤来表示加锁的次数。0表示⽆锁,每加⼀次锁,state就加1,释放锁state就减1。
- AQS内部维护了一个基于 FIFO (先进先出)的队列来管理等待获取锁的线程。
- AQS支持与Condition接口兼容的条件变量,每个Condition对象都维护一个独立的等待队列,允许线程在同步队列和等待队列之间切换。
synchronized
使用方式
方法锁:当 synchronized 修饰一个普通方法时,锁的是当前实例对象(this)。当修饰一个静态方法时,锁的是当前类的 Class 对象。
代码块锁:synchronized 修饰代码块时,通过指定对象作为锁,可以实现更细粒度的锁控制,这个对象可以是任意对象。
//方法锁
public synchronized void instanceMethod() {
// synchronized代码块
}
public static synchronized void staticMethod() {
// synchronized代码块
}
//代码块锁
//锁对象
private static final Object lock = new Object();
//同步块
synchronized (lock) { ... }
作用:确保在同一时刻只有一个线程可以执行被 synchronized 保护的代码块或方法,以解决多线程并发访问共享资源可能引发的数据不一致或数据竞争问题。
这种锁在 JVM 内部实现,并由 JVM 提供支持和优化。
特点
隐式锁:与显式锁(如ReentrantLock)不同,synchronized不需要显式地编写代码去获取和释放锁
可重入锁:当一个线程已经持有一个对象的锁时,它可以再次请求该对象的锁,而不会造成死锁。这是 synchronized 的重要特性之一。
非公平锁:多个线程在尝试获取锁时,会直接竞争,不按照等待的顺序获取锁。
锁升级:在 Java 6 之后,synchronized 进行了优化,引入了锁的升级机制(从偏向锁到轻量级锁再到重量级锁),以减少性能开销。
悲观锁:假设竞争总是会发生,所以在访问共享资源时都会加锁。
package com.cj.thread.sync;
/**
* @Author cyc
* @Description synchronized测试案例
* @Date 2024/4/1 16:21
* @Version 1.0
*/
public class SyncTest {
private final Object lock = new Object();
public void SyncMethod(){
synchronized (lock) {
//同步代码块,只有一个线程可以执行这里的代码
System.out.println("当前线程名" + Thread.currentThread().getName());
//退出同步代码块时,自动释放锁
}
}
public static void main(String[] args) {
SyncTest syncTest=new SyncTest();
Thread thread1 = new Thread(() -> syncTest.SyncMethod(), "Thread-1");
Thread thread2 = new Thread(() -> syncTest.SyncMethod(), "Thread-2");
thread1.start();
thread2.start();
}
}
偏向锁、轻量级锁和重量级锁(重点)
偏向锁
- 当一个线程访问同步块并获取锁时,JVM会在锁对象的对象头中记录持有锁的线程ID。如果下一次该线程再次进入这个同步块,JVM会检查对象头中的线程ID是否匹配当前线程ID,如果匹配,则直接允许该线程进入同步块,无需再进行任何同步操作。
轻量级锁
- 当有其他线程尝试访问已经被偏向锁持有的同步块时,偏向锁就会升级为轻量级锁。轻量级锁依赖CAS操作尝试获取锁,如果获取锁失败,则当前线程会进行自旋,即不断尝试重新获取锁,而不会立即被阻塞。
重量级锁
- 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁。重量级锁的实现依赖于操作系统的互斥量,这会导致线程阻塞和上下文切换。
ReentrantLock(重点)
功能
- ReentrantLock 提供了与 synchronized 关键字相似的互斥锁功能,但提供了更高的灵活性和控制力。
- 需要显式地调用 lock() 方法来获取锁,并在 finally 块中调用 unlock() 方法来释放锁,这有助于避免死锁和资源泄漏。
- 提供了额外的功能,如可中断的锁获取(lockInterruptibly())、尝试非阻塞地获取锁(tryLock())、支持条件变量(通过 Condition 对象)以及公平锁和非公平锁的选择。
实现原理
- ReentrantLock的实现基于AQS框架。
- AQS 使用了一个 volatile int 类型的 state 变量来表示锁的状态,这个变量对所有线程可见,并通过 CAS操作来保证操作的原子性。
- AQS 内部维护了一个基于 FIFO (先进先出)的队列来管理等待获取锁的线程。
可重入性
- ReentrantLock 是支持可重入的,即同一个线程可以多次获得同一个锁。
- 当一个线程成功获取锁时,会检查当前线程是否是锁的持有者(通过记录当前线程的一个标识)。如果是,那么 state 变量的值会增加,表示锁的重入次数。
- 只有当 state 变量的值减少到 0 时,锁才会被完全释放,此时其他线程才能获取锁。
ReentrantLock默认是非公平锁,但可以设置为公平锁。
ReentrantLock的公平锁和非公平锁区别(重点)
线程在使⽤lock()⽅法加锁时
-
如果是公平锁,首先检查AQS队列中是否已经有线程在排队。如果有线程在排队,那么当前线程加入到队列的末尾并等待,按请求顺序获锁,减少饥饿,但可能降低性能。
-
如果是非公平锁,不检查AQS队列中是否有线程在排队,线程直接尝试获取锁。如果获取锁失败,则加入AQS等待队列。不保证请求顺序,可能提高吞吐量但可能导致线程饥饿现象。
//ReentrantLock 非公平锁 公平锁测试案例
ReentrantLock的tryLock()和Lock()方法的区别(重点)
- tryLock():尝试非阻塞地获取锁,该⽅法不会阻塞线程,如果加到锁则返回true,没有加到则返回false。
- Lock():阻塞加锁,线程会阻塞直到加到锁,⽅法没有返回值。
synchronized和ReentrantLock的区别(重点)
- sychronized是⼀个关键字,修饰普通方法、静态方法和代码块;ReentrantLock是⼀个类,只能用于代码块
- sychronized会⾃动的加锁与释放锁;ReentrantLock需要程序员手动加锁和释放锁。
- sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁。
- sychronized是⾮公平锁,ReentrantLock可以选择⾮公平锁(默认)或公平锁。
- sychronized锁的信息保存在锁对象的对象头中,通过JVM内部的机制来管理锁的状态;ReentrantLock通过类中一个int类型的变量(通常是state)来标识锁的状态。
- sychronized存在一个锁升级的过程,从偏向锁、轻量级锁到重量级锁的升级;ReentrantLock的实现基于AQS框架。
ReentrantReadWriteLock
读写锁的原理是多个读操作不需要互斥 。写锁被某个线程持有,读锁将无法获得,只好等待对方操作结束 ,这样就不会读取到有争议的数据 。
public class RWLockDemo {
private final Map<String, String> m = new TreeMap<>();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Lock r = rw.readLock();
private final Lock w = rw.writeLock();
/**
* 查询数据
*
* @param key
* @return
*/
public String get(String key) {
r.lock();
System.out.println(Thread.currentThread().getName() + "获得读锁," + "当前时间" + System.currentTimeMillis());
try {
return m.get(key);
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName() + "释放读锁," + "当前时间" + System.currentTimeMillis());
}
}
/**
* 写入数据
*
* @param key
* @param value
* @return
*/
public String put(String key, String value) {
w.lock();
System.out.println(Thread.currentThread().getName() + "获得写锁," + "当前时间" + System.currentTimeMillis());
try {
return m.put(key, value);
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName() + "释放写锁," + "当前时间" + System.currentTimeMillis());
}
}
}
StampedLock
StampedLock是Java 8引入的一种新的锁机制,它提供了乐观读锁和悲观读写锁的能力
乐观读锁:当线程尝试获取乐观读锁时,会获取一个时间戳(stamp),然后使用validate方法检查乐观读锁在读取过程中,共享资源是否被修改。
validate方法返回true,读取过程中没有写操作,表明读取的数据是有效的。
validate方法返回false,读取的数据已经过时,需要重试。
乐观读锁不会阻塞其他读线程或写线程,可能读取到不一致的数据。
悲观读锁:会阻塞其他写线程的访问,但不会阻塞其他读线程。
悲观写锁:同一时间只能有一个线程持有写锁。当线程尝试获取写锁时,它会阻塞直到没有其他读锁或写锁被持有。
** 可重入性**:StampedLock 支持锁的可重入性,主要适用于写锁。
锁转换:StampedLock 允许线程将乐观读锁转换为悲观读锁或写锁,或将悲观读锁转换为写锁,前提是在转换过程中没有其他线程获得相应的锁。
public class StampedLockDemo {
//创建一个 StampedLock 实例
private final StampedLock stampedLock = new StampedLock();
//共享资源
private int balance = 0;
//使用乐观读锁读取余额
public int getBalanceWithOptimisticReadLock() {
//尝试获取乐观读锁
long stamp = stampedLock.tryOptimisticRead();
System.out.println(Thread.currentThread().getName() + "获得乐观读锁," + "当前时间" + System.currentTimeMillis());
//读取余额
int currentBalance = balance;
//检查乐观读锁在读取过程中是否被改变(比如被写锁干扰)
if (!stampedLock.validate(stamp)) {
// 如果无效,则使用悲观读锁重新读取
stamp = stampedLock.readLock();
try {
currentBalance = balance;
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
System.out.println(Thread.currentThread().getName() + "释放悲观读锁," + "当前时间" + System.currentTimeMillis());
}
}
System.out.println(Thread.currentThread().getName() + "释放乐观读锁," + "当前时间" + System.currentTimeMillis());
return currentBalance;
}
//使用悲观读锁读取余额
public int getBalanceWithPessimisticReadLock() {
//获取悲观读锁
long stamp = stampedLock.readLock();
try{
//读取余额
return balance;
}finally {
stampedLock.unlockRead(stamp);
}
}
//使用写锁更新余额
public void updateBalanceWithWriteLock(int amount) {
//获取写锁
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "获得写锁," + "当前时间" + System.currentTimeMillis());
try{
balance+=amount;
}finally {
//释放写锁
stampedLock.unlockWrite(stamp);
System.out.println(Thread.currentThread().getName() + "释放写锁," + "当前时间" + System.currentTimeMillis());
}
}
public static void main(String[] args) {
StampedLockDemo example = new StampedLockDemo();
// 模拟多线程环境下的读写操作
Runnable readTask = () -> {
int balance = example.getBalanceWithOptimisticReadLock();
System.out.println(Thread.currentThread().getName()+"读取到的余额(乐观读锁): " + balance);
};
Runnable writeTask = () -> {
example.updateBalanceWithWriteLock(100);
System.out.println(Thread.currentThread().getName()+"更新了余额(写锁), 新余额: " + example.getBalanceWithPessimisticReadLock());
};
// 启动多个读线程和写线程来模拟并发访问
// 注意:在实际应用中,应该控制线程的数量和执行顺序以避免过度竞争和潜在的死锁风险。
new Thread(readTask).start();
new Thread(readTask).start();
new Thread(readTask).start();
new Thread(readTask).start();
new Thread(writeTask).start();
new Thread(readTask).start();
new Thread(readTask).start();
// ... 可以继续启动更多线程进行测试
}
//Thread-0获得乐观读锁,当前时间1718708228317
//Thread-6获得乐观读锁,当前时间1718708228317
//Thread-5获得乐观读锁,当前时间1718708228317
//Thread-4获得写锁,当前时间1718708228317
//Thread-3获得乐观读锁,当前时间1718708228317
//Thread-2获得乐观读锁,当前时间1718708228317
//Thread-1获得乐观读锁,当前时间1718708228317
//Thread-2释放悲观读锁,当前时间1718708228317
//Thread-3释放悲观读锁,当前时间1718708228317
//Thread-4释放写锁,当前时间1718708228317
//Thread-3释放乐观读锁,当前时间1718708228318
//Thread-2释放乐观读锁,当前时间1718708228318
//Thread-1释放悲观读锁,当前时间1718708228317
//Thread-2读取到的余额(乐观读锁): 100
//Thread-3读取到的余额(乐观读锁): 100
//Thread-4更新了余额(写锁), 新余额: 100
//Thread-1释放乐观读锁,当前时间1718708228318
//Thread-1读取到的余额(乐观读锁): 100
//Thread-0释放悲观读锁,当前时间1718708228318
//Thread-6释放悲观读锁,当前时间1718708228318
//Thread-5释放悲观读锁,当前时间1718708228318
//Thread-6释放乐观读锁,当前时间1718708228318
//Thread-0释放乐观读锁,当前时间1718708228318
//Thread-6读取到的余额(乐观读锁): 100
//Thread-5释放乐观读锁,当前时间1718708228318
//Thread-0读取到的余额(乐观读锁): 100
//Thread-5读取到的余额(乐观读锁): 100
}
JUC同步器-CountDownLatch(重点)
CountDownLatch允许一个或多个线程等待其他线程完成一组操作。CountDownLatch 内部有一个计数器,其初始值在创建CountDownLatch对象时通过构造参数指定,这个值表示需要等待的操作或事件的数量。每当一个线程完成了其中一个操作,它就会调用countDown()方法来将计数器减一。当计数器的值达到零时,所有因调用await()方法而在等待的线程将被唤醒,继续执行。
场景模拟:跑步比赛 ,裁判需要等到所有的运动员(“其他线程”)都跑到终点,才能去算排名和颁奖
public class CountDownLatchDemo {
/**
* 指明计数数量,被等待线程调用countDown方法将计数器递减
*/
private CountDownLatch countDownLatch=new CountDownLatch(4);
//运动员类
private class Runner implements Runnable {
private int result;
public Runner(int result){
this.result = result;
}
@Override
public void run() {
try{
//线程沉睡多久==模拟当前运动员跑多久
System.out.println(Thread.currentThread().getName()+"跑步时间"+result+"秒");
Thread.sleep(result*1000);
//计数器减1
countDownLatch.countDown();
System.out.println("计数器"+countDownLatch.getCount());
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 开始跑步
*/
private void begin(){
long startTime = System.currentTimeMillis();
System.out.println("赛跑开始时间点"+startTime);
System.out.println("计数器开始数:"+countDownLatch.getCount());
Random random = new Random(startTime);
for (int i = 0; i < 4;i++) {
//随机设置跑步时间
int result = random.nextInt(3)+1;
new Thread(new Runner(result)).start();
}
try {
//线程使用await方法进行线程等待
countDownLatch.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("所有人都跑完了,跑步最久耗时"+(endTime-startTime));
}
public static void main(String[] args) {
CountDownLatchDemo c = new CountDownLatchDemo();
c.begin();
}
//赛跑开始时间点1718864239175
//计数器开始数:4
//Thread-0跑步时间1秒
//Thread-1跑步时间2秒
//Thread-2跑步时间2秒
//Thread-3跑步时间3秒
//计数器3
//计数器1
//计数器1
//计数器0
//所有人都跑完了,跑步最久耗时3002
}
JUC同步器-CyclicBarrier(重点)
- 循环栅栏允许一组线程互相等待。CyclicBarrier内部有一个计数器,其初始值在创建CyclicBarrier对象时通过构造参数指定。
- 当一个线程调用CyclicBarrier.await()方法时,它会尝试通过屏障点。
- 如果计数器的值大于0,表示还有其他线程尚未到达屏障点,那么当前线程会被阻塞,并等待其他线程。
- 当计数器的值减到0时,表示所有线程都到达了屏障点。此时,所有被阻塞的线程都会被释放,并允许继续执行。
- 同时,CyclicBarrier的屏障点会被重置,即计数器的值会被重新设置为初始值,等待下一组线程的到达。
应用场景:10个学生到饭店聚餐,10个人都到了就开饭
public class CyclicBarrierDemo1 {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
public static class Student extends Thread {
/**
* 走路时间
*/
private int runSeconds;
public Student(String name, int runSeconds) {
super(name);
this.runSeconds = runSeconds;
}
@Override
public void run() {
try {
//模拟学生到饭店时间
TimeUnit.SECONDS.sleep(runSeconds);
long startTime = System.currentTimeMillis();
System.out.println(this.getName()+"到饭店时,阻塞线程数:"+cyclicBarrier.getNumberWaiting());
//学生到了就等着
cyclicBarrier.await();
long endTime = System.currentTimeMillis();
System.out.println(this.getName() +"到饭店"+runSeconds+ "(s),到了等待了" + (endTime - startTime)+"(ms),开始吃饭了");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("计数值" + cyclicBarrier.getParties());
for (int i = 1; i <= 10; i++) {
new Student("员工" + i, i).start();
}
}
//计数值10
//员工1到饭店时,阻塞线程数:0
//员工2到饭店时,阻塞线程数:1
//员工3到饭店时,阻塞线程数:2
//员工4到饭店时,阻塞线程数:3
//员工5到饭店时,阻塞线程数:4
//员工6到饭店时,阻塞线程数:5
//员工7到饭店时,阻塞线程数:6
//员工8到饭店时,阻塞线程数:7
//员工9到饭店时,阻塞线程数:8
//员工10到饭店时,阻塞线程数:9
//员工10到饭店10(s),到了等待了0(ms),开始吃饭了
//员工1到饭店1(s),到了等待了9000(ms),开始吃饭了
//员工3到饭店3(s),到了等待了6999(ms),开始吃饭了
//员工5到饭店5(s),到了等待了5000(ms),开始吃饭了
//员工8到饭店8(s),到了等待了1999(ms),开始吃饭了
//员工4到饭店4(s),到了等待了6000(ms),开始吃饭了
//员工2到饭店2(s),到了等待了7999(ms),开始吃饭了
//员工9到饭店9(s),到了等待了1000(ms),开始吃饭了
//员工7到饭店7(s),到了等待了2999(ms),开始吃饭了
//员工6到饭店6(s),到了等待了4000(ms),开始吃饭了
}
CyclicBarrier-循环栅栏,屏障点重复使用
应用场景:10个学生到饭店聚餐,10个人都到了就开饭,学生吃完饭后,到车上准备去下一个景点.
public class CyclicBarrierDemo2 {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
private static class Student extends Thread {
private int time;
public Student(String name, int time) {
super(name);
this.time = time;
}
/**
* 学生到饭店就餐
*/
void runEat() {
try {
//学生到饭店时间
TimeUnit.SECONDS.sleep(time);
long startTime = System.currentTimeMillis();
//到了等待其他学生
cyclicBarrier.await();
long endTime = System.currentTimeMillis();
System.out.println(this.getName() + "到饭店" + time + "(s),到了等待了" + (endTime - startTime) + "(ms),开始吃饭了");
//模拟学生吃饭时间
TimeUnit.SECONDS.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 学生去下一站景点
*/
void drive() {
try {
long startTime = System.currentTimeMillis();
//到了等待其他学生
cyclicBarrier.await();
long endTime = System.currentTimeMillis();
System.out.println(this.getName() + "吃完饭等待了" + (endTime - startTime) + "(ms),开始去下一站景点");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
//学生到饭店,所有人到了一起吃饭
this.runEat();
//等待所有学生到齐,开车去下一站景点
this.drive();
}
}
public static void main(String[] args) {
System.out.println("计数值" + cyclicBarrier.getParties());
for (int i = 1; i <= 10; i++) {
new Student("学生" + i, i).start();
}
}
//计数值10
//学生10到饭店10(s),到了等待了0(ms),开始吃饭了
//学生6到饭店6(s),到了等待了4000(ms),开始吃饭了
//学生5到饭店5(s),到了等待了5000(ms),开始吃饭了
//学生4到饭店4(s),到了等待了6000(ms),开始吃饭了
//学生3到饭店3(s),到了等待了7000(ms),开始吃饭了
//学生1到饭店1(s),到了等待了9000(ms),开始吃饭了
//学生2到饭店2(s),到了等待了8000(ms),开始吃饭了
//学生9到饭店9(s),到了等待了1000(ms),开始吃饭了
//学生8到饭店8(s),到了等待了2000(ms),开始吃饭了
//学生7到饭店7(s),到了等待了3000(ms),开始吃饭了
//学生10吃完饭等待了0(ms),开始去下一站景点
//学生7吃完饭等待了3000(ms),开始去下一站景点
//学生9吃完饭等待了1000(ms),开始去下一站景点
//学生6吃完饭等待了4000(ms),开始去下一站景点
//学生5吃完饭等待了5000(ms),开始去下一站景点
//学生2吃完饭等待了8000(ms),开始去下一站景点
//学生4吃完饭等待了6000(ms),开始去下一站景点
//学生3吃完饭等待了7000(ms),开始去下一站景点
//学生1吃完饭等待了9000(ms),开始去下一站景点
//学生8吃完饭等待了2000(ms),开始去下一站景点
}
JUC同步器-Semaphore(重点)
- Semaphore(信号量),在创建Semaphore对象时可以设置许可的个数,表示同时最多允许多少个线程使⽤该信号量。
- 通过Semaphore.acquire()方法来尝试获取许可,如果没抢到许可,则线程会被阻塞,并加入到由AQS管理的等待队列中。
- 通过Semaphore.release()⽅法来释放许可后,如果等待队列中有线程在等待许可,那么AQS会从队列中取出第一个线程并唤醒它,让它尝试获取许可。这个过程会重复进行,直到没有更多的许可被释放或者没有线程在等待许可。
应用场景:8个线程抢许可执行任务
public class SemaphoreDemo1 {
private static class Worker extends Thread {
private int num;
private Semaphore semaphore;
public Worker(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
long startTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "尝试抢许可,没抢到等待");
semaphore.acquire();
//抢到许可
System.out.println(Thread.currentThread().getName() + "抢到了许可,开始执行任务...");
//执行任务
Thread.sleep(2000);
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "执行任务耗时"+(endTime - startTime));
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + "释放许可...");
}
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 8; i++) {
new Worker(i, semaphore).start();
}
}
//Thread-0尝试抢许可,没抢到等待
//Thread-3尝试抢许可,没抢到等待
//Thread-0抢到了许可,开始执行任务...
//Thread-2尝试抢许可,没抢到等待
//Thread-1尝试抢许可,没抢到等待
//Thread-6尝试抢许可,没抢到等待
//Thread-2抢到了许可,开始执行任务...
//Thread-5尝试抢许可,没抢到等待
//Thread-3抢到了许可,开始执行任务...
//Thread-4尝试抢许可,没抢到等待
//Thread-6抢到了许可,开始执行任务...
//Thread-7尝试抢许可,没抢到等待
//Thread-1抢到了许可,开始执行任务...
//Thread-6执行任务耗时2001
//Thread-0执行任务耗时2002
//Thread-6释放许可...
//Thread-3执行任务耗时2001
//Thread-2执行任务耗时2002
//Thread-1执行任务耗时2002
//Thread-2释放许可...
//Thread-5抢到了许可,开始执行任务...
//Thread-7抢到了许可,开始执行任务...
//Thread-3释放许可...
//Thread-4抢到了许可,开始执行任务...
//Thread-0释放许可...
//Thread-1释放许可...
//Thread-5执行任务耗时4002
//Thread-7执行任务耗时4002
//Thread-4执行任务耗时4002
//Thread-7释放许可...
//Thread-5释放许可...
//Thread-4释放许可...
}
应用场景:Semaphore信号量实现互斥锁,就是设置许可个数为1
public class SemaphoreDemo2 {
private static class Worker extends Thread {
private int num;
private Semaphore semaphore;
public Worker(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
//尝试抢许可,没抢到等待
semaphore.acquire();
//获取锁
System.out.println(Thread.currentThread().getName() + "获取锁,开始执行任务...");
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放许可
semaphore.release();
//释放锁
System.out.println(Thread.currentThread().getName() + "释放锁...");
}
}
}
public static void main(String[] args) {
//许可数量为1
Semaphore semaphore = new Semaphore(1);
for (int i = 0; i < 8; i++) {
new Worker(i, semaphore).start();
}
}
//Thread-0获取锁,开始执行任务...
//Thread-0释放锁...
//Thread-1获取锁,开始执行任务...
//Thread-1释放锁...
//Thread-3获取锁,开始执行任务...
//Thread-3释放锁...
//Thread-2获取锁,开始执行任务...
//Thread-2释放锁...
//Thread-5获取锁,开始执行任务...
//Thread-5释放锁...
//Thread-4获取锁,开始执行任务...
//Thread-4释放锁...
//Thread-6获取锁,开始执行任务...
//Thread-6释放锁...
//Thread-7获取锁,开始执行任务...
//Thread-7释放锁...
}
主内存和工作内存
主内存:所有的变量都存储在主内存,这些变量是多个线程共享的。
工作内存:每个线程都有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
八种主要内存间操作
这些规则定义了多线程环境中变量值的可见性和顺序性。
lock(锁定)和unlock(解锁)
lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,供其他线程锁定。
read(读取)和write(写入)
read:作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load操作。
write:作用于主内存变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
load(载入)和store(存储)
load:作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中。
store:作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作。
use(使用)和assign(赋值)
use:把工作内存中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign:将执行引擎返回的结果值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
volatile(重点)
保证可见性
- 当一个变量被声明为volatile时,它会保证修改的值会立即被更新到主内存。当有其他线程需要读取这个变量的值时,它会去主内存中读取新值,而不是使用线程自己的工作内存中的值。确保多个线程之间能够立即看到该变量的最新值。
禁止指令重排序
- volatile关键字可以禁止指令重排序,这意味着volatile变量的读写操作都是按照代码顺序执行的,不会被重排序优化,避免了由于指令重排序导致的数据不一致问题。
注意:volatile不保证原子性
例如,对于volatile int count = 0;,多个线程同时执行count++操作时,可能会出现线程安全问题,因为count++操作实际上包含三个步骤:读取、增加、写入,这些步骤可能会被其他线程打断。
volatile关键字适用于一些轻量级的线程同步场景,比如标志位等。
但对于复杂的线程同步场景,还是需要使用synchronized或者Lock等更强大的同步机制。
该博客聚焦Java线程与锁的知识。介绍了线程的概念、生命周期、创建方式、常见方法等,还阐述了线程安全的保证方法。同时,详细讲解了各类锁,如公平锁、非公平锁、悲观锁、乐观锁等,以及JUC同步器的使用,包括CountDownLatch、CyclicBarrier、Semaphore等。
3358

被折叠的 条评论
为什么被折叠?



