多线程
一,多线程概述
进程:进程是程序的基本执行实体。一个软件运行之后就是一个进程。
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中实际运作的单位。
简单理解:应用软件中互相独立,可以同时运行的功能就是线程,多个线程同时运行就是多线程。
多线程的应用场景:只要是想让多个事情同时运行就需要用到多线程。
- 拷贝迁移大文件
- 加载大量的资源文件
- 所有的聊天软件
- 所有的后台服务
并发与并行
-
并发:在同一时刻,有多个指令在单个cpu上交替执行。
-
并行:在同一时刻,有多个指令在多个cpu上同时执行。
特性 并发 并行 执行方式 单核交替执行(时间片轮转) 多核同时执行 资源需求 无需额外硬件 依赖多核/多CPU 典型场景 Web服务器处理请求 大数据计算(MapReduce)
二,多线程的实现方式
2.1 继承Thread类的方式进行实现
继承Thread类可以理解为创建一个线程对象,直接执行自定义内容。
步骤:
-
声明要执行多线程的类为
Thread类的子类 -
在这个类中重写
run方法 -
创建子类对象,使用
对象名.start()启动线程线程执行一定调用start方法而不是调用run方法,一个线程对象只能start一次。
特点:每创建一个对象表示一个线程
优点:编程比较简单,可以直接使用Thread类中的方法
缺点:可扩展性差,不能继承其他的类
// 方式1:继承Thread(不推荐,缺乏扩展性)
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
2.2 实现Runnable接口的方式进行实现
实现Runnable接口,可以理解为创建一个任务,然后将任务较给线程Thread去执行。
步骤:
- 实现Runnable接口
- 重写里面的run方法
- 创建实现类对象
- 将实现类对象作为参数传递给Thread类
public class Task2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("----------"+i);
}
}
}
public class Main {
public static void main(String[] args) {
Task2 task2 = new Task2();//创建实现类对象
Thread thread = new Thread(task2);//将实现类对象作为参数传递给Thread类
thread.start();
}
}
优点: 扩展性强,实现该接口的同时可以继承其他的类
缺点:编程相对复杂,不能直接使用
2.3 Lambda表达式创建线程
public class Main {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}).start();
}
}
2.4 实现Callable接口,FutureTask接口创建线程
步骤:
//1.创建一个类实现Callable接口,泛型类型就是线程返回值类型
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
//2.实现call方法
@Override
public String call() throws Exception {
int sum=0;
//求1-n的合,返回
for (int i = 0; i < n; i++) {
sum+=i;
}
return "线程sum="+sum;
}
}
public class Main {
public static void main(String[] args) {
//3.创建一个Callable对象
Callable<String> myCallable = new MyCallable(10);
//4.将Callable对象作为参数传递给FutureTask对象
//FutureTask对象的作用:
//1.是一个任务对象,实现了Runnable对象
//2.可以在线程执行完毕后调用get方法获取线程执行完毕后的结果
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//5.把FutureTask对象作为参数传递给Thread对象
Thread thread = new Thread(futureTask);
thread.start();
//6.获取线程执行完毕后执行的结果
//如果执行到这里,假设上面线程还未执行完毕
//这里会进入等待状态,等待上述线程执行完毕后获取结果
try{
String res = futureTask.get();
System.out.println(res);
}catch (Exception e){
e.printStackTrace();
}
}
}
特点:可以获取多线程运行的结果。作为参数让线程去执行的。
优点:扩展性强,实现该接口的同时可以继承其他的类
缺点:编程相对复杂,不能直接使用Thread类中的方法
2.5 实现方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
继承Thread |
简单直观 | 无法继承其他类 | 简单任务 |
实现Runnable |
灵活扩展 | 无返回值 | 通用场景 |
Callable+Future |
支持返回值与异常 | 编码复杂 | 需要结果反馈的任务 |
| Lambda表达式 | 代码简洁 | 仅适用于简单逻辑 | 快速实现单方法任务 |
三,Thread常见的成员方法
| 方法名 | 说明 |
|---|---|
| String getName() | 返回此线程的名称 |
| viud setName(String name) | 设置线程的名字(构造方法也可也设置名字) |
| static Thread currentThread() | 获取当前线程的对象 |
| static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
| void join() | 让当前调用这个方法的线程先执行完毕。 |
细节:
-
设置线程名字的细节:
- 如果没用给线程设置名字,线程也有默认的名字的,格式是
Thread-XX是序号,规则从0开始 - 如果要给线程设置名字,可以用setName方法设置,也可在子类中继承父类构造方法从而在创建子类对象的时候设置名字。
- 如果没用给线程设置名字,线程也有默认的名字的,格式是
-
获取当前线程对象的细节:
当JVM虚拟机启动后,会自动的启动多条线程,其中有一条线程就叫做main线程。
他的作用就是去调用main方法,并执行里面的代码。
在以前,自己写的所有代码,都是运行在main线程之中的。
-
让线程休眠的细节:
- 哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的事件。
- 方法的参数:表示睡眠事件,单位:毫秒
- 当时间到了之后,线程就会自动的醒来,继续执行下面的代码。
四,线程类型
4.1线程的优先级
线程的调度:在计算机中线程的调度有两种
- 抢占式调度:多个线程在抢夺cpu的执行权,cpu在哪条线程,在线程执行多少时间是不确定的(随机性)(java采取的)
- 非抢占式调度:所有的线程轮流的执行
这个随机性跟线程的优先级有关,线程优先级越高,该线程抢到cpu的概率就越大。
| 方法 | 说明 |
|---|---|
| setPriority(int newPriority) | 设置线程的优先级 |
| final int getPriority() | 获取线程的优先级 |
在java中线程优先级分为十个档次,最小是1最大是10,默认为5
4.2 守护线程
守护线程特点:当其他的非守护线程执行完毕后,守护线程会陆续结束。
设:非守护线程被调用且执行10次,守护线程被调用且执行100次。
那么当非守护线程执行完毕后,假设此时守护线程执行了30次,那么此时守护线程会再次执行几次就结束,而不是执行100次。
| 方法 | 说明 |
|---|---|
| final void setDaemon(boolean on) | 设置线程为守护线程 |
应用场景:
如图:此时线程2可设置为守护线程,当线程1结束了线程2也没必要运行下去了。
4.3 礼让线程
| 方法名 | 说明 |
|---|---|
| public static void yield() | 出让线程/礼让线程 |
方法作用:让调用这个方法的线程变为礼让线程。表示出让当前cpu的执行权
设:线程A抢到了cpu执行权,那么在线程B抢到cpu执行权之前,cpu都是在执行线程A的。
但使用了这个方法后,cpu执行完毕一次线程后,线程A和线程B都需要重新抢夺cpu的执行权。
这样可以让多线程的运行尽可能的均匀。
4.4 插入线程
| 方法名 | 说明 |
|---|---|
| public final void join() | 插入线程/插队线程 |
方法作用:让调用这个方法的线程变为插入线程。
当线程A变为插入线程后,那么只有线程A全部执行完毕,其他线程才可抢夺cpu执行权
五,线程的生命周期

