JUC基础
JUC简述
JUC实际上就是我们对于jdk中java.util.concurrent工具包的简称。这个包下的类都是和 **Java多线程开发 **相关的类。
线程与进程
-
程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。
-
软件:1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。
-
进程是对一个程序运行过程(创建-运行-消亡)的描述,系统会为每个运行的程序建立一个进程,并为进程分配独立的系统资源,比如内存空间等资源。
-
线程:线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源。
面试题:进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。对于方法区,堆中中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。
线程调度
指CPU资源如何分配给不同的线程。常见的两种线程调度方式:
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java采用的是抢占式调度方式。
-
抢占式调度详解
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
-
线程的创建方式
java虚拟机是支持多线程的,当运行Java程序时,至少已经有一个线程了,那就是main线程。
继承Thread类
Java中java.lang.Thread是表示线程的类,每个Thread类或其子类的实例代表一个线程对象。
通过继承Thread类来创建并启动多线程的步骤:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
测试类:创建线程对象并启动线程
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
注意事项:
-
手动调用run方法不是启动线程的方式,只是普通方法调用。
-
start方法启动线程后,run方法会由JVM调用执行。
-
不要重复启动同一个线程,否则抛出异常
IllegalThreadStateException -
不要使用Junit单元测试多线程,不支持,主线程结束后会调用
System.exit()直接退出JVM;
实现Runnable接口
Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法
通过实现Runnable接口创建线程并启动的步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
的线程对象。 - 调用线程对象的start()方法来启动线程。
自定义线程任务类:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
测试类:创建线程对象并启动线程
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
两种创建线程方式比较
-
Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。
public class Thread implements Runnable {} -
因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。
-
实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据。
利用Callable接口、FutureTask对象实现。
可以得到线程执行的结果 通过 FutureTask对象. get()
①、得到任务对象
1.定义类实现Callable接口,重写call方法,封装要做的事情。
import java.util.concurrent.Callable;
public class CallableThread implements Callable {
@Override
public Object call() throws Exception {
int sum =0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
2.用FutureTask把Callable对象封装成线程任务对象。
CallableThread ct = new CallableThread();
FutureTask fk = new FutureTask<>(ct);
②、把线程任务对象交给Thread处理。
③、调用Thread的start方法启动线程,执行任务
Thread thread = new Thread(futureTask);
thread.start();
④、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
System.out.println(futureTask.get()); //get()会堵塞,直到子线程执行结束
System.out.println(futureTask.get(6, TimeUnit.SECONDS));//当前主线程获取子线程的结果,默认阻塞等待,最多只阻塞等待6s
//callable类
import java.util.concurrent.Callable;
public class CallableThread implements Callable {
@Override
public Object call() throws Exception {
int sum =0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
//test
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableThread callableThread = new CallableThread();
FutureTask futureTask = new FutureTask<>(callableThread);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
创建线程3种方式总结
1、直接继承Thread类,线程和任务合并在一起,代码简单,但扩展性差,因为Java是单继承。
2、实现Runnable接口或者Callable接口。线程和任务进行了分离,扩展性强,我们的任务类还可以继续继承某一个类。
3、Runnable接口中的run方法没有返回值也没有异常,Callable中的call方法存在返回值也声明了异常。
Thread类核心API
-
public void run() :此线程要执行的任务在此处定义代码。
-
public String getName() :获取当前线程名称。
-
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
-
public final int getPriority() :返回线程优先级
-
public final void setPriority(int newPriority) :改变线程的优先级
- 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
线程名称
public final String getName() // 获取线程名称
public final void setName(String name) // 调用setName方法设置线程名称
public Thread(String name) // 构造方法设置线程名称
线程对象
获取当前正在执行该方法的线程对象:
public static native Thread currentThread(); // 获取当前执行该线程体的线程对象
线程休眠
public static void sleep(long time) // 让线程休眠指定的时间,单位为毫秒
TimeUnit.时间单位.sleep(时间值); // 使用时间枚举类让线程休眠
线程加入
把某一个线程加入到当前线程的执行流程中。
public final void join() throws InterruptedException
当某一个程序执行流程中调用了其他线程的join()方法,调用线程暂停执行,直到被join()方法加入的join线程执行完成为止。
注: 需要在线程启动以后在进行加入才有效
比如现在存在两个线程,一个t1线程 , 一个是t2线程,当我们t1线程执行到某一个时刻的时候,我们在t1线程的执行流中添加了t2线程,那么此时t1线程暂停执行,直到t2线程执行完毕以后t1线程才可以继续执行。
public class ThreadDemo01 {
public static void main(String[] args) {
// 我们在主线程的执行流中加入其它线程
for(int x = 0 ; x < 100 ; x++) {
// 当x的值等于20的执行加入其它线程
if(x == 10) {
// 创建MyThread线程对象
MyThread myThread = new MyThread();
myThread.setName("atguigu-01");
myThread.start();
// 调用join方法进行线程加入
try {
myThread.join(); // 需要在线程启动以后在进行加入才有效
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 主线程执行代码
System.out.println(Thread.currentThread().getName() + "--------->>" + x);
}
}
}
线程中断
interrupt方法
当调用线程的sleep方法时,可以让该线程处于等待状态,调用该线程的interrupt()方法就可以打断该阻塞状态,中断阻塞状态以后,继续执行(前提是别throw),而不是让线程结束,并且此方法会抛出一个InterruptedException异常。
public void interrupt(); // 中断线程的阻塞状态
案例:演示中断sleep的等待状态
线程类:
public class MyThread extends Thread {
@Override
public void run() {
for(int x = 0 ; x < 100 ; x++) {
System.out.println(Thread.currentThread().getName() + "----" + x );
if(x == 10) {
try {
TimeUnit.SECONDS.sleep(10000); // 线程休眠以后,该线程就处于阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();//如果这里throw,则当前线程的sleep被中断后,后续代码也就无法执行了
}
}
}
}
}
测试类:
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建MyThread线程对象
MyThread t1 = new MyThread();
t1.setName("chs-01");
// 启动线程
t1.start();
try {
// 主线程休眠2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断t1线程的休眠
t1.interrupt();
}
}
控制台输出结果
...
atguigu-01----10
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:339)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at com.atguigu.javase.thread.api.demo14.MyThread.run(MyThread.java:14)
atguigu-01----11
...
通过控制台的输出结果,我们可以看到interrupted方法并没有去结束当前线程,而是将线程的阻塞状态中断了,中断阻塞状态以后,线程chs-01继续进行执行。
stop方法
调用线程的stop方法可以让线程终止执行,没有异常。
public final void stop() // 终止线程的执行
线程类
public class MyThread extends Thread {
@Override
public void run() {
for(int x = 0 ; x < 100 ; x++) {
System.out.println(Thread.currentThread().getName() + "----" + x );
if(x == 10) {
try {
TimeUnit.SECONDS.sleep(10000); // 线程休眠以后,该线程就处于阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
测试类
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建MyThread线程对象
MyThread t1 = new MyThread();
t1.setName("chs-01");
// 启动线程
t1.start();
try {
// 主线程休眠2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 终止线程t1的执行
t1.stop();
}
}
控制台输出结果
...
chs-01----9
chs-01----10
控制台没有任何异常输出,程序结束,"chs-01"线程没有继续进行执行。
总结:interrupt方法、stop方法区别
interrupt用于优雅地请求线程中断,同时让线程决定如何处理该请求;它是安全的,推荐使用。stop是直接强制终止线程,不安全,可能导致数据不一致,已被弃用,不建议使用。
选择使用 interrupt 方法来管理线程的生命周期和控制是多线程编程的推荐做法。
守护线程
有一种线程是在后台运行的,它的任务就是为其他的线程提供服务,这种线程被称之为"后台线程",又被称之为"守护线程"。
JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程都结束,后台线程会自动结束,前后台线程都结束了,JVM就退出了。
常见的前台线程:主线程、之前创建的自定义线程...
创建守护线程
创建普通线程,调用其setDaemon(true)方法,即创建了守护线程。
public final void setDaemon(boolean on) // 将某一个线程设置为后台/守护线程
测试类
public class ThreadDemo01 {
public static void main(String[] args) {
// 开启两个线程
MyThread t1 = new MyThread();
t1.setName("关羽");
MyThread t2 = new MyThread();
t2.setName("张飞");
// 将关羽线程设置为守护线程(将某一个线程设置为守护线程,必须在启动线程之前)
t1.setDaemon(true);
t2.setDaemon(true);
// 启动线程
t1.start();
t2.start();
// 在主线程中编写代码
Thread.currentThread().setName("---------------刘备");
for(int x = 0 ; x < 5 ; x++) {
System.out.println(Thread.currentThread().getName() + "-----" + x);
}
/**
* 主线程 == 前台线程
* t1和t2 == 守护线程
* 当主线程执行完毕以后,剩下的线程都是守护线程了jvm就会终止
*/
}
}
注意:前台线程全部结束后,JVM会通知后台线程全部结束,但从它接收到指令到做出响应,需要一定时间,而在这一段时间内其他线程还可以继续执行。
线程安全问题
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,但是如果多个线程中对资源有读和写的操作,就会出现前后数据不一致问题,这就是线程安全问题。
解决问题思路
解决线程安全的思路: 就是将多个线程对共享数据的并发访问更改为串行访问。
串行访问(同步访问)就是指:一个共享数据一次只能被一个线程访问,该线程访问完毕以后其他的线程才可以访问。
要实现共享数据的串行访问,我们就需要使用锁机制来完成。
相关概念:
1、获取锁/申请锁:一个线程在访问共享数据之前,我们必须要申请锁,申请锁的这个过程我们将其称之为获取锁。
2、持有锁的线程: 一个线程获得了某一个锁,我们就将该线程称之为锁的持有线程。
3、临界区:获取锁 到 释放锁,这个区间称之为“临界区”,共享数据只能在临界区内进行访问,临界区一次只能被一个线程执行。
锁的持有线程可以对该锁所保护的共享数据进行访问,访问结束以后该线程就需要释放锁。

隐式锁(synchronized)
synchronized 实现的加锁和释放锁是自动的,不需要显示执行,称之为“隐式锁”
synchronized概述
synchronized锁是Java中用于控制 多线程访问共享资源 的工具。
它可以用来修饰 代码块 或者 方法 ,确保在同一时刻只有一个线程可以执行被修饰的代码。
当线程尝试获取锁时,如果锁被其他线程持有,那么当前线程会被阻塞,直到锁被释放。
也称之为"进程内"的锁。同一个进程内的多个线程,使用synchronized进行并发安全控制。
注意:
synchronized属于jvm层面的一把锁,不同的进程使用的jvm是不一样的,所以不同的进程之间的多个线程是不能使用synchronized进行并发安全控制的。
同步代码块的格式:
synchronized (对象) {
// 在此代码块中访问共享数据
}
//该对象可以是任意的对象,这个对象可以简单的理解就是一把锁,但是需要保证多个线程在访问的时候使用的是同一个对象。
同步方法的格式
public synchronized void sellTicket(){...}
public static synchronized void sellTicket(){...}
锁对象研究
思考问题:同步代码块、同步方法、静态同步方法的锁对象分别是谁?
1、普通同步方法,是实例锁,锁是当前实例对象。
2、静态同步方法,是类锁,锁是当前类的Class对象。
3、同步代码块,锁是synchonized括号里配置的对象。
死锁现象
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
显式锁(Lock)
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
- Lock属于api层面的一把锁,和synchronized都是属于进程内的一把锁。
| 方法名称 | 说明 |
|---|---|
| public ReentrantLock() | 获得Lock锁的实现类对象 |
| 方法名称 | 说明 |
|---|---|
| void lock() | 获取锁,如果锁已经被其他线程持有,调用线程将被阻塞直到锁可用 |
| void unlock() | 释放锁,必须在持有该锁的线程中调用 |
| boolean tryLock() | 尝试获取锁,如果锁可用则返回 true,否则返回 false。不会阻塞 |
| boolean tryLock(long timeout, TimeUnit unit) | 尝试获取锁,如果在给定的等待时间内能够获得锁,则返回 true,否则返回 false |
| Condition newCondition() | 返回一个 Condition 变量,用于实现等待/通知机制 |
| boolean isHeldByCurrentThread() | 判断当前线程是否持有此锁 |
| int getHoldCount() | 返回当前线程保持此锁的次数,仅在 ReentrantLock 中有效 |
| Thread getOwner() | 返回当前持有锁的线程,仅在 ReentrantLock 中有效 |
static Lock lock = new ReentrantLock();
lock.lock() //加锁
try{
//被锁的代码
}catch(Execption e){
//处理异常
}finally{
lock.unlock(); //释放锁
}
synchronized和Lock的区别:
1、前者属于jvm层面的锁,java中的一个关键字,锁数对像可以是某个实例对像,也可以是类对像Class,Lock属于api层面的一把锁。是juc包下的一个接口。
2、synchronized是非公平锁,不可中断锁,可重入的,悲观锁,独占锁,进程内锁。
Lock接口的实现类ReentrantLock,即可实现公平锁也可以实现非公平锁,可中断锁,可重入,悲观锁,进程内锁,独占锁(如果需要实现共享锁,需要使用其他的实现类ReentrantReadWriteLock).3、并发量不高的情况下,两者的性能没有区别。高并发情况下,Lock(pi层面的)效率更高,并目Lock更灵活。
4、synchronized隐式锁,Lock显示锁。
分布式锁
解决多进程之间的多个线程安全问题使用
- 跨进程协作: 分布式锁能够在多个进程或服务器之间进行协作,确保同一时刻只有一个进程或服务器能获得锁。
- 高可用性: 分布式锁跟随分布式系统的高可用特性,通常锁的实现需要在多个节点上具备容灾能力。
- 租约机制: 为了避免死锁,很多分布式锁都有租约机制,设计为锁会在一定时间后自动释放,即使持有锁的进程崩溃或者未进行解锁操作。
- 性能考虑: 分布式锁通常需要在网络上传输相关请求,所以在性能方面要尽量优化,降低锁的粒度和持有时间。
基于数据库的锁:
- 利用数据库的事务和锁机制,例如使用
SELECT ... FOR UPDATE或乐观锁。 - 缺点:会增加数据库的负担,并且可能受到数据库性能的限制。
基于 Redis 的锁:
-
利用 Redis 的
SETNX命令可以实现锁的获取,使用 TTL(过期时间)防止死锁。 -
Redis 的性能高、延迟低,适合用于分布式环境。
-
示例代码(伪代码):
if (SETNX("lock_key", "value")) { // 获取锁成功 // 处理业务逻辑 DEL("lock_key"); // 释放锁 }
基于 Zookeeper 的锁:
-
Zookeeper 用于维护分布式协调,通过创建临时节点的方式来实现分布式锁。
-
当持有锁的客户端崩溃时,临时节点将被删除,其他客户端可以获取到锁。
-
示例代码(伪代码):
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:2181", new ExponentialBackoffRetry(1000, 3)); InterProcessSemaphoreV2 lock = new InterProcessSemaphoreV2(client, "/lock_path", 1); lock.acquire(); // 获取锁 // 处理业务逻辑 lock.release(); // 释放锁
基于 Etcd 的锁:
- Etcd 是一个分布式键值存储,可以利用其原子性和可用性特性实现分布式锁。
使用分布式锁的场景
- 防止重复操作: 在多个请求同时处理时,防止同一操作被多次执行,如创建订单、发放优惠等。
- 限流: 对某些资源的访问频率进行限制。
- 数据一致性的保障: 确保在执行某些操作时数据状态的准确性,比如库存扣减时。
锁的分类
可重入锁和不可重入锁
Java的synchronized关键字和ReentrantLock类提供的锁都是可重入的。
可重入锁可以有效避免因先后获取同一把锁而导致的死锁。
可重入锁也叫作递归锁,指的是一个线程可以多次抢占同一个锁。
例如,线程A在进入外层函数抢占了锁之后,当线程A继续进入内层函数时,线程A依然可以再抢到这把锁。

不可重入锁与可重入锁相反,指的是一个线程只能抢占一次同一个锁。
悲观锁和乐观锁
synchronized和ReentrantLock都是悲观锁.
悲观锁,每次进入临界区操作数据的时候都认为别的线程会修改,有安全问题,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。
总体来说,悲观锁适用于写多读少的场景,遇到高并发写时性能高。
乐观锁,每次进入临界区操作数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复 “读-比较-写”的操作。
总体来说,乐观锁适用于读多写少的场景,遇到高并发写时性能低。
扩展:
mybatis-plus实现了乐观锁(版本控制),@Version标注一个字段int类型的version字段。
约定:任何线程修改了数据都必须将version+1.
例:
get oldVersio where id=1得到0
update xxx set ageage+1,version version +1 where id 1 and version oldVersion
如果update操作返回的影响行数=0,表示本次修改的过程中,其他的线程先行一步,将数据值进行了修改,当前线程就应该重试(重新获取oldVersion,重新执行update,直到执行成功为止)。
缺点:乐观锁的这种重试机制(自旋),可能会导致cpu的利用率标高(每次修改都失败了)
公平锁和非公平锁
//synchronized 是非公平锁,ReentrantLock既可以实现公平锁也可以非公平锁。
//ReentrantLock默认是非公平锁
static Lock lock = new ReentrantLock(true); //公平锁
static Lock lock = new ReentrantLock(false); //非公平锁
公平锁,多个线程按照申请锁的顺序来获取锁,先到先得。有一个等待锁的队列,申请锁的线程进入到队列中去队等待,队列中的第一个线程才能获得锁。
非公平锁,多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待,但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
在选择公平锁和非公平锁时,通常需要考虑性能和公正性之间的权衡。如果你的应用程序对锁的公平性要求较高,并且能够承受一些额外的性能开销,可以选择公平锁;否则,默认的非公平锁一般情况下更为高效。
非公平锁的吞吐量更高(效率更高),非公平锁下,可以有效的避免一些线程的阻塞(挂起)和唤醒(减少系统开销)
可中断锁和不可中断锁
Java中的synchronized关键字实现的就是一种不可中断锁的机制。
Java中的ReentrantLock类支持可中断锁,即线程在等待获取锁时可以被中断。
当一个线程通过调用lock锁的lock.locklnterruptibly()方法去获取锁时,其他的线程中,可以调用当前线程的thread.interrupt()将等待锁的线程进行中断
lock.locklnterruptibly();
lock.lockInterruptibly()是 JavaReentrantLock类中的一个方法,用于尝试获取锁,并且可以响应中断。它与lock()方法的主要区别在于,当调用lockInterruptibly()的线程被中断时,当前阻塞状态会被终止,从而抛出InterruptedException。
这是一个非常有用的特性,尤其是在需要更高的线程控制和更复杂的线程处理场景中,比如在实现某些复杂的并发算法时,或在需要协作停止长时间等待的线程时。
可中断锁:线程在等待获取锁的过程中,如果收到中断信号,会立即响应中断,并结束等待状态,这种机制允许线程在等待期间执行其他任务或进行其他操作。
不可中断锁:线程一旦开始等待获取锁,除非成功获取到锁,否则不会被任何中断信号所打断。线程会一直等待,直到成功获取到锁或者线程本身被终止。在等待期间,线程无法响应中断信号,也无法执行其他任务。
共享锁和独占锁
独占锁(Exclusive Lock)和共享锁(Shared Lock)。
synchronized 是一种独占锁。
ReentrantLock可以是独占锁也可是共享锁。
共享锁(ReentrantReadWriteLock)
特点:ReentrantReadWriteLock 提供了一种读写锁机制,允许多个线程同时读取,而不允许写线程在有读线程时执行。也就是说,当有一个线程在写时,所有其他线程(读或写)都将被阻塞。
使用场景:适用于读操作频繁而写操作相对少的情况。
独占锁也叫互斥锁。特点就是同一时刻,多个线程中只能有一个线程获取到锁,当一个线程获得一个独占锁后,其他线程将无法获取该锁,直到该线程释放锁。
共享锁,允许多个线程同时获取同一个锁。
扩展
进程内锁和分布式锁
Lock和synchronized都是进程内锁,只有同一个进程内的多个线程才可以使用Lock和synchronized保证正并发安全。
如果需要跨进程的多个线程保证并发安全,这里就需要分布式锁,通过可以使用redis来实现分布式锁。就是使用redis中setnx命令(如果当前key在redis中不存在,则set成功,否则set失败)。
自旋锁
加锁的操作并不会阻塞,而是通过重试的方式,重新获取锁

自旋锁优点,减少线程的阻塞起和唤醒的系统开销。
缺点,如果长时间没有获取到锁,则会导致cpu利用率标高。
ReentrantLock常用API
1、构造方法
ReentrantLock():创建一个非公平锁的实例。ReentrantLock(boolean fair):如果fair为true,则创建一个公平锁;如果为false,则创建一个非公平锁。
2. 锁的获取和释放
-
void lock():获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞,直到锁被释放。 -
void lockInterruptibly():获取锁,【如果锁已经被其他线程持有,则当前线程会被阻塞,直到锁被释放】。但是,支持中断;如果当前线程在等待锁的过程中被中断,将抛出InterruptedException。 -
boolean tryLock():尝试获取锁,如果锁当前未被其他线程持有,则获取成功并返回true;否则返回false。 -
boolean tryLock(long timeout, TimeUnit unit):尝试获取锁,在指定的时间内。如果在超时之前获取成功返回true,否则返回false。 -
void unlock():释放锁。必须在获取锁后调用此方法,否则会抛出IllegalMonitorStateException。
3. 查询状态
boolean isLocked():检查当前锁是否被任何线程持有。boolean isHeldByCurrentThread():检查当前线程是否持有此锁。int getHoldCount():返回当前线程持有此锁的次数。int getQueueLength():返回等待获取此锁的线程数。boolean hasQueuedThreads():判断是否有其他线程在等待获取此锁。
4. 其他
Condition newCondition():创建一个与此锁相关联的Condition实例。可以用来实现更复杂
Lock的读写锁
ReentrantReadWriteLock(读写锁)
java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。
用于提供读写锁的功能,从而允许控制对共享资源的访问。它的主要思想是,允许多个线程同时读取(共享访问),但在写入操作时必须独占访问,这样可以提高并发性能,尤其是在读操作较多、写操作较少的场景中。
读写锁的特点:
读锁(Shared Lock):多个线程可以同时获取读锁,只要没有任何线程持有写锁。
写锁(Exclusive Lock):只有一个线程可以持有写锁,同时在持有写锁的情况下,不能有任何其他线程获取读锁或写锁。
公平性
- 公平锁:按照请求的顺序分配锁。
- 非公平锁:可能会更快,但可能会导致某些线程长时间等待。
1、写写不可并发
2、读写不可并发
3、写读不可并发
4、读读可以并发
只要写线程出现,多个线程就开始使用写锁(独占锁),保证写的安全和读的一致性。
Lock writeLock().lock():返回写锁。
如果只有读线程,多个线程使用读锁(共享锁),保证并发读的效率。
Lock readLock().lock():返回读锁。
常用方法
- 构造方法
ReentrantReadWriteLock():创建一个非公平的读写锁。ReentrantReadWriteLock(boolean fair):如果fair为true,则创建公平锁;如果为false,则创建非公平锁。
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); //创建一个非公平的读写锁
- 获取锁
Lock readLock():返回读锁。Lock writeLock():返回写锁。
锁降级
锁降级:锁降级就是从写锁降级成为读锁。
在当前线程拥有写锁的情况下,之后再获取到读锁,随后释放写锁的过程就是锁降级。
锁降级使用场景:当多线程情况下,更新完数据后,立刻查询刚更新完的数据。

注意:ReentrantReadWriteLock 支持锁降级,不支持锁升级。
线程间通信
线程间通讯概述:线程间通信指的就是让多个线程进行协同工作,来完成特定的任务。
线程间通信,方案一: synchronized + wait() + notify()/notifyAll() 方法二:Lock + Condition
单生产单消费
需求分析
需求:两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
代码演示
// 线程
class ShareDataOne {
private Integer number = 0;
// 加1方法
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();//释放该对象的锁,并使自己进入等待状态
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知(由于方法执行结束,所以自动释放锁)
this.notifyAll();//所有等待的线程被唤醒,然后它们会竞争获取对象的锁
}
// 减1方法
public synchronized void decrement() throws InterruptedException {
// 1. 判断
if (number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
}
public class NotifyWaitDemo {
public static void main(String[] args) {
// 创建ShareDataOne对象
ShareDataOne shareDataOne = new ShareDataOne();
// 单线程对number变量进行+1操作10次
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "AAA").start();
// 单线程对number变量进行-1操作10次
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
shareDataOne.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "BBB").start();
}
}
sleep和wait的区别
区别:
1、sleep是Thread类中方法,wait方法是Object类中的方法
2、sleep方法不会释放同步锁,wait方法会释放同步锁
多生产多消费
产生虚假唤醒问题
当前线程获取到锁之后,并不能直接去操作共享数据,还需要判断条件是否成立(判断是否可以去操作共享数据),判断出当前线程不具备操作共享数据的资格,此时当前线程就要等待并释放锁。当前线程之后再次获取到锁时,应该再次判断是否具有操作共享数据的资格,如果依然不具备,继续等待并释放锁。
条件判断时:如果使用f,当等待状态的线程再次获取到锁之后,并没有进行重新的条件判断,而是直接向下执行。这就会产生虚假唤醒问题
解决办法:if改成while,一个等待状态的线程,当再次获取到锁时,就是自旋,重新进行条件判断。

解决办法==》if换成while
// 线程
class ShareDataOne {
private Integer number = 0;
// 加1方法
public synchronized void increment() throws InterruptedException {
// 1. 判断
while (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
// 减1方法
public synchronized void decrement() throws InterruptedException {
// 1. 判断
while (number != 1) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
this.notifyAll();
}
}
Lock+Condition实现通信
condition.await(); 线程等待
condition.signalAll(); 唤醒在此
Condition上等待的所有线程condition.signal(); 唤醒在此
Condition上等待的任意一个线程
// 线程
class ShareDataOne {
private Integer number = 0;
//创建Lock锁
private static final ReentrantLock reentrantLock = new ReentrantLock() ;
//创建Lock锁对应的condition,用于线程等待和唤醒
private static final Condition condition = reentrantLock.newCondition() ;
// 加1方法
public void increment() throws InterruptedException {
//获取锁
reentrantLock.lock();
// 1. 判断
while (number != 0) {
condition.await();//释放锁
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知。唤醒所有等待该Condition的线程(不会释放锁,所以下一步需要unlock)
condition.signalAll();
// 释放锁
reentrantLock.unlock();
}
// 减1方法
public void decrement() throws InterruptedException {
reentrantLock.lock(); // 获取锁
// 1. 判断
while (number != 1) {
condition.await();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + ": " + number);
// 3. 通知
condition.signalAll();
// 释放锁
reentrantLock.unlock();
}
}
注意:每个线程都可以创建独立的condition对象(多个线程使用同一个lock,但是使用各自独立的condition)。这样可以实现指定唤醒
实现多线程的有序执行(有序唤醒)
涉及到的知识点,为每个线程创建独立的condition对象(多个线程使用同一个lock,但是使用各自独立的condition),当某个线程使用condition1对象的await方法之后,将来当前线程能够被唤醒必须也得使用condition1对象的signalAll()方法。
private static final Lock lock = new ReentrantLock();
private static final Condition condition1 = lock.newCondition();
private static final Condition condition2 = lock.newCondition();
private static final Condition condition3 = lock.newCondition();
//等待
condition1.await();
//唤醒
condition1.await();
线程状态
java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态:
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
每种线程状态的含义:
| 线程状态 | 具体含义 |
|---|---|
| NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
| RUNNABLE | 调用线程对象的start方法,此时线程进入了RUNNABLE状态。(就绪状态) 线程一经启动并不是立即得到执行,线程的运行与否要听令于CPU的调度,可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的调度。 |
| BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态; 当该线程获取到锁时,该线程将变成Runnable状态。 |
| WAITING | 一个正在等待的线程的状态,也称之为等待状态。 造成线程等待的原因有两种,分别是调用wait()、join()方法。 处于等待状态的线程,正在等待其他线程去执行一个特定的操作。 例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll(); 一个因为join()而等待的线程正在等待另一个线程结束。 |
| TIMED_WAITING | 一个在限定时间内等待的线程的状态,也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:sleep(long)、wait(long)、join(long)。 |
| TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态。 |


浙公网安备 33010602011771号