多线程相关知识
一,进程和线程
进程和线程的概念
并行和并发的概念
线程基本应用
进程:当一个程序被运行,就相当于开启了一个进程。
线程:一个进程内可以分到1到多个线程。Java中线程是最小的调度单位。
并发:同一时间应对多件事情的能力
并行:同一时间动手做多件事情的能力
Java线程
创建和运行线程
查看线程
线程API
线程状态
方法1,直接使用thread
Thread t1 = new Thread(){ @Override public void run() { log.info("doing..."); } }; t1.start();
方法2,使用Runnable配合thread
thread代表线程
Runnable代表可运行的任务
Runnable running = new Runnable() { @Override public void run() { log.info("running"); } }; Thread t2 = new Thread(running); t2.start();
java8后可用lamda表达式
new Thread(
()->{log.info("running");
}).start();
方法3,FutureTask配合Thread
FutureTask<Integer> futureTask = new FutureTask<Integer>(() -> {
log.info("running...");
return 10;
});
new Thread(futureTask, "t1").start();
int result = futureTask.get();
log.info("the result is {}", result);
线程上下文切换
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
线程的CPU时间片用完
垃圾回收
有更高优先级的线程要执行
线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法。
线程常见方法
start(),启动一个线程
run(),线程启动后调用该方法
join(), 等待某个线程结束
getId(),获取编号
getName(),获取线程名
setName(),设置线程名
getState(),获取线程状态
isIntersrupted(),判断是否被打断
isAlive(),线程是否存活
interrupt(),打断线程
currentThread(),获取当前执行的线程
sleep(long n),让当前执行的线程休眠n毫秒
yield(),提示线程调度器让出当前线程对CPU的使用
详细介绍:
start(), 启动一个线程需要调用start()方法,调用run方法只是在主线程启动run方法中的代码;
sleep(),调用该方法,会让线程从running进入time waiting状态,其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException异常。
睡眠后的线程未必会立即执行
yield(),调用该方法会让当前线程从running进入runnable就绪状态,然后执行其他线程
一,进程和线程
进程和线程的概念
并行和并发的概念
线程基本应用
进程:当一个程序被运行,就相当于开启了一个进程。
线程:一个进程内可以分到1到多个线程。Java中线程是最小的调度单位。
并发:同一时间应对多件事情的能力
并行:同一时间动手做多件事情的能力
Java线程
创建和运行线程
查看线程
线程API
线程状态
方法1,直接使用thread
Thread t1 = new Thread(){ @Override public void run() { log.info("doing..."); } }; t1.start();
方法2,使用Runnable配合thread
thread代表线程
Runnable代表可运行的任务
Runnable running = new Runnable() { @Override public void run() { log.info("running"); } }; Thread t2 = new Thread(running); t2.start();
java8后可用lamda表达式
new Thread(
()->{log.info("running");
}).start();
方法3,FutureTask配合Thread
FutureTask<Integer> futureTask = new FutureTask<Integer>(() -> {
log.info("running...");
return 10;
});
new Thread(futureTask, "t1").start();
int result = futureTask.get();
log.info("the result is {}", result);
线程上下文切换
因为以下一些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
线程的CPU时间片用完
垃圾回收
有更高优先级的线程要执行
线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法。
线程常见方法
start(),启动一个线程
run(),线程启动后调用该方法
join(), 等待某个线程结束
getId(),获取编号
getName(),获取线程名
setName(),设置线程名
getState(),获取线程状态
isIntersrupted(),判断是否被打断
isAlive(),线程是否存活
interrupt(),打断线程
currentThread(),获取当前执行的线程
sleep(long n),让当前执行的线程休眠n毫秒
yield(),提示线程调度器让出当前线程对CPU的使用
详细介绍:
start(), 启动一个线程需要调用start()方法,调用run方法只是在主线程启动run方法中的代码;
sleep(),调用该方法,会让线程从running进入time waiting状态,其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException异常。
睡眠后的线程未必会立即执行
yield(),调用该方法会让当前线程从running进入runnable就绪状态,然后执行其他线程
setpriority()线程优先级,从1到10,10优先级最高;
线程优先级会提示调度器优先调用该线程,但他仅仅是一个提示,调度器可以忽略。
sleep防止CPU占用100%;
while (true) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } }
避免空转浪费CPU。
join()方法
线程调用,等待当前调用线程执行结束。
join(long n)
最多等待n豪秒
interrupt方法详解
1.打断sleep,wait,join的线程,会清空打断标记。
2.打断正常的线程,打断标记是true
两阶段终止模式:在一个线程怎样停止另一个线程
public class Test3 { public static void main(String[] args) { TwoPhaseTermination abc = new TwoPhaseTermination(); abc.start(); try { Thread.sleep(3500); } catch (InterruptedException e) { e.printStackTrace(); } abc.stop(); } } @Slf4j class TwoPhaseTermination { private Thread monitor; public void start() { monitor = new Thread(() -> { while (true) { Thread current = Thread.currentThread(); if (current.isInterrupted()) { log.info("料理后事"); break; } try { Thread.sleep(1000);//情况1 log.info("执行监控记录");//情况2 } catch (InterruptedException e) { e.printStackTrace(); current.interrupt(); } } }); monitor.start(); } public void stop() { monitor.interrupt(); } }
不推荐方法
stop()停止线程,suspend()暂停线程,resume()恢复线程
主线程和守护线程
垃圾回收器就是守护线程,设置为守护线程monitor.setDaemon(true);
线程状态
初始状态,可运行状态,运行状态,阻塞状态,终止状态。
new - runnable - blocked - waiting - timed_waiting - terminated
共享模型之管程
多个线程访问共享资源
多个线程对共享资源读写操作时发生指令交错,就会出现问题。
一段代码内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
为了避免临界区的竞争条件发生,有多种手段可以达到目的。
阻塞式的解决方案
synchronized,Lock
非阻塞式的解决方案
原子变量
synchronized(对象){
}
synchronized加在方法上:
1.加在成员方法上
2.加在静态方法上
成员变量和静态变量是否线程安全
如果他们没有共享,那么线程安全
如果他们被共享了根据他们是否能被改变又分为两种情况
只有读操作,线程安全
如果有读写操作,则这块代码是临界区,需要考虑线程安全
局部变量是否线程安全
局部变量是线程安全的
常见线程安全类
String,Integer,StringBuffer,Random,Vector,HashTable,java.util.concurrent包下的类。
他们的每个方法都是原子的。
经典卖票问题
Monitor监视器
轻量级锁使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。语法仍然是synchronized
锁膨胀:如果在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况就是有其他线程为此对象加了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
自旋优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
在Java6之后自旋锁是自适应的,自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
Java7之后不能控制是否开启自旋功能。
偏向锁:轻量级锁在没有竞争时(就自己这个线程),每次冲入仍然需要执行cas操作。
Java6中引入了偏向锁来进一步优化:只有第一次使用cas将线程ID设置到对象的Mark word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有。
一个对象创建时
如果开启了偏向锁(默认开启),那么对象创建后Mark word最后三位是101,这时它的thread,epoch,age都为0
偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加vm参数来禁用延迟
如果没有开启偏向锁,那么对象创建后Mark word的最后三位是001,这时它的hashcode,age都为0,第一次用到hashcode时才会赋值。
偏向锁调用hashcode方法时会撤销偏向锁,
当另一线程竞争锁时,偏向锁升级为轻量级锁
批量重偏向
锁消除
wait,notify
wait()方法
notify()方法
notifyAll()方法
都属于object方法,必须获得此对象的锁才能调用这几个方法。
sleep(long n)与wait(long n)的区别
1.sleep是thread的方法,wait是object方法
2.sleep不需要强制和synchronized一起使用,但wait需要
3.sleep在睡眠的同时不会释放锁,但wait在等待的时候会释放锁。
synchronized(lock){
while(条件不成立){
lock.wait();
}
//干活
}
//另一个线程
synchronized(lock){
lock.notifyAll();
}
同步模式之保护性暂停
异步模式之生产者消费者
1.new-->runnable
当调用t.start(),由new-->runnable
2.runnable->waiting
t线程用synchronized(obj)获取了对象锁后
调用了obj.wait()方法时,runnable-》waiting
调用obj.notify(),obj.notifyAll(),t.interrupt()时
竞争锁成功waiting-〉runnable
竞争锁失败waiting-》blocked
3.runnable<->waiting
当调用t.join()方法时,当前线程runnable->waiting
t线程运行结束,或调用了当前线程的interrupt方法时,当前线程waiting->runnable
4.runnable<-->waiting
当前线程调用LockSupport.park()方法时,会让当前线程从runnable-》waiting
调用unpack()方法时,或调用了线程的interrupt方法,waiting--》runnable
5.runnable《-》time_waiting
t线程用synchronized(obj)获取了对象锁后
调用wait(long n),runnable-〉time_waiting
当前线程等待时间超过了n秒,或调用obj.notify(),obj.notifyAll(),t.interrupt()方法时,
竞争锁成功time_waiting---》runnable
竞争锁失败time_waiting---〉blocked
6.runnable《-》time_waiting
调用wait(long n),runnable-〉time_waiting
当前线程等待时间超过了n秒,或当t运行结束,或调用了当前线程interrupt方法,time_waiting-》runnable
7.runnable《-》time_waiting
调用thread.sleep(long n),time_waiting---》runnable
等待时间超过n毫秒,runnable-〉time_waiting
8.
9.runnable〈- 〉blocked
t线程用synchronized(obj),获取对象锁如果竞争失败,runnable ---〉blocked
持锁的线程执行完后会唤醒所有blocked的线程重新竞争,如果t线程竞争成功,blocked---》runnable,其他失败的线程仍然blocked
10.runnable-〉terminated
当前线程所有代码执行完毕,进去terminated
活跃性
死锁
t1线程已经获得A对象锁,接下来想获得B对象锁
t2线程已经获得B对象锁,接下来想获得A对象锁
package com.example.mall2.ticket; import lombok.extern.slf4j.Slf4j; /** * Author: zhangbicheng * Date: 2022/5/7 */ @Slf4j public class DeadLock { public static void main(String[] args) { Object a = new Object(); Object b = new Object(); new Thread(() -> { synchronized (a) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { log.info("do something..."); } } }, "t1").start(); new Thread(() -> { synchronized (b) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a) { log.info("do something..."); } } }, "t2").start(); } }
哲学家就餐问题
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
解决:执行顺序交错开
饥饿
得不到机会执行
reentrantLock
相对于synchronized,具备如下特点
可中断
可以设置超时时间
可以设置为公平锁,一般没有必要。
支持多个条件变量
与synchronized一样,支持可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
基本语法
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { //临界区 } finally { //释放锁 lock.unlock(); }
可见性和有序性
volatile
可用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取变量的值,线程操作volatile变量都是直接操作主存。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)