六,线程安全问题
当多个线程操作同一个数据的时候,会出现:相同数据出现多次,出现了超出范围的数据。等线程安全问题。
原因:线程在执行代码的时候随时有可能被其他线程抢走。即线程执行时,具有随机性。
解决办法:将操作数据的代码块锁住,让所有的线程在操作这段代码块的时候能轮流执行。
线程同步的常见方案
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
6.1 同步代码块
同步代码块:把操作共享数据的代码锁起来,以包装线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动上锁,其他线程才可以进来执行。
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
格式:
synchronized (锁){
操作共享数据的代码
}
特点:
-
锁默认打开,有一个线程进去了,锁自动关闭。
-
锁里面的代码全部执行完毕,线程出来,锁自动打开。
-
锁对象可以是任意的,比如说Object类型的都可以,但是必须是唯一的,即多个对象都只有唯一这一个锁对象,可以使用static修饰或者使用class类。
多线程要操作的那个数据一般也是唯一的。
细节:
- synchronized要写在循环里面
- synchronized锁对象必须是唯一的,如果每个线程看的锁是不一样的,那么这个同步代码块就没用意义了。
- 实例方法的锁对象一般我们采用this作为锁对象
- 静态方法里面上锁一般采用类名.class作为锁对象
6.2 同步方法
同步方法:就是把synchronized关键字加到方法上面
格式:
修饰符 synchronized 返回值类型 方法名(参数){...}
特点:
- 同步方法是锁住方法里面的所有代码
- 锁对象不能自己指定,是java已经规定好的
- 如果方法是非静态的,那么锁对象就是this
- 如果方法是静态的,那么锁对象就是当前字节码文件对象
同步方法技巧:先写同步代码块,然后把同步代码块抽取为同步方法即可。
6.3 lock锁
6.3.1 ReentrantLock 锁
jdk5的时候java提供了一个新的锁对象Lock
在Lock中提供了void lock()获得锁 void unlock()释放锁,可以手动的上锁和解锁。
注:Lock是接口,采用它的实现类ReentrantLock创建锁对象
即可以用lock锁代替同步代码块。
package domain;
import lombok.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class Account {
private String cardId;
private int acount;
private final Lock lock=new ReentrantLock();//创建锁对象
public Account(String cardId, int acount) {
this.cardId = cardId;
this.acount = acount;
}
public void drawMoney(int money) {
String name = Thread.currentThread().getName();
//上锁和解锁必须放到try-finally里面保证即使程序出现异常也能解锁
try {
lock.lock();//上锁
if(this.acount >= money){
System.out.println(name+"取钱成功,余额为:"+(this.acount-money));
this.acount -= money;
System.out.println(name+"取钱成功,余额为:"+(this.acount));
}else{
System.out.println(name+"取钱失败,余额不足");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
6.3.2 ReadWriteLock 读写锁
ReadWriteLock 接口提供了两种锁:读锁和写锁。多个线程可以同时持有读锁,但只能有一个线程持有写锁。当没有线程持有写锁时,多个线程可以同时获取读锁,这样可以提高并发性能。读写锁适用于读多写少的场景。
读写锁就是:
- 读数据的时候上读锁
- 写数据的时候上写锁
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
public int readData() {
lock.readLock().lock();
try {
Thread.sleep(100); // 模拟耗时读操作
return data;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1;
} finally {
lock.readLock().unlock();
}
}
public void writeData(int newData) {
lock.writeLock().lock();
try {
Thread.sleep(200); // 模拟耗时写操作
data = newData;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.writeLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockExample example = new ReadWriteLockExample();
Thread[] readers = new Thread[5];
// 启动读线程
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
System.out.println("Read Thread " + Thread.currentThread().getId() + " reads: " + example.readData());
});
readers[i].start();
}
// 启动写线程
Thread writer = new Thread(() -> {
example.writeData(100);
System.out.println("Write Thread " + Thread.currentThread().getId() + " writes: 100");
});
writer.start();
// 等待所有线程完成
for (Thread reader : readers) {
reader.join();
}
writer.join();
// 写操作后再启动新的读线程
new Thread(() -> {
System.out.println("New Read Thread " + Thread.currentThread().getId() + " reads: " + example.readData());
}).start();
}
}
七,线程通信模型
线程通信:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据,
- 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!
案例
Object类的等待合唤醒方法

上述方法应该使用当前锁对象来调用。
代码
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumer {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity;
public ProducerConsumer(int capacity) {
this.capacity = capacity;
}
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (this) {
while (buffer.size() == capacity) {
wait();
}
System.out.println("Produced " + value);
buffer.add(value++);
notify();
Thread.sleep(1000); // 模拟生产时间
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
int value = buffer.poll();
System.out.println("Consumed " + value);
notify();
Thread.sleep(1000); // 模拟消费时间
}
}
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer(10);
Thread producerThread = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
八,多线程的6种状态

注:运行状态在java中是不存在的,只是为了方便理解加上的。
因为当线程抢到cpu执行权限时,jvm虚拟机会把这个线程交给操作系统,就不管这个线程了。
线程的6种状态总结:

状态转换图:

九,线程池
9.1 线程池的概念
以前使用多线程的弊端是会造成系统资源的浪费,使用线程池,当用完线程后将线程放入线程池中,再次使用的时候就直接从线程池中调用线程,避免的过多的浪费系统资源。
线程池主要核心原理
- 创建一个池子,里面是空的
- 提交任务的时候,池子会创建新的线程对象,任务执行完毕,线程归还池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
- 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。

9.2 自定义线程池的创建和使用
9.2.1 自定义线程池的创建
使用ExecutorService的实现类ThreadPoolExecutor来创建一个线程池对象。
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
2,//corePoolSize:指定线程池的核心线程数量(可复用的线程的数量)
5,//maxmumPoolSize:执行线程池的最大线程数量
1,//keepAliveTime:指定临时线程的存活时间
TimeUnit.SECONDS,//unit:keepAliveTime的单位(秒,分,时,填)
new LinkedBlockingQueue<>(3),//workQueue:任务队列,用于存放等待执行的任务
Executors.defaultThreadFactory(),//threadFactory:线程工厂,用于创建线程
new ThreadPoolExecutor.AbortPolicy()//handler:拒绝策略,当任务太多来不及处理时,此时处理的方式
);
常用任务队列对象:
LinkedBlockingQueue:底层基于链表实现,不限制大小。ArrayBlockingQueue:基于数组实现,在构造方法中限制大小。
声明线程工厂:Executors.defaultThreadFactory()声明一个默认线程工厂。
任务拒绝策略:
new ThreadPoolExecutor.AbortPolicy():新任务来了就抛出一个异常。(默认)new ThreadPoolExecutor.DiscardPolicy():丢弃任务,但不抛出异常,(不推荐)new ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列中- new ThreadPoolExecutor.
CallerRunsPolicy:由主线程负责调用run方法从而绕过线程池直接执行。
线程池注意事项:
-
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
-
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
-
线程池不会死亡,需要手动调用方法关闭线程池分为:
shutdown方法等待任务执行完毕关闭线程池。shutdownNow方法,直接关闭线程池并且返回未执行的任务,并且抛出异常
一般不会手动去关闭线程池,了解这俩方法即可。
9.2.2 线程池处理Runnable任务
//任务对象
package domain;
public class MyRunnable implements Runnable{
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+"线程正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//测试类
import domain.*;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
3,//corePoolSize:指定线程池的核心线程数量(可复用的线程的数量)
5,//maxmumPoolSize:执行线程池的最大线程数量
1,//keepAliveTime:指定临时线程的存活时间
TimeUnit.MINUTES,//unit:keepAliveTime的单位(秒,分,时,填)
new ArrayBlockingQueue<>(3),//workQueue:任务队列,用于存放等待执行的任务
Executors.defaultThreadFactory(),//threadFactory:线程工厂,用于创建线程
new ThreadPoolExecutor.AbortPolicy()//handler:拒绝策略,当任务太多来不及处理时,此时处理的方式
);
Runnable target = new MyRunnable();
threadPoolExecutor.exeecute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的。我们只需在这里将任务交给线程池即可
threadPoolExecutor.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的。我们只需在这里将任务交给线程池即可
threadPoolExecutor.execute(target);//线程池会自动创建一个新线程,自动处理这个任务,自动执行的。我们只需在这里将任务交给线程池即可
threadPoolExecutor.execute(target);//复用前面的核心线程
threadPoolExecutor.execute(target);//复用前面的核心线程
threadPoolExecutor.execute(target);//复用前面的核心线程
threadPoolExecutor.shutdown();//等待线程池中所有任务执行完毕,再关闭线程池
List<Runnable> runnables = threadPoolExecutor.shutdownNow();//直接关闭线程池并且返回未执行的任务,并且抛出异常
}
}
9.2.3 线程池处理Callable任务
package domain;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum=0;
//求1-n的合,返回
for (int i = 0; i < n; i++) {
sum+=i;
}
return Thread.currentThread().getName()+"求出了1-"+n+"的和是:"+sum;
}
}
import domain.*;
import java.util.List;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
3,//corePoolSize:指定线程池的核心线程数量(可复用的线程的数量)
5,//maxmumPoolSize:执行线程池的最大线程数量
1,//keepAliveTime:指定临时线程的存活时间
TimeUnit.MINUTES,//unit:keepAliveTime的单位(秒,分,时,填)
new ArrayBlockingQueue<>(3),//workQueue:任务队列,用于存放等待执行的任务
Executors.defaultThreadFactory(),//threadFactory:线程工厂,用于创建线程
new ThreadPoolExecutor.CallerRunsPolicy()//handler:拒绝策略,当任务太多来不及处理时,此时处理的方式
);
Future<String> f1 = threadPoolExecutor.submit( new MyCallable(100));//调用submit方法执行Callable任务并且返回未来任务对象
Future<String> f2 = threadPoolExecutor.submit( new MyCallable(200));//调用submit方法执行Callable任务并且返回未来任务对象
System.out.println(f1.get());
System.out.println(f2.get());
}
}
9.3 Executors工具类实现线程池
Executors:是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
常见方法:

