并发与多线程
1.线程
1.1 线程创建
- 继承 Thread类
public class MyThread extends Thread{
/**
* Thread 类本质上是实现了Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方
* 法就是通过Thread 类的start()实例方法。start()方法是一个native 方法,它将启动一个新线
* 程,并执行run()方法。
*/
public void run(){
System.out.println("My Thread");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现Runnable接口
public class MyThread1 implements Runnable{
@Override
public void run() {
System.out.println("My Thread 1");
}
public static void main(String[] args) {
MyThread1 thread1 = new MyThread1();
Thread thread = new Thread(thread1);
thread.start();
}
- 有返回值线程
public class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1;
}
/**
* 有返回值的任务必须实现Callable 接口,类似的,无返回值的任务必须Runnable 接口。执行
* Callable 任务后,可以获取一个Future 的对象,在该对象上调用get 就可以获取到Callable 任务
* 返回的Object 了,再结合线程池接口ExecutorService 就可以实现传说中有返回结果的多线程了。
* @param args
*/
public static void main(String[] args) {
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Callable c = new MyThread2();
// 执行任务并获取Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future 对象上获取任务的返回值,并输出到控制台
try {
System.out.println("res:" + f.get().toString());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 基于线程池
/**
* 线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
* 毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
*/
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
1.1.1 start和run
- start方法启动线程,实现多线程运行,无需等待run执行完毕可以继续执行下面代码
- 调用Thread类的start方法启动线程,处于就绪状态
- run线程体,包含要执行的线程内容,线程进入运行状态,开始运行run函数的代码
1.2 线程状态
- 新建状态:new线程后
- 就绪:调用start
- 运行:获得cpu,执行run
- 阻塞:1.等待阻塞:wait方法,进入等待队列。 2.同步阻塞:获取对象同步锁时,其被占用,则放入锁池。 3.其他阻塞:现场sleep或join或发出IO请求,设置为阻塞,请求完转入可运行状态
- 死亡:1.正常死亡:run/call完成 2.异常结束:线程抛出未捕获的异常。 3.调用stop:容易死锁。
Object:wait notity notityAll
Thread: join sleep yield interrupt
1.2.1 wait:
线程会阻塞直到:1.其他线程notify/notifyAll,2.其他线程调用该线程interrupt方法,该线程抛异常 3.超时返回
当wait时 没有获得监视器锁,会抛异常,共享变量使用synchronized修饰才能获取监视器锁
- 虚假唤醒:一个线程挂起之后没有notify等操作变为运行状态。需要循环测试是否满足唤醒条件,不断调用wait
// 生产者
synchronized (queue) {
while (queue.size() == MAX_SIZE){
try {
// 挂起当前线程,并释放queue的锁
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
queue.add(element);
// 通知消费者线程
queue.notifyAll();
}
// 消费者
synchronized (queue) {
while (queue.size() == 0){
try {
// 挂起当前线程,并释放queue的锁
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
queue.take(element);
// 通知生产者线程
queue.notifyAll();
}
1.2.2 notify:
唤醒该共享变量上wait被挂起的线程,随机唤醒。被唤醒的线程必须获取共享变量的锁之后才能返回
1.2.3 notifyAll:
唤醒全部
1.2.4 join:
等待线程执行终止,调用join的线程会被阻塞,直到被调用线程执行完毕
1.2.5 sleep:
线程让出指定时间的执行权,但是锁不会让出,时间到了正常返回
1.2.6 yield:
让出CPU执行权,告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
1.2.7 interrupt:
线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理
- 线程A因为调用了wait、join sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
- isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返回false。
1.3 线程上下文切换
线程使用完时间片,cpu换线程
1.4 死锁
条件:互斥,请求并持有,不可剥夺,环路等待
1.5 守护线程与用户线程
也称服务线程,为用户线程提供公共服务,在Daemon线程中产生的新线程也是Daemon的,线程是JVM级别的,停止Web应用线程依旧活跃
- 优先级:较低
- 设置:在线程对象创建之前 用setDaemon将用户现场设置为守护线程
1.6 ThreadLocal
提供线程本地变量,访问ThreadLocal变量的线程都会有一个本地副本。
public class ThreadLocalTest {
static ThreadLocal<String> localV = new ThreadLocal<String>();
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
localV.set("one");
print("one");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
localV.set("two");
print("two");
}
});
thread.start();
thread2.start();
}
static void print(String s) {
System.out.println(s + " " + localV.get());
}
}
2.并发编程
2.1 线程安全问题
2.2 共享变量内存可见性
Java内存模型,将所有变量放到主内存,线程使用时,把主内存里面复制到自己的工作内存。
2.3 synchronized 关键字
同步锁,可以把任意一个非NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
内部锁,监视器锁。
- 作用范围:1. 作用于方法锁住是对象实例this
2. 作用于静态方法,锁住Class实例,会锁住调用该方法的线程
3. 作用于对象实例,锁住是所有以该对象为锁的代码块 - 核心组件:1.Wait Set 调用wait方法被阻塞的线程被放置在这里
2.Contention List:竞争队列,所有请求锁的线程被放这里
3.Entry List: 竞争队列中那些有资格成为候选资源的线程被移动到这里
4.OnDeck:任意时刻 最多只有一个线程在竞争锁资源,该线程被称为onDeck
5.Owner:当前已经获取到资源的线程
6.!Owner:当前释放的线程
2.4 volatile
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
synchronized 是独占锁,同时只能有一个线程调用get()方法,其他调用线程会被阻塞,同时会存在线程上下文切换和线程重新调度的开销,这也是使用锁方式不好的地方。
volatile 是非阻塞算法,不会造成线程上下文切换的开销。但并非在所有情况下使用它们都是等价的,volatile虽然提供了可见性保证,但并不保证操作的原子性。
当写入变量不依赖当前值时使用:读改写就不能使用
2.5 原子性操作
- synchronized 来实现线程安全性,即内存可见性和原子性
- 非阻塞CAS算法实现的原子性操作AtomicLong:使用锁导致线程上下文切换和调度开销。
CAS:Compare and Swap,通过硬件来保证比较-更新的原子性,JDK里面的Unsafe类提供方法
3.锁
3.1 乐观锁
拿数据不上锁,更新时比较版本号,加锁操作,更新失败后指定重试次数,重新获取数据
update xxx set xxx where version = #{version}
CAS操作实现,比价当前值和传入值是否一样,一样更新
3.2 悲观锁
在数据被处理前先加锁,处理中处于锁定状态。
select * from table where xxx for update
AQS框架下,先尝试CAS乐观锁获取锁,拿不到转化为悲观锁
3.3 非公平锁
JVM按随机,就近原则分配锁的机制成为不公平锁
ReentrantLock pairLock = new ReentrantLock(false) //默认
3.4 公平锁
分配锁的机制是公平的,通常先对锁提出请求的线程先被分配到锁,会带来性能开销
ReentrantLock pairLock = new ReentrantLock(true)
3.5 独占锁
只有一个线程能得到锁,悲观锁
ReentrantLock
3.6 共享锁
同时多个线程持有,乐观锁
ReadWriteLock:读写锁允许多个线程同时读取数据,但写入时独占锁,适合读多写少的场景。
3.7 可重入锁
线程再次获取已经获取的锁不被阻塞的锁
3.8 自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
必须设定最大等待时间,超过最大时间停止线程并阻塞
4.JUC
4.1 ThreadLocalRandom
随机数生成器,Random在多线程时,多个线程竞争同一个原子变量的更新操作,CAS操作,导致大量线程自旋重试。
原理:每个线程复制一份变量
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = 0; i < 5; i++) {
System.out.println(random.nextInt(5));
}
4.2 原子变量操作类
4.2.1 Atomic类
AtomicLong原子性递增递减,内部使用Unsafe类实现
在高并发下,该类存在性能问题,使用LongAdder类替换
public class CacheWithAtomic {
private final AtomicReference<String> cache = new AtomicReference<>();
public void write(String value) {
cache.set(value);
}
public String read() {
return cache.get();
}
}
4.2.2 LongAdder
原理:竞争一个变量的更新分解为多个变量,内部维护多个Cell变量
4.3 并发List
4.3.1 CopyOnWriteArrayList
线程安全的ArrayList,对其修改操作在底层的一个复制的数组上进行,写时复制策略
内部ReentrantLock保证同时只有一个线程进行修改
4.4 锁
4.4.1 LockSupport
工具类,挂起和唤醒线程
与每个使用它的线程关联一个许可证,默认没有许可证
- park
调用park线程拿到许可证,会马上返回,否则阻塞挂起。其他线程调用unpark,并将当前线程作为参数,被阻塞的当前线程会返回。 - unpark
使线程获取许可证
4.4.2 AQS抽象同步队列
AbstartctQueuedSynchronizer,实现锁的底层。
4.4.3 ReentrantLock
ReentantLock 继承接口Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
4.4.3 ReentrantReadWriteLock
采用读写分离策略,允许多个线程同时获取,写少读多的情况
4.4.4 StampedLock
提供三种模式的读写控制,写锁writeLock,悲观锁readLock,乐观锁tryOptimisticRead
4.5 并发队列
4.5.1 ConcurrentLinkedQueue
非阻塞队列,单向链表加CAS实现的入队出队
4.5.2 LinkedBlockingQueue
有界链表,独占锁实现的阻塞队列
4.5.3 ArrayBlockingQueue
有界数组,独占锁实现的阻塞队列
4.5.4 PriorityBlockingQueue
带优先级的无界阻塞队列,出队返回优先级最高/低的元素。使用平衡二叉树实现,直接遍历不保证有序
4.5.5 DelayQueue
无界阻塞延迟队列,每个元素都有过期时间,获取元素时,只有过期元素出队列
4.6 线程同步器
4.6.1 CountDownLatch
主线程等待子线程完成后汇总场景,join不够灵活
CountDownLatch可以在子线程运行任何时候让await返回,不一定等到结束。使用线程池时,直接添加到线程池的没有办法使用join
4.6.2 CyclicBarrier回环屏障
让一组线程全部到达一个状态再全部同时执行,
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2022-09-05 BFS基础
2022-09-05 LC733
2022-09-05 LC695
2022-09-05 pair
2022-09-05 Spring事务