JAVA中多线程的基本用法
文章目录
- 一、基本概念
- (一)进程控制块PCB
- (二)并行和并发
- (三)进程调度
- 1.进程的状态
- 2.优先级
- 3.记账信息
- 4.上下文
- (四)进程和线程
- 1.概述
- 2.线程为什么比进程更轻量
- 3.进程和线程的区别和联系
- 二、多线程
- (一)线程的创建
- (二)Thread类的属性和方法
- 1.构造方法
- 2.属性
- 3.启动线程start()
- 4.线程中断
- 5.线程等待
- 6.获取当前线程的引用
- 7.线程休眠
- 三、线程状态
- (一)线程的基本状态
- (二)线程的状态转移
- (三)线程安全
- 1.基本介绍
- 2.产生线程不安全的原因
- 3.死锁
- (1)死锁产生的情况
- (2)死锁的四个必要条件
- 4.synchronized关键字(监视器锁 monitor lock)
- (1)synchronized关键字的特性
- (2)synchronized的使用方法
- 5.JAVA标准库中的线程安全类
- 6.volatile 关键字
- 7.wait 和 notify 方法
一、基本概念
(一)进程控制块PCB
进程可以理解为一个正在运行的程序,进程由PCB、程序段、数据段组成。进程控制块PCB是一个结构体,存储了进程的相关属性;程序段包含程序的代码(指令序列);数据段包含了进程运行过程中产生的各种数据。
操作系统首先描述一个进程,明确出进程上面的一些相关属性,再组织若干个进程,使用数据结构把很多描述信息的进程放到一起,方便进行增删改查。描述进程用到的结构体被称为 PCB(Process Control Block)即进程控制块。PCB中的描述信息包含了进程的基本信息、给进程分配的资源、进程的运行情况等。
所谓的创建进程就是先创建出 PCB,然后把 PCB 加到双向链表中;销毁进程就是找到链表上的PCB,并且从链表上删除;查看任务管理器就是 遍历链表。PCB是进程存在的唯一标志,程序开始运行前,需要创建对应的进程,即创建相应的PCB,PCB当中包含一些属性,包括进程id、内存指针、文件描述符表等
- 进程 id :相当于进程的身份标识,系统中每个进程有唯一的id
- 内存指针:指明了当前该进程要执行的代码/指令在内存的哪个位置,以及进程执行过程中依赖的数据位置。每运行一个exe文件,此时操作系统就会把这个exe加载到内存中变成进程,进程不仅要执行一些编译器生成的二进制指令,还要处理对应的数据。
- 文件描述符: 进程每次打开一个文件,就会在文件描述表上多增加一项,文件描述表的下标就是文件描述符,一个程序只要启动,不管代码中是否包含打开/操作文件的代码,都会默认打开三个文件(标准输入System.in、标准输出System.out、标准错误System.err)。文件描述符可以看成是一个数组,里面的每个元素是一个结构体,对应一个文件的相关信息。
(二)并行和并发
-
并行:微观上两个CPU核心,同时执行两个任务的代码
-
并发:微观上一个CPU核心先执行一会任务1,在执行一会任务2,在执行一会任务3…只要切换的足够快,宏观上看起来,就好像一个CPU在执行多个任务一样
-
并行和并发只能在微观上进行区分,在宏观上来看不能进行区分
-
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在
(三)进程调度
进程的调度就是在考虑CPU资源如何分配给各个进程
操作系统对CPU的资源分配,采用的是时间模式:不同的进程在不同的时间段去使用CPU资源。操作系统对内存的资源分配,采用的是空间模式:不同进程使用内存中的不同区域,互相之间不会干扰。
不同的进程需要访问内存空间,如果所有进程都访问同一个内存空间,那么如果某个进程出现问题把内存中的数据写错了,就可能会引起其他进程的崩溃。这时通过设置虚拟地址空间来实现 " 进程的独立性"
- 两个进程之间隔离开不能直接进行交互,但是在实际工作中,进程之间还是需要互相交互的,此时操作系统就设置了一个 “公共空间” ,进程A把数据放到公共空间上,进程B再取走,从而实现了进程之间的通信
- 操作系统提供的 “公共空间” 有多种,包含存储空间的大小、存取速度的快慢等多种。现在用到的最主要的两种进程间的通讯方式包括:文件操作和网络操作
1.进程的状态
运行状态:进程已占用CPU,并在CPU上执行
就绪状态:进程已具备运行条件,随时可以上CPU去执行
阻塞状态:进程暂时不可以上CPU执行
2.优先级
CPU先给谁分配时间,后给谁分配时间,即进程获取CPU资源的先后顺序
3.记账信息
记账信息统计了每个进程都分别被执行了多久,又分别执行了那些命令,分别排队多久
4.上下文
上下文表示了上次进程被调度出CPU时,当时程序的运行状态,等到下次进程上CPU时就可以恢复之前的状态,再继续往下执行
(四)进程和线程
1.概述
- 进程就是一个跑起来的程序,进程包含线程
- 频繁的创建/销毁/调度进程的成本比较高,若考虑使用进程池,池子里面闲置的进程不使用时也在消耗系统资源,因此考虑使用线程的方式来降低成本,
- 线程比进程更轻量,每个进程或者线程都能够执行一个任务,也都能够并发编程,但是线程的创建/销毁/调度成本比进程要低很多
- 在Linux上,线程也被称为轻量级进程(LWP,Light Weight Process)
2.线程为什么比进程更轻量
-
进程的重量在于资源的申请和释放,线程是包含在进程中的,一个进程中的多个线程共用同一份内存+文件
-
只是创建进程的第一个线程的时候需要分配资源,成本相对较高,但后续这个进程中再创建其他线城市就不必在分配资源了,成本较低
-
同时,如果线程多了,这些线程可能要竞争同一个资源,这样整体的速度就受到了限制
3.进程和线程的区别和联系
- 进程包含线程,一个进程可以有一个线程,也可以有多个线程
- 进程和线程都是为了处理并发变成这样的场景。但是频繁的创建/释放/调度进程的效率比较低,而线程更加轻量,效率更高
- 进程是系统分配资源(内存、文件等资源)的基本单位,线程是系统调度执行的基本单位(CPU)。操作系统创建进程,要给进程分配系统资源,进程是操作系统分配资源的基本单位’操作系统创建的线程,是要在CPU上调度执行,线程是操作系统调度执行的基本单位
- 进程具有独立性,每个进程都有各自独立的虚拟地址空间,不会影响到其他进程
同一个进程中有多个线程时,多个线程共用一个内存空间,一个线程出现bug可能影响到其他线程,甚至导致整个程序崩溃
二、多线程
(一)线程的创建
操作系统提供了一组关于线程的API,JAVA对这组API进一步封装了一下,就成了Thread类。在JAVA标准库中,一个Thread类用来表示或操作线程,Thread类可以理解为JAVA标准库提供的API,创建好的Thread实例,其实和操作系统中的线程是一一对应的关系
1.写法一,创建子类继承Thread
-
run方法内部描述了线程内部要执行的代码,run方法中的逻辑,是在新创建出来的线程中执行的代码,
-
t.start()这里调用start方法,才是真正在系统中创建了线程,并开始run操作,调用star之前系统中是没有创建出线程的
class MyThread extends Thread{@Overridepublic void run(){System.out.println("hello world!");}
}
public class Demo {public static void main(String[] args) {Thread t = new MyThread();t.start();}
}
2.写法二,创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例。
- 通过Runnable来描述任务的内容,进一步的再把描述好的任务交给Thread实例
class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("MyRunnable");}
}
public class Demo3 {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start();}
}
3.写法三/四,使用了匿名内部类,继承自Thread类,同时重写run方法,再new出这个匿名内部类的实例
- 通常认为Runnable的写法会更好些,能够做到让线程和线程执行的任务更好的解耦,因为Runnable单纯只描述了一个任务,但任务通过进程来执行,还是其他的进程池,或者协程来执行,与Runnable本身相关性不大
public static void main(String[] args) {Thread t = new Thread(){@Overridepublic void run() {System.out.println("inner Thread");}};t.start();}
public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("inner Thread");}});t.start();}
4.写法五,使用lambda表达式,本质上是用lambda代替了Runnable
public static void main(String[] args) {Thread t = new Thread(() ->{System.out.println("hello");});t.start();}
多线程能够提高任务完成的效率,尤其是需要同时处理的数据量较大时,例如下面的例子,给a和b两个变量分别自增到count数量级,使用线程处理的时间效率相较于顺序执行要优很多,
运行得到:顺序执行时间为634ms,多线程运行时间为380ms,相差一倍左右
注意创建线程代码中,main和t1、t2并发执行,此时t1、t2还没有执行完就开始记录时间是不准确的,应该让main线程等待t1、t2执行完再来记录,使用join的效果就是等待线程结束, t1.join()让main线程等待t1结束,t2.join()让main线程等待t2结束
public class Demo {private static final long count = 10_0000_0000;public static void serial() {long begin = System.currentTimeMillis();long a = 0;for(long i = 0;i < count; i++){a++;}long b = 0;for(long j = 0;j < count; j++){b++;}long end = System.currentTimeMillis();System.out.println("消耗时间:"+(end-begin)+"ms");}public static void concurrency() throws InterruptedException {long begin = System.currentTimeMillis();Thread t1 = new Thread(() ->{long a = 0;for(long i = 0;i < count; i++){a++;}});t1.start();Thread t2 = new Thread(() ->{long a = 0;for(long i = 0;i < count; i++){a++;}});t2.start();t1.join();t2.join();long end = System.currentTimeMillis();System.out.println("消耗时间:"+(end-begin)+"ms");}public static void main(String[] args) throws InterruptedException {serial();concurrency();}
}
(二)Thread类的属性和方法
1.构造方法
给线程起名字方便再调试的时候对线程进程区分,可以借助JDK中的jconsole观察线程的名字
方法 | 说明 |
---|---|
public Thread() | 创建线程对象 |
public Thread(Runnable target) | 使用Runnable对象创建线程对象 |
public Thread(String name) | 创建线程对象,并命名 |
public Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
2.属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- 是否后台线程isDaemon():JVM会在一个进程的所有非后台线程结束之后才会结束执行。如果线程是后台线程,那么就不影响进程的推出,如果是前台线程,就会影响到进程的进出,例如上面例子中创建的t1、t2线程默认为前台线程,当main方法执行完毕,两个进程也不能退出,需要等t1、t2线程执行完毕后整个进程才能退出;若为后台线程,那么main方法执行完毕后整个进程直接退出,t1、t2线程强行终止
- 是否存活 isAlive():指操作系统中对应的线程是否正在运行,Thread t 只是创建了一个对象,创建 t 对象后调用t.start()才是创建了一个线程,因此t对象的生命周期和内核中线程对应的生命周期并不完全一致,run方法执行完毕后系统中的线程就销毁了,但是t对象可能还存在。start()开始之后,run()结束之前的时间内isAlive()判定为true
3.启动线程start()
- start()是真正决定线程是否创建的方法,而run()方法仅是一个普通的方法,仅调用run方法并不会创建新的线程。
例如下面的代码,没有start()创建线程,程序中依然只有main这一个线程,循环五次hello thread后顺序执行
public class Demo {public static void main(String[] args) {Thread t = new Thread(()->{while (true){System.out.println("hello thread");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.run();while(true){System.out.println("hello main");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
4.线程中断
中断线程即让线程对应的run方法执行完毕,特殊的main线程来说,mian方法执行完毕,线程即结束,有以下两种方式进行线程中断
1. 手动设置一个标志位来控制线程是否要执行。此处因为多线程共用一个虚拟地址空间,因此main线程修改的 isQuit和 t 线程判定的 isQuit 是同一个值
public class Demo {private static boolean isQuit = false;public static void main(String[] args) {Thread t = new Thread(()->{while (!isQuit){System.out.println("hello thread");try{Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}isQuit = true;System.out.println("终止t线程");}
}
2. 使用Thread内置的标志位来进行判定,使用Thread.interrupted();设置的一个静态的方法,或者使用Thread.currentThread().isInterrupted() 实例的方法,currentThread能够获取到当前线程的实例
-
一般来说更推荐使用 isInterrupted方法,因为Thread.interrupted()方法判定的标志位是Thread的一个static静态成员,这样一个程序中只有一个标志位;而一个代码中的线程可能有多个,Thread.currentThread().isInterrupted()这个方法判断的标志位是Thread的普通成员,每个示例都有自己的标志位
-
对于调用这个t.interrupt();方法,需要注意 如果t线程处在就绪状态,就设置线程的标志位为true;如果t线程处在阻塞状态(sleep状态),就会触发一个InterruptedException异常
-
对于下面的代码来说,运行后发现interrupt方法会在线程阻塞态时调用,此时就会让sleep触发一个异常从而导致线程从阻塞状态被唤醒,一旦触发异常之后,就进入了catch语句,从而打印了一个日志,可以在打印后立刻break,来避免这种情况
public class Demo {public static void main(String[] args) {Thread t = new Thread(()->{while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;//添加的操作}}});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}t.interrupt();}
}
5.线程等待
一般来说,线程之间的调度是按照调度器来安排的,这个过程是无序且随机的,当我们需要控制线程之间的顺序时,需要使用到join方法,对应的线程会进入阻塞等待,直到对应的线程执行完毕(对应线程的 run 方法执行完)
-
在下面这个例子中,main线程中调用 t.join,那么main线程就会进入阻塞状态,直到 t 线程执行完毕,一定程度上干预了两个线程的执行顺序
-
join操作默认是死等,因此提供了另一个版本,设置最多join最多等待过长时间,即在join内部设置最大时间,当超过这个时间之后,这样join就直接返回,不再继续等待
public class Demo {public static void main(String[] args) {Thread t = new Thread(()->{for(int i=0;i<5;i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});try {t.join(10000);} catch (InterruptedException e) {e.printStackTrace();}}
}
6.获取当前线程的引用
Thread.currentThread()能够获取到当前线程的引用,哪个线程调用的currentThread,就获取到哪个线程的实例
下面这个代码中通过继承Thread的当时来创建线程,此时在run方法中拿到的就是Thread的实例
public class Demo11 {public static void main(String[] args) {Thread t = new Thread(){@Overridepublic void run(){//System.out.println(Thread.currentThread().getName());System.out.println(this.getName());}};t.start();//在main线程中调用的,因此拿到的就是main这个线程实例System.out.println(Thread.currentThread().getName());}
}
如果是实现了Runnable接口的线程,由于Runnable是一个单独的任务没有name属性,此时不能通过this获得引用,只能通过Thread.currentThread().getName()进行获取
public class Demo {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println(this.getName());//errorSystem.out.println(Thread.currentThread().getName());}});t.start();}
}
7.线程休眠
当一个进程有多个线程的时候,此时每个线程都有一个PCB,此时一个进程就对应一组PCB了。PCB上的一个字段 tgroupId 这个 id 相当于是进程的 id,同一个进程中的若干个线程的tgroupId 是相同的
- 针对Linux系统而言,当前链表中的PCB都有各自的状态(就绪/阻塞),当其中某个线程调用了sleep方法,这个PCB就会进入到阻塞队列,,当操作系统调度线程的时候,就只是从就绪队列中挑选合适的PCB到CPU上运行,阻塞队列中的PCB只能等到睡眠时间结束后,操作系统才会把其从阻塞队列调回到就绪队列
三、线程状态
(一)线程的基本状态
通过t.getState()方法获取对应线程的状态
- NEW状态:安排了工作还未开始行动,即Thread对象创建了但是还没有调用start()方法
- TERMINATED状态:工作完成了,操作系统中的线程已经执行完毕销毁了,但是Thread对象还在,从而获取到的状态
- RUNNABLE状态:可工作的,又可以分为正在工作中和即将开始工作的,也是就绪状态,处于该状态的线程在就绪队列中,随时可以被调用到CPU上执行。如果代码中没有进行sleep或其他可能导致阻塞的操作,线程即在RUNNABLE状态
- TIMED_WAITING状态:代码中调用了 sleep 或 join 等操作就会进入到该状态,即当前线程在一定时间内是阻塞的状态,一定时间后状态解除
- BLOCKED状态:当前线程在等待锁导致了阻塞
- WAITING状态:当前线程在等待唤醒导致了阻塞
(二)线程的状态转移
(三)线程安全
1.基本介绍
线程的安全主要指的是由于操作系统进行线程调度的时候是否会带来问题
一个经典的线程不安全的例子是两个线程分别自增对应的数字,如下所示,两个线程总共相加应该为100000的情况,但最终得到的结果总在50000和100000之间
class Counter{public int count;public void increase(){count++;}
}
public class Demo13 {private static Counter counter = new Counter();private static int count2 = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i<50000;i++){counter.increase();count2++;}});Thread t2 = new Thread(()->{for(int i = 0;i<50000;i++){counter.increase();count2++;}});t1.start();t2.start();//在t1、t2执行完之后再打印count的结果//在main中打印两个线程执行的结果t1.join();t2.join();System.out.println(counter.count);System.out.println(count2);}
}
count在进行自增时候,首先要内存中的 count 值加载到CPU寄存器中,在寄存器中的值加一,最后把寄存器的值写回到内存的count中。因此在“抢占式执行”的时候,两个操作导致本来应该自增两次,结果只自增了一次
可以通过在自增之前加锁,自增后解锁。加锁后并发程度就降低了,此时的数据操作更具有安全性,但也会导致执行速度变慢。加锁最常使用synchronized,给方法直接加synchronized关键字,此时进入方法会自动加锁,离开方法自动解锁。线程加锁成功后,其他线程尝试加锁会触发阻塞等待,此时的线程就处在BLOCKED状态,阻塞会一致持续到占用锁的线程把锁释放位置。
class Counter{public int count;synchronized public void increase(){count++;}
}
2.产生线程不安全的原因
- 线程抢占式执行,线程之间的调度随机
- 多个线程对同一个变量进行修改,针对两个变量的情况不会产生问题,或多个线程针对同一个变量进行读操作时也不会产生问题
- 针对变量的操作不是原子性的,例如针对读取变量的值,该操作只对应一条机器语言,因此可以认为是原子的,对非原子的操作可以通过加锁把多个指令打包成一个原子的
- 内存的可见性也会影响到线程的安全。即一个t1线程在循环读,一个t2线程进行写操作,由于 t1 线程读取内存的效率比读取寄存器的效率要低很多(3-4个数量级),于是编译器自我优化,此时t1就直接从寄存器中读取数据,t2 再进行修改的后,t1 就会读取到错误的数据,如下例所示,线程 t 没有感知到数据的变化
import java.util.Scanner;public class Demo {private static int isQuit = 0;public static void main(String[] args) {Thread t =new Thread(()->{while(isQuit == 0){//循环读取isQuit的值}System.out.println("循环结束!t 线程退出!");});t.start();Scanner scanner = new Scanner(System.in);System.out.println("请输入一个isQuit值:");//输入修改后t线程并没有执行结束isQuit = scanner.nextInt();System.out.println("main线程执行完毕");}
}
针对内存的可见性导致的线程不安全,可以使用下面的方法进行修改
- 使用synchronized关键字: synchronized以保证指令的原子性,同时也可以保证内存的可见性,相当于手动禁用了编译器的优化
- 使用volatile关键字,和原子性无关但能保证内存可见性,会禁止编译器进行优化,此时编译器每次执行判定会重新读取内存的值,如下所示
public class Demo {private static volatile int isQuit = 0;public static void main(String[] args) {...}
}
如果在循环中加入sleep,可以发现编译器优化消失了,即也不存在内存可见性问题
public class Demo {private static int isQuit = 0;public static void main(String[] args) {Thread t =new Thread(()->{while(isQuit == 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("循环结束!t 线程退出!");});...}
}
- 指令重排序,也是编译器优化中的一种操作,即编译器在不改变逻辑的前提下,改变代码执行的先后顺序,需要使用synchronized关键字,同时保证原子性、内存可见性、禁止指令重排序
3.死锁
(1)死锁产生的情况
- 一个线程,同时加多个锁,相互等待
- 两个线程,两个锁,互相等待
- N个线程M个锁(哲学家就餐问题)
(2)死锁的四个必要条件
- 互斥使用:进程之间互斥使用资源,任意时刻一个资源只能给一个进程使用,即一个锁被一个线程占用后其他线程占用不了
- 不可抢占:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放,即一个锁被线程占用后,其他线程只能等待其释放,不能进行抢占
- 请求和保持:进程在申请新资源时,继续占用已分配到的资源,即线程占据了多把锁后,除非显示的释放锁,否则会一直持有
- 环路等待:每个线程都在等待下一个线程持有的资源,从而形成一个锁
4.synchronized关键字(监视器锁 monitor lock)
(1)synchronized关键字的特性
-
synchronized会起到互斥的效果
-
synchronized可以刷新内存,保证内存的可见性。首先获得互斥锁,从主内存中拷贝变量的副本到工作的内存,执行代码后将更改后的共享变量的值更新到主内存,最后释放互斥锁
-
synchronized具有可重入的特性
对于连续加锁的操作来说,一般来说,外层锁需要等到整个方法执行完毕后才能释放,而内层锁又由于外层锁被占用无法进行加锁,从而导致死锁情况。但对于synchronized来说这种连续加锁的操作并不会导致死锁。
class Counter{public int count;synchronized public void increase(){synchronized(this){count++;}}
}
synchronized会在加锁时记录加锁的次数,后续在解锁的时候再依次减少,解锁次数减到0的时候完全解锁,这就是可重入锁。这种锁降低了使用成本,相应提高了开发效率,但是加减计数操作降低了运行效率
(2)synchronized的使用方法
- 1.修饰普通的方法:在使用synchronized的时候,本质上是在对某个“对象”进行加锁,此时的锁对象就是this。一个new出来的实例,包含了程序员自身设置的属性,还包括一个“对象头”,包含对象的一些元数据,加锁操作就是在对象头里设置了标志位,加锁对象在this的对象头的标志位
class Counter{public int count;synchronized public void increase(){count++;}
}
- 2.修饰一个代码块:需要显示的指定那个对象加锁,在JAVA中,任意对象都可以作为一个锁对象,这也是JAVA语言的一个特色
class Counter{public int count;public void increase(){synchronized(this){count++; }}
}
- 3.修饰一个静态方法:因为静态方法本质上来说是一个类方法,因此相当于给当前的类对象加锁
class Counter{public int count;public static void func(){synchronized(Counter.class){}}
}
class Counter{public int count;synchronized public static void func(){}
}
5.JAVA标准库中的线程安全类
-
线程不安全类:ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder
-
线程安全类:vector、HashTable、ConcurrentHashMap、StringBuffer(前四种都带有synchronized修饰)、String(String是不可变对象)
6.volatile 关键字
volatile 关键字能够保证内存可见性,但不能保证原子性,因此 volatile 只能处理一个线程读,一个线程写之间的问题
-
JMM(Java Memory Model,Java内存模型)将硬件结构在Java中用专门的术语封装了一遍,CPU抽象成工作内存(work memory),内存抽象成主内存(main memory)
-
频繁读取数据时,CPU从内存中读取的数据速度太慢,因此要把这样的数据放到寄存器里,通过寄存器进行读取,但寄存器空间紧张,而缓存cache比寄存器大,比内存小,速度比寄存器慢,比内存快,因此数据会放到缓存中进行读取
-
对于一般的三级缓存来说,存储空间:CPU < L1 < L2 < L3 < 内存;速度:CPU > L1> L2 > L3 > 内存;成本:CPU > L1> L2 > L3 > 内存。最常用的数据放到CPU的寄存器里
-
当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去
7.wait 和 notify 方法
wait 和 notify 都是 Object 对象的方法,都针对同一个对象来操作,都需要搭配 synchronized 来使用。调用wait方法会首先使线程进入到阻塞状态,阻塞到其他线程通过notify进行通知
- wait 内部首先会把线程放入到等待队列中,然后释放当前的锁,最后等待其他线程的通知,收到通知后重新获取锁,并继续往下执行
- notify 也需要在 synchronized 下进行使用,通过notify来通知可能等待该对象的对象锁的其他线程,并发出notify通知,并使他们重新获取该对象的对象锁
- 可以通过wait 和 notify来协调多个线程之间执行的先后顺序
- notifyAll会全部唤醒所有的等待线程,notify则是随机唤醒一个等待线程
- notifyAll在唤醒所有等待线程的时候,wait会重新获取锁,同样也会发生竞争
public class Demo {private static Object locker = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{synchronized (locker){System.out.println("wait 前");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("wait 后");}});t1.start();Thread.sleep(3000);Thread t2 = new Thread(()->{synchronized (locker){System.out.println("notify 前");locker.notify();System.out.println("notify 后");}});t2.start();}
}
相关文章:
JAVA中多线程的基本用法
文章目录 一、基本概念(一)进程控制块PCB(二)并行和并发(三)进程调度1.进程的状态2.优先级3.记账信息4.上下文 (四)进程和线程1.概述2.线程为什么比进程更轻量3.进程和线程的区别和联…...
健康与好身体笔记
文章目录 保证睡眠饭后百步走,活到九十九补充钙质一副好肠胃肚子咕咕叫 健康和工作的取舍 以前对健康没概念,但是随着年龄增长,健康问题凸显出来。 持续维护该文档,健康是个永恒的话题。 保证睡眠 一是心态要好,沾枕…...
如何下载谷歌浏览器增强版(扩展支持版)
在日常浏览和工作中,Chrome 浏览器因其强大的性能和丰富的扩展插件,成为全球范围内使用最广泛的浏览器之一。然而,对于需要进行深度扩展管理或需要稳定扩展环境的用户来说,标准版的 Google Chrome 可能在某些方面仍显不足。这时候…...
TDDMS分布式存储管理系列文章--分片/分区/分桶详解
友情链接: 星环分布式存储TDDMS大揭秘(一)分布式存储技术推出背景以及当前存在的挑战TDDMS是什么 前情提要 通过上个系列的文章我们了解到了各节点数据副本间通过一致性算法确保每次写入在响应客户端请求之前至少被多数节点(N/2…...
Spring Boot(九十):集成SSE (Server-Sent Events) 服务器实时推送
1 SSE简介 Server-sent Events(SSE) 是一种基于 HTTP 协议的服务器推送技术,它允许服务器主动向客户端发送数据。与 WebSocket 不同,SSE 是单向通信,即服务器可以主动向客户端推送数据,而客户端只能接收数据。 2 SSE特点 单向通信:SSE 是服务器向客户端的单向推送,客户…...
ubuntu22.04安装ROS2 humble
参考: https://zhuanlan.zhihu.com/p/702727186 前言: 笔记本安装了ubuntu20.04安装ros一直失败,于是将系统升级为ununut22.04,然后安装ros,根据上面的教程,目前看来是有可能成功的。 系统升级为ununut…...
力扣第206场周赛
周赛链接:竞赛 - 力扣(LeetCode)全球极客挚爱的技术成长平台 1. 二进制矩阵中的特殊位置 给定一个 m x n 的二进制矩阵 mat,返回矩阵 mat 中特殊位置的数量。 如果位置 (i, j) 满足 mat[i][j] 1 并且行 i 与列 j 中…...
C++17 主要更新
C17 主要更新 C17 是继 C14 之后的重要标准更新,引入了许多提升开发效率、简化代码和增强性能的特性。以下是 C17 的主要更新,按类别分类: 1. 语言核心特性 结构化绑定(Structured Bindings) 解构元组、结构体或数组…...
k8s master节点部署
一、环境准备 1.主机准备 192.168.10.100 master.com master 192.168.10.101 node1.com node1 192.168.10.102 node2.com node2 互信 时间同步 关闭防火墙 关闭selinux 2.创建/etc/sysctl.d/k8s.conf,添加如下内容 cat > /etc/sysctl.d/k8s.conf <<EOF net.br…...
YOLO学习笔记 | YOLOv8 全流程训练步骤详解(2025年4月更新)
===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 这里写自定义目录标题 一、数据准备1. 数据标注与格式转换2. 配置文件生…...
centos7.9 升级 gcc
本片文章介绍如何升级gcc,centos7.9 仓库默认的gcc版本为:4.8.5 4.8.5-44) Copyright (C) 2015 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY…...
Linux基本指令
Linux目录结构 Linux的目录结构是一个树形结构。Windows系统可以拥有多个盘符,如C盘、D盘、E盘。而Linux没有盘符这个概念,只有一个根目录/,所有文件都在它下面。如下图所示: Linux路径的描述方式 在Linux系统中,路径之间的层级…...
Google A2A协议,是为了战略性占领标准?
一、导读 2025 年 4 月 9 日,Google 正式发布了 Agent2Agent(A2A)协议。 A2A 协议致力于打破智能体之间的隔阂,让它们能够跨越框架和供应商的限制,以一种标准化、开放的方式进行沟通与协作 截止到现在,代…...
每日一题(小白)暴力娱乐篇29
题目比较简单,主要是判断条件这块,一定要注意在奇数的位置和偶数的位置标记,若奇数位为奇数偶数位为偶数才能计数加一,否则都是跳过。 ①接收数据n ②循环n次,拆解n,每次拆解记录ans ③拆解n为若干次x&a…...
瀚天天成闯港交所上市:业绩波动明显,十分依赖少数客户和供应商
撰稿|张君 来源|贝多财经 近日,瀚天天成电子科技(厦门)股份有限公司(下称“瀚天天成”)递交招股书,报考港交所主板上市。据贝多财经了解,瀚天天成曾计划在上海证券交易所科创板上市࿰…...
全国产压力传感器常见的故障有哪些?
全国产压力传感器常见的故障如哪些呢?来和武汉利又德的小编一起了解一下,主要包括以下几类: 零点漂移 表现:在没有施加压力或处于初始状态时,传感器的输出值偏离了设定的零点。例如,压力为零时,…...
计算机视觉卷积神经网络(CNN)基础:从LeNet到ResNet
计算机视觉卷积神经网络(CNN)基础:从LeNet到ResNet 一、前言二、卷积神经网络基础概念2.1 卷积层2.1.1 卷积运算原理2.1.2 卷积核的作用与参数 2.2 池化层2.2.1 最大池化与平均池化2.2.2 池化层的优势与应用 2.3 全连接层2.3…...
在封装DLL时,避免第三方命名空间的依赖方法[PIMPL模式技术指南]
1. 概述 PIMPL(Pointer to IMPLementation)模式是C++中实现信息隐藏和二进制兼容性的重要设计范式,通过创建实现细节的私有封装层,有效隔离接口与实现。本文档详细阐述其核心原理、现代实现方式和典型应用场景。 2. 核心架构 2.1 经典结构 // 头文件(widget.h) class Wid…...
镜舟科技亮相 2025 中国移动云智算大会,展示数据湖仓一体创新方案
4月10-11日,2025 中国移动云智算大会在苏州金鸡湖国际会议中心成功举办。大会以“由云向智,共绘算网新生态”为主题,汇聚了众多行业领袖与技术专家,共同探讨了算力网络与人工智能的深度融合与未来发展趋势。 作为中国领先的企业级…...
通过Python实现定时重启H3C AP设备
一、背景 因为H3C的AP设备老化,网络出现高延迟、高丢包率,需要隔一段时间去重启AP后恢复。但是由于白天在使用无法进行重启,并且容易遗忘等用户反馈又太晚了,但是AC版本太老没有定时重启功能,于是通过Python做了自动重…...
火山RTC 4 音视频引擎 IRTCVideo,及 音视频引擎事件回调接口 IRTCVideoEventHandler
一、IRTCVideo、IRTCVideoEventHandler 音视频引擎 IRTCVideo,及 音视频引擎事件回调接口 IRTCVideoEventHandler 负责音视频管理、创建房间/获得房间实例 1、创建引擎、及事件回调示例 如: void VideoConfigWidget::initRTCVideo() {m_handler.res…...
Matlab 考虑电机激励力的整车垂向七自由度模型参数研究
1、内容简介 Matlab 201-考虑电机激励力的整车垂向七自由度模型参数研究 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
Matlab 三容水箱系统故障诊断算法研究
1、内容简介 Matlab 190-三容水箱系统故障诊断算法研究 可以交流、咨询、答疑 2、内容说明 略 其次,对 DTS200 三容水箱系统进行机理建模,可分为对象建模和故障 建模,搭建了水箱系统的 SIMULINK 模型并建立了基于 Taylor 展开及 T-…...
Mac学习使用全借鉴模式
Reference https://zhuanlan.zhihu.com/p/923417581.快捷键 macOS 的快捷键组合很多,相应的修饰键就多达 6 个(Windows 系统级就 4 个): Command ⌘ Shift ⇧ Option ⌥ Control ⌃ Caps Lock ⇪ Fn 全屏/退出全屏 command con…...
Arrays.asList() 隐藏的陷阱
在Java中,我们经常需要将数组转换为List来方便地进行操作。Arrays.asList()方法是一种常见的方式,但是它存在一个不太常见但需要注意的坑。 本文将深入探讨Arrays.asList()的使用,揭示其中的陷阱,并提供解决方案。 1、Arrays.as…...
Cables 现已正式启动积分计划 Alpha 阶段,开放早期白名单申请
Cables 现已正式启动积分计划,并开放白名单抢先体验通道,这标志着 Cables 平台进入第一阶段的部署,旨在为外汇及现实世界资产(RWAs)构建首个集成的流动性质押与永续期货 DEX。 Cables 平台的设计目标是通过单一系统实…...
Spring Cloud 远程调用
4.OpenFeign的实现原理是什么? 在使用OpenFeign的时候,主要关心两个注解,EnableFeignClients和FeignClient。整体的流程分为以下几个部分: 启用Feign代理,通过在启动类上添加EnableFeignClients注解,开启F…...
STM32单片机中EXTI的工作原理
目录 1. EXTI概述 2. EXTI的组成部分 3. 工作原理 3.1 引脚配置 3.2 中断触发条件 3.3 中断使能 3.4 中断处理 4. 使用示例 5. 注意事项 结论 在STM32单片机中,EXTI(外部中断)是一种用于处理外部事件的机制,能够提高对硬…...
排序算法详细介绍对比及备考建议
文章目录 排序算法对比算法逐一介绍1. 冒泡排序(Bubble Sort)2. 选择排序(Selection Sort)3. 插入排序(Insertion Sort)4. 希尔排序(Shell Sort)5. 归并排序(Merge Sort&…...
网页布局思路
一、布局思路 1,确定页面的版心(可视区) 2、分析页面中的行模块,以及每个行模块中的列模块。(页面布局第一准则) 3、一行中的列模块经常用浮动布局,先确定每个列的大小,之后确定列的位置(页面…...
CloudFlare Page 如何和 GitHub 创建连接
CloudFlare Page 能够对前端项目进行构建。 他们能支持从 GitHub 上直接拉取代码。 如果 GitHub 上的代码仓库不存在的话,首先需要创建一个连接才可以。 随后会要求登录 GitHub,然后可以在希望访问的组织中进行选择。 随后同意访问赋予权限即可。 Clou…...
Python爬虫第13节-解析库pyquery 的使用
目录 前言 一、pyquery 初始化 1.1 字符串初始化 1.2 URL 初始化 1.3 文件初始化 二、基本 CSS 选择器 三、pyquery 查找节点 3.1 子节点 3.2 父节点 3.3 兄弟节点 四、遍历 五、获取信息 5.1 获取属性 5.2 获取文本 六、节点操作 6.1 addClass 和 removeClass…...
【学习笔记】头文件中定义函数出现重复定义报错
目录 错误复现原因解决方案inlinestatic 扩展参考 错误复现 现在有一个头文件 duplicate_define.h 和两个源文件 duplicate_define_1.cpp 和 duplicate_define_2.cpp。 两个源文件都引入了头文件 duplicate_define.h,且在各自的函数中调用了定义在头文件中的全局函…...
Java 中的零拷贝技术:提升性能的利器
Java 中的零拷贝技术:提升性能的利器 在现代高性能应用中,数据传输的效率至关重要。传统的 I/O 操作通常涉及多次数据拷贝,这会导致性能瓶颈。而零拷贝(Zero-Copy)技术通过减少数据拷贝次数,显著提升了 I/…...
JavaScript:基本语法
今天我要介绍的新知识点内容为:JavaScript的基本语法以及使用说明。 首先我们先了解一下JS(JavaScript简称)是什么以及怎么使用JS: 介绍:JavaScript(JS)是一门弱类型的语言,用于给HTML页面上添加动态效果…...
Matlab 三维时频图
1、内容简介 Matlab 202-三维时频图 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
MySQL中动态生成SQL语句去掉所有字段的空格
在MySQL中动态生成SQL语句去掉所有字段的空格 在数据库管理过程中,我们常常会遇到需要对表中字段进行清洗和整理的情况。其中,去掉字段中的空格是一项常见的操作。当表中的字段数量较少时,我们可以手动编写 UPDATE 语句来处理。但如果表中包…...
NO.91十六届蓝桥杯备战|图论基础-图的存储和遍历|邻接矩阵|vector|链式前向星(C++)
图的基本概念 图的定义 图G是由顶点集V和边集E组成,记为G (V, E),其中V(G)表⽰图G中顶点的有限⾮空集;E(G)表⽰图G中顶点之间的关系(边)集合。若 V { v 1 , v 2 , … , v n } V \left\{ v_{1},v_{2},\dots,v_{n} …...
树、二叉树、二叉查找树、AVL 树及红黑树的深入解析
树、二叉树、二叉查找树、AVL 树及红黑树的深入解析 1 .树的基本知识1.1 树的定义1.2 基本术语和概念1.3 常见树的结构1.4 树的遍历(取决于什么时候访问根节点) 2 二叉树2.1 二叉树的定义2.2二叉树与度为2的树的区别2.3二叉树的性质2.4 二叉树分类 3 红黑…...
BUUCTF-web刷题篇(21)
30.hark world 判断注入类型: 输入1报错提示bool(false)可知是字符型的布尔注入(盲注) 尝试万能密码 1 or 11 已检测SQL注入,猜测某些关键词或者字符被过滤。 使用FUZZ字典爆破...
Linux 网络基础知识总结
Linux 网络基础知识总结 1. 计算机网络体系结构 • OSI七层模型 由国际化标准组织(ISO)制定,将网络通信分为七层: • 物理层:比特流传输(如网线、光纤)。 • 数据链路层:帧传输&am…...
Day 8 上篇:深入理解 Linux 驱动模型中的平台驱动与总线驱动
在 Linux 内核驱动模型中,设备与驱动的组织方式不是随意堆砌,而是基于清晰的分类逻辑进行架构设计的。最核心的架构基础是“设备模型”(Device Model),而在此模型之上,各类驱动通过“平台驱动模型”与“总线…...
如何启动spark
解决:spark的bin目录下,无法启动spark问题 [roothadoop7 sbin]# ./start-all.sh ./start-all.sh:行29: /root/install/spark-2.4.0-bin-hadoop2.7/sbin/spark-config.sh: 没有那个文件或目录 ./start-all.sh:行32: /root/install/spark-2.4.0-bin-hadoo…...
Java网络编程干货
1.网络编程是什么 了解 在Java语言中,我们可以使用java.net包下的技术轻松开发出常见的网络应用程序,从而把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统&#x…...
Java实现安卓手机模拟操作
文章目录 第一部分:安卓模拟操作基础1.1 安卓输入系统概述1.1.1 输入事件传递机制1.1.2 输入事件类型 1.2 模拟操作的核心类1.2.1 Instrumentation类1.2.2 KeyEvent类1.2.3 MotionEvent类 1.3 权限要求1.3.1 普通权限1.3.2 特殊权限 第二部分:基础模拟操…...
一文讲清楚PLC、运动控制卡、运动控制器
随着工业技术的发展,工业机器人应用越来越广泛,PLC也不再是简单的可编程逻辑控制器,各个品牌厂家都推出了自己的运动控制型PLC,来实现一些运动控制功能,与此同时,运动控制卡及运动控制器也在如火如荼地发展…...
蓝桥杯备战
#include<bits/stdc.h> using namespace std; int main(){ios::sync_with_stdio(false);cin.tie(0);return 0; } 输入输出加速 ios::sync_with_stdio(false) 作用: 禁用 C 和 C 标准流的同步,使 cin/cout 速度接近 scanf/printf。 适用性ÿ…...
python保留关键字详解
一、什么是保留关键字? 保留关键字是Python语言中具有特殊含义和功能的词汇,这些词汇构成了Python的语法基础。它们不可被重新定义或用作变量名、函数名等标识符,在代码中承担着控制程序逻辑、定义数据结构等重要职责。 二、查看保留关键字…...
NLP中的“触发器”形式
在自然语言处理(NLP)中,触发器的设计更加依赖于文本特征,而非视觉特征。以下是NLP中常见的触发器类型及其实现方式: 1. 特定词汇或短语 定义:在文本中插入特定的单词、短语或符号。示例: 罕见…...
uView修改样式(持续更新)
场景 通过样式穿透修改uView2.0组件样式,用于app 注意版本不一样方法可能不同 实现 通用 .uni-body{line-height: 0; }u-input ::v-deep .u-input{height: 20.51rpx !important;padding: 0 6.59rpx !important; } ::v-deep .uni-input-input{height:50%;font-s…...