注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
Executors使用可能存在的陷阱:大型并发系统环境中使用Executors如果不注意可能会出现系统风险

9.4 核心线程数量配置
计算密集型的任务:核心线程数量=CPU的核数+1
I0密集型的任务:核心线程数量=CPU核数*2
十,多线程中的三种锁
10.1 悲观锁
悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全但性能较差。
悲观锁实现逻辑举例:
//任务对象
package domain;
public class MyRunnable implements Runnable{
private int count;
@Override
public void run() {
//100次
for (int i = 0; i < 100; i++) {
synchronized (this){
System.out.println(Thread.currentThread().getName()+"count===>"+(++count));
}
}
}
}
//测试类
import domain.*;
import java.util.List;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
//需求:1个遍历100个线程,每个线程对其加100次
Runnable target = new MyRunnable();
for (int i = 1; i <= 100; i++) {
new Thread(target).start();
}
}
}
10.2 乐观锁
乐观锁:一开始不上锁,认为是没有问题的、人家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。
比较合交换算法(CAS):首先,它会比较内存中某个位置的值与预期值是否相等,如果相等,就会将该位置的值更新为新的值;如果不相等,则操作失败,不进行更新。这个比较和更新的过程是一个原子操作,不会被其他线程中断。
乐观锁实现逻辑举例
package domain;
import java.util.concurrent.atomic.AtomicInteger;
public class MyRunnable implements Runnable{
//整数修改的乐观锁:原子类实现
//还有其他的数组,包装类,都存在原子对象
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
//100次
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"count===>"+(count.incrementAndGet()));
}
}
}
//测试类
import domain.*;
import java.util.List;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
//需求:1个遍历100个线程,每个线程对其加100次
Runnable target = new MyRunnable();
for (int i = 1; i <= 100; i++) {
new Thread(target).start();
}
}
}
常见的原子类:
- AtomicBoolean: 提供原子更新 boolean 类型的操作。
- AtomicInteger: 提供原子更新整型的操作。
- AtomicLong: 提供原子更新长整型的操作。
- AtomicReference: 提供原子更新引用类型的操作。
- AtomicIntegerArray: 提供原子更新整型数组的操作。
- AtomicLongArray: 提供原子更新长整型数组的操作。
- AtomicReferenceArray: 提供原子更新引用类型数组的操作。
原子类常用方法
注意:以原子的方式的意思就是采用CAS算法在多线程中使用乐观锁对原子值进行操作
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) {
// 创建一个初始值为0的原子整型变量
AtomicInteger atomicInteger = new AtomicInteger(0);
// 获取当前值
int value = atomicInteger.get();
System.out.println("Current value: " + value);
// 设置新值
atomicInteger.set(10);
System.out.println("New value: " + atomicInteger.get());
// 获取当前值,并设置新值
int oldValue = atomicInteger.getAndSet(20);
System.out.println("Old value: " + oldValue);
System.out.println("New value after getAndSet: " + atomicInteger.get());
// 比较并设置值
// 如果当前值等于预期值(expect),则以原子方式将该值设置为新值(update),返回是否设置成功。
boolean success = atomicInteger.compareAndSet(20, 30);
System.out.println("Compare and set success? " + success);
System.out.println("Value after compareAndSet: " + atomicInteger.get());
// 以原子方式将当前值加 1,并返回增加后的值。
int newValue = atomicInteger.incrementAndGet();
System.out.println("New value after incrementAndGet: " + newValue);
// 以原子方式将当前值减 1,并返回减少后的值。
newValue = atomicInteger.decrementAndGet();
System.out.println("New value after decrementAndGet: " + newValue);
// 以原子方式将当前值返回,并将该值加 1。
oldValue = atomicInteger.getAndIncrement();
System.out.println("Old value after getAndIncrement: " + oldValue);
System.out.println("Value after getAndIncrement: " + atomicInteger.get());
//以原子方式将当前值返回,并将该值减 1。
oldValue = atomicInteger.getAndDecrement();
System.out.println("Old value after getAndDecrement: " + oldValue);
System.out.println("Value after getAndDecrement: " + atomicInteger.get());
}
}
10.3 死锁
死锁是多线程编程中常见的一种问题,指的是两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行的状态。简单来说,就是多个线程在互相等待对方释放资源,但是它们本身又不释放已经持有的资源,导致程序无法继续执行下去。
死锁通常涉及多个锁对象,并且在获取锁的顺序上存在问题。一个常见的例子是两个线程分别持有不同的锁,并试图获取对方持有的锁。当它们同时获取了自己所需的锁,却无法释放对方持有的锁时,就会陷入死锁状态。
例如:线程 A 持有锁 1,想要获取锁 2;线程 B 持有锁 2,想要获取锁 1。如果线程 A 和线程 B 同时尝试获取对方持有的锁,那么它们就可能发生死锁。
死锁的出现可能会导致程序永远无法继续执行下去,直到手动干预或者操作系统强制结束这些线程。因此,死锁是需要避免的,对多线程编程的设计和调试都有一定挑战。
预防死锁的常用方法包括:
- 避免锁的嵌套:尽量减少锁的使用层级,降低死锁的概率。
- 按顺序获取锁:规定所有线程获取锁的顺序,确保所有线程都以相同的顺序获取锁,从而避免死锁。
- 使用定时锁:如果获取锁失败,不要无限期等待,而是使用定时锁,并设定超时时间,超时后放弃获取锁并执行相应的逻辑。
- 使用并发工具类:如
java.util.concurrent包中提供的并发工具类,它们已经实现了一些避免死锁的机制,ReentrantLock、Semaphore、CountDownLatch等。
尽管有这些方法,但是死锁仍然可能出现。因此,在设计多线程程序时,需要仔细考虑锁的使用方式,以尽量避免死锁的发生。
代码示例:
public class DeadLockDemo {
public static void main(String[] args) {
//死锁示例
DeadLocks threadA = new DeadLocks();
DeadLocks threadB = new DeadLocks();
threadA.setName("线程A");
threadB.setName("线程B");
threadA.start();
threadB.start();
}
}
class DeadLocks extends Thread {
//两把锁
static Object lockA = new Object();
static Object lockB = new Object();
@Override
public void run() {
//嵌套锁
if ("线程A".equals(getName())) {
synchronized (lockA) {
System.out.println("线程A得到锁A");
synchronized (lockB) {
System.out.println("线程A得到锁B");
}
}
} else if ("线程B".equals(getName())) {
synchronized (lockB) {
System.out.println("线程B得到锁B");
synchronized (lockA) {
System.out.println("线程B得到锁A");
}
}
}
}
}

浙公网安备 33010602011771号