赞
踩
导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
推荐视频:
推荐书籍:
《Java编程思想 (第4版)》
《Java核心技术·卷I(原书第12版) : 开发基础》
目录
11.5.2.4 知识加油站:synchronized锁的原理
进程:是操作系统分配资源的基本单位,有独立的地址空间(内存空间的一部分,用于存储进程中的代码、数据和堆栈等信息)和内存空间,进程之间不能共享资源,上下文切换慢,并发低,能独立执行(有程序入口、执行序列、出口),更健壮(因为进程崩溃后不会影响其他进程)。
线程:是操作系统调度的基本单位,没有独立的地址空间和内存空间(只有自己的堆栈和局部变量,只能共享所在进程的内存空间),线程之间可以共享进程内的资源,上下文切换快,并发高,不能独立执行(应用程序控制多线程执行,进程通过管理线程优先级间接控制线程执行),不健壮(因为一个线程崩溃会导致整个进程崩溃)。
关系:一个程序运行后至少包括一个进程,一个进程至少有一个线程。
运行时数据区包括本地方法栈、虚拟机栈、方法区、堆、程序计数器。每个线程都有独自的本地方法栈、虚拟机栈、程序计数器。各线程共享进程的方法区和堆。
JVM运行时数据区参考:
什么是JVM的内存模型?详细阐述Java中局部变量、常量、类名等信息在JVM中的存储位置_jvm中主要用于存储类的元数据(类型信息(类的描述信息 类的元数据))、静态变量、常-CSDN博客
一个程序运行后至少包括一个进程,一个进程至少有一个线程,一个进程下有多个线程并发地处理任务,称为多线程。
多线程的好处:当一个线程进入阻塞或者等待状态时,其他的线程可以获取CPU的执行权,提高了CPU的利用率。
多线程的缺点:
线程的优先级:java是抢占式调度模型,每一个 Java 线程都有一个优先级,优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
注意:优先级高的线程只是获取CPU时间片的几率高,但并不能保证先执行。
创建线程有4种方式:
创建并启动线程的步骤:
run()和start()区别:
构造方法:
常用方法:
属性方法:
代码示例1:主线程设置名字并查看:
public static void main(String[] args) { Thread.currentThread().setName("主线程"); System.out.println(Thread.currentThread().getName()); }代码示例2:创建并启动线程
线程类:
/** * @Author: vince * @CreateTime: 2024/07/16 * @Description: 打印数字线程类 * @Version: 1.0 */ public class PrintNumberThread extends Thread{ /** * 打印1-100 */ @Override public void run(){ for(int i=0;i<100;i++) { System.out.println(getName()+":"+i); } } /** * 构造方法 * @param name 线程名 */ public PrintNumberThread(String name) { super(name); } }创建并启动线程 :
public class Test { public static void main(String[] args) { PrintNumberThread a = new PrintNumberThread ("a"), b = new PrintNumberThread ("b"); a.start(); b.start(); } }运行结果:
可以看到两个线程是随机交替打印的,因为它们获取CPU的调度是随机的:
Runnable翻译:可运行的
步骤:
这种办法更好,优点:
示例:
/** * @Author: vince * @CreateTime: 2024/07/16 * @Description: 打印数字Runnable * @Version: 1.0 */ public class PrintNumberRunnable implements Runnable{ @Override public void run(){ for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } }
public class Test { public static void main(String[] args) { // 方法1:使用普通方式实现Runnable接口 PrintNumberRunnable runnable = new PrintNumberRunnable(); Thread a = new Thread(runnable, "a"), b = new Thread(runnable, "b"); // 方法2:使用Lambda表达式实现Runnable接口,无需再创建PrintNumberRunnable类 Thread d = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } },"d"); a.start(); b.start(); d.start(); } }运行结果:
通过实现Callable接口来创建线程的步骤如下
相比于前两种方法,此方法可以获取线程执行完后的返回值,而前两种方式不能,因为call()方法是有返回值的。
代码示例:
- class MyCallable implements Callable {
- @Override
- public Object call() throws Exception {
- return null;
- }
- }
- FutureTask task = new FutureTask(new MyCallable());
- new Thread(task).start();
线程池(Thread Pool)是一种多线程处理方式,用于减少创建和销毁线程的开销,提高系统资源利用率和处理效率。
线程池作用:
线程池的两种创建方法:
线程池两种提交任务的方法:
execute和submit都是ExecutorService接口的方法,用于线程池提交任务。所有线程池都直接或间接实现ExecutorService接口。
代码示例:
两种创建线程池的方法:
线程池工具类,创建固定大小的线程池:
ExecutorService executorService = Executors.newFixedThreadPool(10); executorService.execute(new Runnable() { @Override public void run() { System.out.println("当前线程"+Thread.currentThread()); } });自定义线程池(腿姐):
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, //核心线程数 200, //最大线程数量,控制资源并发 10, //存活时间 TimeUnit.SECONDS, //时间单位 new LinkedBlockingDeque<>( 100000), //任务队列,大小100000个 Executors.defaultThreadFactory(), //线程的创建工厂 new ThreadPoolExecutor.AbortPolicy()); //拒绝策略 // 任务1 executor.execute(() -> { try { Thread.sleep(3 * 1000); System.out.println("--helloWorld_001--" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } });
Java线程在运行的生命周期中,在任意给定的时刻,只能处于下列6种状态之一:
等待状态:Object类的wait()方法让线程进入等待状态,notify()唤醒该对象上的随机一个线程,notifyAll()唤醒该对象上的所有线程。这3个方法必须处于synchronized代码块或方法中,否则会抛出IllegalMonitorStateException异常。因为调用这三个方法之前必须拿要到当前锁对象的监视器(Monitor对象),synchronized基于对象头和Monitor对象。
也可以通过Condition类的 await/signal/signalAll方法实现线程的等待和唤醒,从而实现线程的通信,令线程之间协作处理任务。这两个方法依赖于Lock对象。
线程运行过程:线程在创建之后默认为NEW初始状态,在调用start方法之后进入RUNNABLE可运行状态,可运行状态不代表线程正在运行,它有可能正在等待操作系统的调度。WAITING 等待状态的线程需要其他线程的通知才能返回到可运行状态,而TIMED_WAITING超时等待状态相当于在等待状态的基础上增加了超时限制,除了他线程的唤醒,在超时时间到达时也会返回运行状态。此外,线程在执行同步方法时,在没有获取到锁的情况下,会进入到BLOCKED 阻塞状态。线程在执行完run方法之后,会进入到TERMINATED终止状态。
线程通信:用于多个线程之间协作工作,共同完成某个任务。多个线程在并发执行的时候,他们在CPU中是随机切换执行的,这个时候我们想多个线程一起来完成一件任务,这个时候我们就需要线程之间的通信了,多个线程一起来完成一个任务。
线程通信方式:
应用场景:
- //第一个线程,例如打印A
- new Thread(
- () -> {
- while (true) {
- synchronized (lock) {
- // 1.临界值校验:到临界值唤醒其他线程,防止其他线程永远等待;
- // 2.打印判断:如果需要打印,则打印、操作原子类。 如果用的当前行值原子类,则加1;如果用的总行数原子类,则减1
- // 4.线程通信:唤醒、等待。
- // 如果删除下面两行代码,可能导致当前线程释放锁后立刻又拿到锁了,从而达不到交替打印的效果
- lock.notifyAll();
- try-catch{lock.wait();}
-
- }
- }
-
- }
- ).start();
- //另一个线程,例如打印B...
为了对多线程进行统一的管理,Java引入了线程池,它通过限制并发线程的数量、将待执行的线程放入队列、销毁空闲线程,来控制资源消耗,使线程更合理地运行,避免系统因为创建过多线程而崩溃。
线程池作用:
生命周期:
通常线程池的生命周期包含5个状态,对应状态值分别是:-1、0、1、2、3,这些状态只能由小到大迁移,不可逆。
执行器工具类Executors创建线程池: 底层都是return new ThreadPoolExecutor(...)。一般不使用这种方式,参数配置死了不可控。
- ExecutorService executorService = Executors.newFixedThreadPool(10);
-
- //源码
- FixedThredPool: new ThreadExcutor(n, n, 0L, ms, new LinkedBlockingQueue<Runable>()
- SingleThreadExecutor: new ThreadExcutor(1, 1, 0L, ms, new LinkedBlockingQueue<Runable>())
- CachedTheadPool: new ThreadExcutor(0, max_valuem, 60L, s, new SynchronousQueue<Runnable>());
- ScheduledThreadPoolExcutor: ScheduledThreadPool, SingleThreadScheduledExecutor.
一般要搭配计数器CountDownLatch,await(时间)让主线程等待,直到任务线程都执行完(计数器减为零),或者到达超时时间,防止无线等待。
线程池执行器ThreadPoolExecutor创建自定义线程池:
- ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor(
- 5, //核心线程数
- 200, //最大线程数量,控制资源并发
- 10, //存活时间
- TimeUnit.SECONDS, //时间单位
- new LinkedBlockingDeque<>( 100000), //任务队列,大小100000个
- Executors.defaultThreadFactory(), //线程的创建工厂
- new ThreadPoolExecutor.AbortPolicy()); //拒绝策略
-
- CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { //开启异步编排,有返回值
- return 1;
- }, threadPoolExecutor).thenApplyAsync(res -> { //串行化,接收参数并有返回值
- return res+1;
- }, threadPoolExecutor);
- Integer integer = future.get(); //获取返回值
七个参数:
new PriorityBlockingQueue<>((o1, o2) -> o1.length() - o2.length());
下面的参数只是一个预估值,适合初步设置,具体的线程数需要经过压测确定,压榨(更好的利用)CPU的性能。
CPU核心数为N;
核心线程数:
最大线程数:设成核心线程数的2-4倍。数量主要由CPU和IO的密集性、处理的数据量等因素决定。
需要增加线程的情况:jstack打印线程快照,如果发现线程池中大部分线程都等待获取任务、则说明线程够用。如果大部分线程都处于运行状态,可以继续适当调高线程数量。
jstack:打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。线程快照:该进程内每条线程正在执行的方法堆栈的集合。
任务加入时判断的顺序:核心线程数 、阻塞队列、最大线程数、拒绝策略。
线程池执原理:
实际上线程本身没有核心和非核心的概念,都是靠比较corePoolSize和当前线程数判断一个线程是不是能看作核心线程。
可能某个线程之前被看作是核心线程,等它空闲了,线程池又有corePoolSize个线程在执行任务,这个线程到keepAliveTime后还是会被回收。
核心逻辑:创建线程,循环加锁,执行以下逻辑:
坑点:
- 临界值判断不能放到while里:防止最后一个线程无法唤醒其他线程,从而导致死锁(其他线程没人唤醒了)。
- 必须用线程通信:防止当前线程释放锁后立刻又拿回锁(因为多线程是CPU随机切换的),从而达不到交替打印的效果
具体代码(Object类的wait()和notifyAll()方案、不抽取方法):
- /**
- * @Author: vince
- * @CreateTime: 2024/05/13
- * @Description: 多线交替打印A/B/C
- * @Version: 1.0
- */
- public class Test2 {
- /**
- * 当前行值
- */
- private static AtomicInteger index = new AtomicInteger(0);
- /**
- * 总打印行数
- */
- private static final int count = 9;
-
- public static void main(String[] args) {
- Object lock = new Object();
- // 下面创建三个线程可以抽取成一个方法,这里方便理解所以拆开
- new Thread(() -> {
- // tip:这里条件没必要index.get()<count,因为where不在锁里。
- // 如果临界值判断加到这里,会导致最后一个线程无法唤醒其他线程,从而导致死锁(其他线程没人唤醒了)。
- while (true) {
- synchronized (lock) {
- // 1.临界值判断:到达临界值后唤醒其他线程并结束锁;
- if(index.get()>=count){
- lock.notifyAll();
- break;
- }
- // 2.打印判断:如果需要打印,则打印、操作原子类
- if (index.get() % 3 == 0) {
- System.out.println("A");
- // 只有打印后才操作原子类,否则就是不满足条件,需要下一步的唤醒等待后,进入下一轮的循环
- index.getAndIncrement();
-
- }
- // 3.线程通信:唤醒、等待
- // 3.1 唤醒其他线程:不管能不能整除,结束后都唤醒其他线程
- // notifyAll()唤醒该对象上的所有线程
- lock.notifyAll();
- // 3.2 当前线程等待:Object类的wait()方法让线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒
- try {
- lock.wait();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
- }, "线程1打印A").start();
- new Thread(() -> {
- while (true) {
- synchronized (lock) {
- if(index.get()>=count){
- lock.notifyAll();
- break;
- }
- if (index.get() % 3 == 1) {
- System.out.println("B");
- index.getAndIncrement();
- }
- lock.notifyAll();
- try {
- lock.wait();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
- }, "线程2打印B").start();
- new Thread(() -> {
- while (true) {
- synchronized (lock) {
- if(index.get()>=count){
- lock.notifyAll();
- break;
- }
- if (index.get() % 3 == 2) {
- System.out.println("C");
- index.getAndIncrement();
- }
- lock.notifyAll();
- try {
- lock.wait();
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
- }, "线程3打印C").start();
- }
- }
结果:
具体代码(创建和启动线程抽取方法):
import java.util.concurrent.atomic.AtomicInteger; /** * @Author: vince * @CreateTime: 2024/05/13 * @Description: 多线交替打印A/B/C * @Version: 1.0 */ public class Test2 { /** * 当前行值 */ private static AtomicInteger index = new AtomicInteger(0); /** * 总打印行数 */ private static final int count = 9; public static void main(String[] args) { Object lock = new Object(); createAndStartThread("线程1打印A", lock, 0, "A"); createAndStartThread("线程2打印B", lock, 1, "B"); createAndStartThread("线程3打印C", lock, 2, "C"); } private static void createAndStartThread(String threadName, Object lock, int remainder, String output) { new Thread(() -> { while (true) { synchronized (lock) { if (index.get() >= count) { lock.notifyAll(); break; } if (index.get() % 3 == remainder) { System.out.println(output); index.getAndIncrement(); } lock.notifyAll(); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }, threadName).start(); } }
其他线程通信方式:
- Object类的wait()和notifyAll()(采用)
- Conditon的await,sign或signAll方法:创建三个Conditon对象A/B/C,A.await()就是让A线程等待;
- Semaphore的acquire和release方法:使用三个Semaphore对象,分别初始化为1、0、0,表示A、B、C三个线程的初始许可数。每个线程在打印字母之前,需要调用对应的Semaphore对象的acquire方法,获取许可。每个线程在打印字母之后,需要调用下一个Semaphore对象的release方法,释放许可。
线程安全:程序在多线程环境下可以持续进行正确的处理,不会产生数据竞争(例如死锁)和不一致的问题。解决方案:原子类、volatile、锁、线程安全的集合
线程安全的解决方案:按照资源占用情况由轻到重排列:
原子类是具有原子操作特征(化学中原子是最小单位、不可分割)的类,原子是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
在java.util.concurrent.atomic包下,有一系列“Atomic”开头的类,统称为原子类。例如AtomicInteger替代int ,底层采用CAS原子指令实现,内部的存储值使用volatile修饰,因此多线程之间是修改可见的。
以AtomicInteger为例,某线程调用该对象的incrementAndGet()方式自增时采用CAS尝试修改它的值,若此时没有其他线程操作该值便修改成功否则反复执行CAS操作直到修改成功。
CAS:不断对变量进行原子性比较和交换,从而解决单个变量的线程安全问题。。比较内存中值和预期值,如果相等则交换,如果不相等就代表被其他线程改了则重试。
AtomicInteger常用方法:
验证原子类的线程安全:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { public static int num=0; public static void main(String[] args) throws InterruptedException { AtomicInteger atomicInteger = new AtomicInteger(0); // 创建10个线程,分别对atomicInteger进行操作 for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 10000; j++) { atomicInteger.incrementAndGet(); num++; } }).start(); } // 阻塞主线程1s,保证10个线程执行完毕 Thread.sleep(1000); System.out.println(atomicInteger); System.out.println(num); } }可以看到原子类正常加到100000,而num没有:
volatile是一个关键字,被volatile声明的变量存在共享内存中,所有线程要读取、修改这个变量,都是从内存中读取、修改,并且修改操作是原子性的,所以它能保证线程安全。
volatile特性:
读写内存语义:
有序性实现机制:
volatile有序性是通过内存屏障来实现的。内存屏障就是在编译器生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
机器指令:JVM包括类加载子系统、运行时数据区、执行引擎。 执行引擎负责将字节码指令转为操作系统能识别的本地机器指令。
指令重排序:处理器为了提高运算速度会对指令重排序,重排序分三种类型:编译器优化重排序、处理器指令级并行重排序、内存系统重排序。
加锁的方式有两种,分别是synchronized关键字和Lock接口(在JUC包下)。
synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。synchronized锁基于对象头和Monitor对象,在1.6之后引入轻量级锁、偏向锁等优化。
lock锁接口可以通过lock、unlock方法锁住一段代码,Lock实现类都是基于AQS实现的。Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
多条语句共享数据时,多线程程序会出现数据安全问题。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
Java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized关键字和Lock接口(在JUC包下)。
synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。synchronized锁基于对象头和Monitor对象,在1.6之后引入轻量级锁、偏向锁等优化。
lock锁接口可以通过lock、unlock方法锁住一段代码,Lock实现类都是基于AQS实现的。Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
对比线程安全和线程同步:线程同步是实现线程安全的一种手段
synchronized锁:
synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。synchronized锁基于对象头、CAS、Monitor对象,在1.6之后引入轻量级锁、偏向锁等优化。
作用于三个位置:
1.作用在静态方法上,则锁是当前类的Class对象。
2. 作用在普通方法上,则锁是当前的实例(this)。
3. 作用在代码块上,则需要在关键字后面的小括号里,显式指定锁对象,例如this、Xxx.class。
同步代码块作用在代码块上,则需要在关键字后面的小括号里,显式指定锁对象,例如this、Xxx.class。
同步代码块简单来说就是将一段代码用一把锁给锁起来, 只有获得了这把锁的线程才访问, 并且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码.
- synchronized(同步对象) {
- //多条语句操作共享数据的代码
- }
同步代码块的好处:解决了多线程的数据安全问题
弊端:线程很多时,每个线程都会去判断锁,这是很耗费资源和时间的。
代码示例:共有100张票,三个窗口卖票,通过加锁防止超卖
public class SellTicket implements Runnable { private int tickets = 100; private final Object obj = new Object(); @Override public void run() { while (true) { synchronized (obj) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } else { break; } } } } public static void main(String[] args) { SellTicket sellTicket = new SellTicket(); Thread t1 = new Thread(sellTicket, "窗口1"); Thread t2 = new Thread(sellTicket, "窗口2"); Thread t3 = new Thread(sellTicket, "窗口3"); t1.start(); t2.start(); t3.start(); } }
1.作用在静态方法上,则锁是当前类的Class对象。
2. 作用在普通方法上,则锁是当前的实例(this)。
非静态同步方法的锁对象为this。下面代码是相同功能的同步方法和同步代码块:
代码示例:
锁的粒度是当前对象:
// 方法1:实例方法,使用this对象锁 private void sellTicket1() { synchronized (this) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } } } // 方法2:实例方法,使用this对象锁 private void sellTicket2() { synchronized (this) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } } }锁的粒度是真个类:
静态同步方法的锁对象为:类名.class。下面代码是相同功能的同步方法和同步代码块
// 方法3:静态方法,使用类对象锁 private static synchronized void sellTicket3() { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } } // 方法4:静态方法,使用类对象锁 private static void sellTicket4() { synchronized (SellTicket.class) { if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } } }
synchronized锁:
synchronized锁是互斥锁,可以作用于实例方法、静态方法、代码块,能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。synchronized锁基于对象头、CAS、Monitor对象,在1.6之后引入轻量级锁、偏向锁等优化。
作用于三个位置:
1.作用在静态方法上,则锁是当前类的Class对象。
2. 作用在普通方法上,则锁是当前的实例(this)。
3. 作用在代码块上,则需要在关键字后面的小括号里,显式指定锁对象,例如this、Xxx.class。
对象头存储锁信息: synchronized的底层是采用对象头的Mark Word来存储锁信息的。Hotspot 虚拟机(JVM默认虚拟机)中每个对象都有一个对象头(Object Header),包含Mark Word(标记字段) 和 Class Pointer(类型指针)。
不考虑共享资源是类变量等特殊情况的话,有共享资源的多个线程通常都属于同一个对象。
Monitor对象:每个 Java 对象都可以关联一个 Monitor 对象,也称为监视器锁或Monitor锁。Monitor锁用于控制线程对共享资源的访问,开发人员不能直接访问Monitor对象。当一个线程获取了Monitor的锁后,其他试图获取该锁的线程就会被阻塞,直到当前线程释放锁为止。
当一个线程执行synchronized方法或代码块并升级成重量级锁时,当前对象会关联一个Monitor对象,线程必须先获得该对象的Monitor锁才能执行。Monitor有Owner、EntryList、WaitSet三个字段,分别表示Monitor的持有者线程(获得锁的线程)、阻塞队列、和等待队列。
线程通信:synchronized通过Monitor对象,利用Object的wait,notify,notifyAll等方法来实现线程通信。
锁升级:JDK6之前synchronized只有无锁和重量级锁两个状态,JDK6引入偏向锁、轻量级锁两个状态,锁可以根据竞争程度从无锁状态慢慢升级到重量级锁。当竞争小的时候,只需以较小的代价加锁,直到竞争加剧,才使用重量级锁,从而减小了加锁带来的开销。
Lock提供比同步方法和代码块更广泛的锁定操作。
代码示例:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SellTicket implements Runnable { private int tickets = 100; private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { try { lock.lock(); if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 张票"); tickets--; } else { break; } } finally { lock.unlock(); } } } public static void main(String[] args) { SellTicket sellTicket = new SellTicket(); Thread t1 = new Thread(sellTicket); Thread t2 = new Thread(sellTicket); Thread t3 = new Thread(sellTicket); t1.start(); t2.start(); t3.start(); } }
Lock和synchronized有以下几点不同:
lock锁中断线程:若有线程已拿到锁,其他线程使用lock()获取锁时会阻塞,使用lockInterruptibly()获取锁时会直接中断抛出InterruptedException异常
lock锁编码习惯:加锁代码要放到try外面。如果放在try里面的话,加锁失败抛异常或者加锁前的代码抛异常后,执行finally里的解锁代码,而其实加锁都没成功,最终解锁就也不合适了。
- lock.lock(); // 加锁
- try{
- // do something
- }finally{
- lock.unlock(); // 解锁
- }
-
- //不推荐
- try{
- int a=3/0;//这里抛异常会直接进入finally
- lock.lock(); // 加锁
- // do something
- }finally{
- lock.unlock(); // 解锁
- }
分布式锁:SETNX、Redisson。Redisson基于Redis协议,可以实现可重入锁、公平锁、读写锁、信号量、闭锁(计数器),支持看门狗自动续期。
反射:在程序运行期间动态地获取类的信息并对类进行操作的机制。
通过反射机制可以实现:
类的字节码文件和Class对象的区别:
- 类的class字节码文件是编译时生成的,类的class对象是运行时生成的。
- 类的字节码文件是一个存储在电脑硬盘中的文件,例如Test.class;类的Class对象是存放在内存中的数据,可以快速获取其中的信息;
- 两者都存储类的各种信息;
获取类Class对象的JVM底层:如果该类没有被加载过,会首先通过JVM实现类的加载过程,即加载、链接(验证、准备、解析)、初始化,加载阶段会生成类的Class对象。
获取类Class对象的方法:dog.getClass();Dog.class;Class.forName("package1.Dog");
特点:
反射的优缺点:
应用场景:
验证反射可以绕过泛型检查:
基于反射,我们可以给ArrayList<Integer>对象中,加入字符串
public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * @param args * @throws NoSuchMethodException * @throws InvocationTargetException * @throws IllegalAccessException */ public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { ArrayList<Integer> integers = new ArrayList<>(); Class<? extends ArrayList> aClass = integers.getClass(); Method add = aClass.getMethod("add", Object.class); add.invoke(integers, 1); add.invoke(integers, 2); add.invoke(integers, 3); add.invoke(integers, 4); add.invoke(integers, "hello"); System.out.println(integers); } }
Class类的对象:程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的所有方法和属性(包括私有,私有需要给该字段调用setAccessible(true)方法开启私有权限)。
注意类的class对象是运行时生成的,类的class字节码文件是编译时生成的。
获取类Class对象:
Class对象的常用方法:
Spring源码:Bean初始化时判断类是否Bean、判断属性是否需要填充都用到了反射
代码示例:
准备Dog类
/** * @Author: vince * @CreateTime: 2024/07/02 * @Description: 狗类 * @Version: 1.0 */ public class Dog{ /** * 体重 */ private int weight; /** * 名字 */ public String name; public Dog() { } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Dog(int weight, String name) { this.weight = weight; this.name = name; } @Override public String toString() { return "Dog{" + "weight=" + weight + ", name='" + name + '\'' + '}'; } }获取类的Class对象:
public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * @param args */ public static void main(String[] args) throws ClassNotFoundException { //方法1:类的class属性 Class<Dog> c1=Dog.class; //方法2:对象的getClass方法 Dog wangCaiDog = new Dog(23, "旺财"); Class<? extends Dog> c2= wangCaiDog.getClass(); //方法3:Class类的静态方法forName Class<?> c3= Class.forName("package1.Dog"); // 方法4:使用类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Class<?> c5 = systemClassLoader.loadClass("package1.Dog"); // 输出:class package1.Dog System.out.println(c1); //三种方式获取到Class对象地址是完全一致的 // 输出:true System.out.println(c1==c2&&c1==c3); } }获取类的信息:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * * @param args */ public static void main(String[] args) throws Exceptionn { Class<? extends Dog> dogClass = Dog.class; System.out.println(dogClass.getName()); System.out.println(dogClass.getSimpleName()); System.out.println(dogClass.getCanonicalName()); } }
全限定名和规范名:
外部类的全限定名和规范名是一样的,都是“xxx.类名”。区别主要在内部类,内部类的全限定名是“xxx.外部类名$内部类名”,规范名是“xxx.外部类名.内部类名”。
代码示例:
public static void main(String[] args) throws Exception { // java.util.Map System.out.println(Map.class.getName()); // java.util.Map System.out.println(Map.class.getCanonicalName()); // 输出 "java.util.Map$Entry" System.out.println(Map.Entry.class.getName()); // 输出 "java.util.Map.Entry" System.out.println(Map.Entry.class.getCanonicalName()); }
Class对象获取构造器:
Constructor译作构造方法,构造器。
代码示例:
获取所有构造器对象:
/** * @Author: vince * @CreateTime: 2024/07/02 * @Description: 狗类 * @Version: 1.0 */ public class Dog{ /** * 体重 */ private int weight; /** * 名字 */ public String name; public Dog() { } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Dog(int weight, String name) { this.weight = weight; this.name = name; } @Override public String toString() { return "Dog{" + "weight=" + weight + ", name='" + name + '\'' + '}'; } }
public static void main(String[] args) { Class<Dog> dogClass=Dog.class; Constructor<?>[] cons = dogClass.getDeclaredConstructors(); for(Constructor<?> con:cons){ System.out.println(con); } }
获取单个构造器并实例化:
无参:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * @param args */ public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { Class<Dog> dogClass=Dog.class; //获取单个构造方法对象 Constructor<?> con=dogClass.getDeclaredConstructor(); //构造方法对象实例化,会调用无参构造方法 Object dogObject = con.newInstance(); // 无参构造器实例化,也可以直接用Class对象的newInstance方法,带参就不行了 Dog dog = dogClass.newInstance(); //重写了Dog类的to_String,所以输出:Dog{weight=0, name='null'} System.out.println(dogObject); System.out.println(dog); } }带参:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * @param args */ public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { Class<Dog> dogClass=Dog.class; Constructor<?> con=dogClass.getConstructor(int.class,String.class); Object obj = con.newInstance(32,"旺财"); System.out.println(obj); } }
Class对象获取字段:
字段类Field常用方法:
获取字段信息:
获取和设置字段值:
其他方法:
Spring源码:Bean初始化时判断类是否Bean、判断属性是否需要填充都用到了反射
代码示例:
准备Dog类
/** * @Author: vince * @CreateTime: 2024/07/02 * @Description: 狗类 * @Version: 1.0 */ public class Dog{ /** * 体重 */ private int weight; /** * 名字 */ public String name; public Dog() { } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Dog(int weight, String name) { this.weight = weight; this.name = name; } @Override public String toString() { return "Dog{" + "weight=" + weight + ", name='" + name + '\'' + '}'; } }获取成员变量对象并赋值:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * * @param args */ public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException { // 1.获取Class对象,并实例化 Class<? extends Dog> dogClass = Dog.class; Dog dog = dogClass.newInstance(); // 2.获取字段对象 Field nameField = dogClass.getDeclaredField("name"); Field weightField = dogClass.getDeclaredField("weight"); // 3.给字段对象赋值 nameField.set(dog, "旺财"); // 注意私有字段默认不允许赋值,要赋值必须给私有字段设置可访问 weightField.setAccessible(true); weightField.set(dog, 10); System.out.println(dog); } }可以看到私有、公有字段都赋值成功:
通过Field获取的Class类:
/** * @Author: vince * @CreateTime: 2024/06/27 * @Description: 测试类 * @Version: 1.0 */ public class Test { /** * 测试方法,实际场景建议try-catch,而不是throws * @param args */ public static void main(String[] args) throws NoSuchFieldException { Class<Dog> dogClass=Dog.class; Field weightField = dogClass.getDeclaredField("name"); Class<?> dogClassByField = weightField.getDeclaringClass(); // 通过字段获取到的class对象和源class对象是地址是一样的,事实上一个类的所有Class对象都是一个实例 // true System.out.println(dogClassByField==dogClass); } }
Class对象获取成员方法的方法:
Method类的方法:
获取成员变量对象并调用:
- // 1.获取构造方法对象并实例化
- Class<?> c= Class.forName("train.Dog");
- Constructor<?> con=c.getConstructor();
- Object obj = con.newInstance();
- // 2.获取成员方法对象
- Method eat = c.getMethod("eat");
- // 3.通过成员方法对象的invoke方法,调用构造方法对象的成员方法
- //无参无返回值方法
- eat.invoke(obj);
- //带参有返回值方法
- Object sucess= eat.invoke(obj,"food");
- System.out.println((boolean)sucess);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。