当前位置: 首页 > news >正文

【JAVA EE初阶】多线程(进阶)

目录

1.常见的锁策略

1.1 悲观锁vs乐观锁

1.2 重量级锁vs轻量级锁

1.3 挂起等待锁vs自旋锁

1.4 普通互斥锁vs读写锁

1.5 可重入锁vs不可重入锁

1.6 公平锁vs非公平锁

1.7 synchronized的优化:

1.8 相关面试题

2.CAS比较和交换(compare and swap)

2.1 相关面试题

3.JUC中的组件

3.1 Callable接口

3.2 ReentrantLock可重入

3.3 原子类

3.4 线程池

3.5 信号量Semaphore

3.6 CountDownLatch

3.7 相关⾯试题

4.线程安全的集合类

4.1 多线程环境使用ArrayList

4.2 多线程环境使用队列

4.3 多线程环境使用哈希表

5. 面试题


1.常见的锁策略

1.1 悲观锁vs乐观锁

不是针对某一个具体的锁,而是某个具体锁具有“悲观”特性或者“乐观”特性。

悲观:加锁的时候,预测接下来的锁竞争的情况非常激烈,需要针对这样的激烈情况额外做一些工作。

乐观:加锁的时候,预测接下来的锁竞争的情况不激烈,不需要做额外工作。

1.2 重量级锁vs轻量级锁

重量级锁,当悲观的场景下,此时要付出更多的代价(更低效)。

轻量级锁,当乐观的场景下,此时要付出的代价更小(更高效)。

1.3 挂起等待锁vs自旋锁

挂起等待锁:重量级锁的典型实现。操作系统内核级别的,加锁的时候发现竞争,就会使该线程进入阻塞状态,后续就需要内核进行唤醒。

自旋锁:轻量级锁的典型实现。应用程序级别的,加锁的时候发现竞争,一般也不是进入阻塞,而是通过忙等的形式进行等待。(乐观锁的场景,本身遇到锁竞争的概率很小,真的遇到竞争,在短时间内就能拿到锁)

JVM内部会统计每个锁竞争的激烈程度,如果竞争不激烈,此时synchronized就会按照轻量级锁(自旋);如果竞争激烈,此时synchronized就会按照重量级锁(挂起等待)。

1.4 普通互斥锁vs读写锁

synchronized不是读写锁。

多个线程读取一个数据,是本身就线程安全的。多个线程读取,一个线程修改,肯定会涉及到线程安全问题。

读写锁,确保读锁和读锁之间不是互斥的(不会产生阻塞)。写锁和读锁之间产生互斥。写锁和写锁之间产生互斥。

保证线程安全的前提下,降低锁冲突的概率,提高效率。

读写锁适合读多写少的情况。

1.5 可重入锁vs不可重入锁

synchronized是“可重入锁”,一个线程一把锁,连续加锁多次,是否会死锁。出现死锁,就是可重入,否则不可重入。

  1. 锁要记录当前是哪个线程拿到的这把锁
  2. 使用计数器,记录当前加锁了多少次,在合适的时候进行解锁。

1.6 公平锁vs非公平锁

synchronized是非公平锁。

结合上⾯的锁策略, 我们就可以总结出, synchronized 具有以下特性(只考虑 JDK 1.8):
1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较⻓, 就转换成重量级锁.
3. 实现轻量级锁的时候⼤概率⽤到的⾃旋锁策略
4. 是⼀种不公平锁
5. 是⼀种可重⼊锁
6. 不是读写锁

1.7 synchronized的优化:

锁升级:

synchronized是自适应的。自适应的过程,锁升级:无锁=>偏向锁=>自旋锁=>重量级锁。

偏向锁:本质上是懒汉模式。进行synchronized一开始不真加锁,而是简单做个标记,这个标记非常轻量,相对于加锁解锁效率高很多。如果没有其他线程竞争这个锁,最终当前线程执行到解锁代码也就是简单清楚上述标记即可。如果有其他线程来竞争,就抢先一步,在另一个线程拿到锁之前抢先拿到锁,进行加锁,偏向锁就变成轻量级锁,其他线程只能阻塞等待。

当前JVM中,只提供了“锁升级”不能“锁降级”。 

锁消除:也是编译器优化的一种体现。编译器会判定当前这个代码逻辑是否真的需要加锁,如果不需要加锁,但有synchronized,就会自动把synchronized去掉。

锁粗化

加锁和解锁之间包含的代码越多(不是代码行数,而是实际执行的指令/时间),就认为锁的粒度越粗。如果包含的代码越少,就认为锁的粒度越细。

一个代码中,反复针对细粒度的代码加锁,就可能被优化成更粗粒度的加锁。

1.8 相关面试题

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.
在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突.
2. 介绍下读写锁?
读写锁就是把读操作和写操作分别进⾏加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要⽤在 "频繁读, 不频繁写" 的场景中.
3. 什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
相⽐于挂起等待锁, 优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景下⾮常有⽤.
缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.
4. synchronized 是可重⼊锁么?
是可重⼊锁.
可重⼊锁指的就是连续两次加锁不会导致死锁.
实现的⽅式是在锁中记录该锁持有的线程⾝份, 以及⼀个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数⾃增.

2.CAS比较和交换(compare and swap)

        boolean CAS(address,expectValue,swapValue){//(内存地址,寄存器的值,另一个寄存器的值)if(&address==expectValue){&address=swapValue;return true;}return false;}//判定内存中的值和寄存器1的值是否一致,如果一致就把内存中的值和寄存器2进行交换。//但是由于基本上只是关心交换后内存中的值,不关心寄存器2的值,此处也可以把这样的操作理解为“赋值”

CAS是CPU的一条指令。(原子)

CAS本质上是CPU的指令,操作系统就会把这个指令进行封装,提供一些API,就可以在C++中调用,JVM又是基于C++实现的,JVM也能够使用C++调用CAS操作。

CAS最主要的用途:实现原子类。使用原子类的目的就是为了避免加锁。通过CAS能够确保性能,同时保证线程安全。

import java.util.concurrent.atomic.AtomicInteger;public class Demo39 {//使用原子类代替intprivate static AtomicInteger count = new AtomicInteger(0);//原子类基于CAS实现public static void main(String[] args) {Thread t1=new Thread(()->{for(int i=0;i<50000;i++){count.incrementAndGet();}});Thread t2=new Thread(()->{for(int i=0;i<50000;i++){count.incrementAndGet();}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count.get());}
}

原子类,专有名词,atomic这个包里的类,synchronized保证一个修改的原子性,和“原子类”无关。

基于CAS实现自旋锁

    public class SpinLock{private Thread owner=null;//如果为null,锁是空闲的。如果非null,锁已经被某个线程占有了public void lock(){while(!CAS(this.owner,null,Thread.currentThread())){//加锁操作中,需要判定锁是否被人占用//如果未被人占用就把当前线程的引用设置到owner中//如果已经被人占用就等待//这里就开始自旋,发现锁已经被占用,CAS不会执行交换,返回false,进入循环,进入下一次判定//由于循环体是空着的,整个循环速度非常快(忙等)//但是一旦其他线程释放锁,此时该线程就能第一时间拿到这里的锁}}public void unlock(){this.owner=null;//引用赋值操作,本身就是原子指令}}

CAS的典型缺陷:ABA问题

使用CAS能够进行线程安全的编程,核心是先比较“相等”,内存和寄存器是否相等。(这里本质上在判定是否有其他线程插入进来做了一些修改)

认为如果发现这里寄存器和内存的值一致,就可以认为是没有线程穿插过来修改,因此接下来的修改操作就是线程安全的。本来判定内存的值是否是A,发现果然是A,说明没有其他线程修改过。但是实际上,可能存在一种情况,另一个线程把内存从A修改成B,又从B修改为A。

2.1 相关面试题

1. 讲解下你⾃⼰理解的 CAS 机制
全称 Compare and swap, 即 "⽐较并交换". 相当于通过⼀个原⼦的操作, 同时完成 "读取内存, ⽐较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的⽀撑.
1. ABA问题怎么解决?
给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版本号⽐之前读到的版本号⼤, 就认为操作失败

3.JUC中的组件

3.1 Callable接口

和Runnable接口并列关系

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo40 {public static void main(String[] args) throws ExecutionException, InterruptedException {//此处的Callable只是定义了一个“带有返回值”的任务//并没有真正在执行,执行还需要搭配Thread对象Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 10; i++) {sum += i;}return sum;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);//Thread本身不提供获取结果的方法,需要凭FutureTask对象来拿到结果Thread thread = new Thread(futureTask);thread.start();System.out.println(futureTask.get());//get操作就是获取到FutureTask的返回值,这个返回值来自于Callable的call方法}
}

创建线程的写法:

  1. 继承Thread(定义单独的类/匿名内部类)
  2. 实现Runnable(定义单独的类/匿名内部类)
  3. lambda
  4. 实现Callable(定义单独的类/匿名内部类)
  5. 线程池 ThreadFactory

3.2 ReentrantLock可重入

和synchronized是并列关系。

import java.util.concurrent.locks.ReentrantLock;public class Demo42 {private static int count=0;public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 50000; i++) {locker.lock();try{count++;}finally {locker.unlock();}}}});Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 50000; i++) {locker.lock();try{count++;}finally {locker.unlock();}}}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count);}
}

ReentrantLock和synchronized的区别:

  1. synchronized是关键字(内部实现是JVM,内部通过C++实现),ReentrantLock标准库的类(基于JAVA实现)
  2. synchronized通过代码块控制加锁解锁,不需要手动释放锁,ReentrantLock需要lock/unlock方法,需要注意unlock不被调用的问题,需要手动释放锁
  3. ReentrantLock除了lock,unlock之外,还提供了一个方法tryLock(),不会阻塞。加锁成功后返回true,加锁失败返回false.调用者判定返回值决定接下来怎么做。可以设置超时时间,等待时间达到超时时间再返回true/false
  4. ReentrantLock提供了公平锁的实现,默认是非公平的
    ReentrantLock locker = new ReentrantLock(true);//公平锁
  5. ReentrantLock搭配的等待通知机制是Condition类,可以更精确控制唤醒某个指定的线程,相比wait notify来说功能更强大
如何选择使⽤哪个锁?
  1. 锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.
  2. 锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.
  3. 如果需要使⽤公平锁, 使⽤ ReentrantLock

3.3 原子类

原子类内部使用CAS实现,性能要比加锁实现i++高很多,原子类有AtomicInteger,AtomicLong

3.4 线程池

3.5 信号量Semaphore

能够协调多个线程之间的资源分配

信号量表示的是“可用资源的个数”,申请一个资源(P操作),计数器-1,释放一个资源(V操作),计数器+1。计数器为0,继续申请就会阻塞等待。

信号量的一个特殊情况:初始值为1的信号量,取值要么是1要么是0(二元信号量),等价于“锁”,普通的信号量相当于锁的广泛推广。

如果是普通的N个信号量,就可以限制同时有多少个线程来执行某个逻辑。

import java.util.concurrent.Semaphore;public class Demo44 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(1);Thread t1=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 50000; i++) {try{semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});Thread t2=new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 50000; i++) {try{semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

3.6 CountDownLatch

使用多线程,经常把一个大的任务拆分成多个任务,使用多线程执行这些子任务提高程序的效率。

衡量子任务全部完成:

  1. 构造方法指定参数,描述拆分成多少个任务
  2. 每个任务执行完毕之后,都调用一次countDown方法
  3. 主线程中调用await方法,等待所有任务执行完毕。await就会返回/阻塞等待
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Demo45 {public static void main(String[] args) throws InterruptedException {//把整个任务拆成10个部分,每个部分视为一个“子任务”//可以把10个子任务丢到线程池,也可以安排10个独立的线程执行CountDownLatch latch = new CountDownLatch(10);//构造方法中传入的10表示任务的个数,CountDownLatch用于阻塞主线程,直到所有的子任务都执行完毕ExecutorService exec = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) {int id=i;exec.submit(()->{System.out.println("子任务开始执行"+id);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("子任务结束执行"+id);latch.countDown();});}latch.await();//阻塞等待所有的任务结束System.out.println("所有任务执行完毕");exec.shutdown();}
}

3.7 相关⾯试题

1. 线程同步的⽅式有哪些?
synchronized, ReentrantLock, Semaphore 等都可以⽤于线程同步.
2. 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,
synchronized 使⽤时不需要⼿动释放锁. ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活,
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放
弃.
synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启
公平锁模式.
synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.
ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
3. AtomicInteger 的实现原理是什么?
基于 CAS 机制. 伪代码如下:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}
4. 信号量听说过么?之前都⽤在过哪些场景下?
信号量, ⽤来表⽰ "可⽤资源的个数". 本质上就是⼀个计数器.
使⽤信号量可以实现 "共享锁", ⽐如某个资源允许 3 个线程同时使⽤, 那么就可以使⽤ P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进⾏ P 操作就会阻塞等待, 直到前⾯的线程执⾏了 V 操作.

4.线程安全的集合类

4.1 多线程环境使用ArrayList

  1. 自己使用同步机制(synchronized或者ReentrantLock)自行加锁,分析清楚把哪些代码打包到一起成为一个“原子”操作
  2. Collections.synchronizedList(new ArrayList),返回的List的各种关键方法都是带有synchronized,类似于Vector,Hashtable,StringBuffer
  3. 使用CopyOnWriteArrayList,多线程读取,复制过程中如果其他线程在读,就直接读取旧版本的数据,虽然复制过程不是原子的(消耗一段时间),由于提供了旧版本的数据,不影响其他线程读取。新版本数组复制完毕之后,直接进行引用的修改,引用的赋值是“原子”。确保读取过程中,要么读到的是旧版数据,要么读到的是新版数据,不会读到“修改一半”的数据。
    1. 这个过程没有加锁,不会产生阻塞
    2. 有明显的缺点:数组很大,非常低效;如果多个线程同时修改容易出问题

4.2 多线程环境使用队列

  1. ArrayBlockingQueue基于数组实现的阻塞队列
  2. LinkedBlockingQueue基于链表实现的阻塞队列
  3. PriorityBlockingQueue基于堆实现的带优先级的阻塞队列
  4. TransferQueue最多只包含一个元素的阻塞队列

4.3 多线程环境使用哈希表

HashMap本身不是线程安全的。

Hashtable是线程安全的(给各种public 方法都加synchronized),此时任意两个线程,访问任意的两个不同元素都会产生锁竞争。

如果修改的两个元素在不同链表上,本身就不涉及线程安全问题(修改不同变量);

如果修改同一个链表上的两个元素,可能有线程安全问题,比如把这两个元素插入到同一个元素后面,就可能产生竞争。

给每一个链表加上不同的锁(针对不同的锁对象加锁),不会产生锁竞争(不会阻塞)。Java中任意一个对象都可以作为锁对象,在这个逻辑中不需要额外创建对象作为锁,直接使用每个链表的头结点作为synchronized的锁对象即可。

在多线程环境下通常使用ConcurrentHashMap替代Hashtable

ConcurrentHashMap核心优化点:

  1. 按照桶级别进行加锁,而不是给整个哈希加一个全局锁,把锁整个表优化成锁桶(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率
  2. 充分利用CAS特性,使用原子类针对size进行维护,避免出现重量级锁的情况
  3. 优化了扩容方式:化整为零。针对哈希扩容(意味着需要创建更大的数组,把旧哈希表中的所有元素复制到新的哈希中,元素多耗时长),一次复制完所有的元素比较耗时,需要多次的put/get来完成

5. 面试题

1. ConcurrentHashMap的读是否要加锁,为什么?
读操作没有加锁. ⽬的是为了进⼀步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了 volatile
关键字.
2. 介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7 中采取的技术. Java1.8 中已经不再使⽤了. 简单的说就是把若⼲个哈希桶分成⼀个
"段" (Segment), 针对每个段分别加锁.
⽬的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同⼀个段上的时候, 才触发锁竞争.
3. ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁, 直接给每个哈希桶(每个链表)分配了⼀个锁(就是以每个链表的头结点对象作为锁对
象).
将原来 数组 + 链表 的实现⽅式改进成 数组 + 链表 / 红⿊树 的⽅式. 当链表较⻓的时候(⼤于等于 8 个元素)就转换成红⿊树.
4. Hashtable和HashMap、ConcurrentHashMap 之间的区别?
HashMap: 线程不安全. key 允许为 null
Hashtable: 线程安全. 使⽤ synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
ConcurrentHashMap: 线程安全. 使⽤ synchronized 锁每个链表头结点, 锁冲突概率低, 充分利⽤
CAS 机制. 优化了扩容⽅式. key 不允许为 null.
5. 谈谈 volatile关键字的⽤法?
volatile 能够保证内存可⻅性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.
6. Java多线程是如何实现数据共享的?
JVM 把内存分成了这⼏个区域:
⽅法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.
7. Java创建线程池的接⼝是什么?参数 LinkedBlockingQueue 的作⽤是什么?
创建线程池主要有两种⽅式:
1.通过 Executors ⼯⼚类创建. 创建⽅式⽐较简单, 但是定制能⼒有限.
2.通过 ThreadPoolExecutor 创建. 创建⽅式⽐较复杂, 但是定制能⼒强.
LinkedBlockingQueue 表⽰线程池的任务队列. ⽤⼾通过 submit / execute 向这个任务队列中
添加任务, 再由线程池中的⼯作线程来执⾏任务.
8. Java线程共有⼏种状态?状态之间怎么切换的?
NEW: 安排了⼯作, 还未开始⾏动. 新创建的线程, 还没有调⽤ start ⽅法时处在这个状态.
RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作. 调⽤ start ⽅法之后, 并正在 CPU 上运⾏/在即将准备运⾏ 的状态.
BLOCKED: 使⽤ synchronized 的时候, 如果锁被其他线程占⽤, 就会阻塞等待, 从⽽进⼊该状态.
WAITING: 调⽤ wait ⽅法会进⼊该状态.
TIMED_WAITING: 调⽤ sleep ⽅法或者 wait(超时时间) 会进⼊该状态.
TERMINATED: ⼯作完成了. 当线程 run ⽅法执⾏完毕后, 会处于这个状态.
9. 在多线程下,如果对⼀个数进⾏叠加,该怎么做?
使⽤ synchronized / ReentrantLock 加锁
使⽤ AtomInteger 原⼦操作.
10. Servlet是否是线程安全的?
Servlet 本⾝是⼯作在多线程环境下.
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进⾏操
作, 是可能出现线程不安全的情况的.
11. Thread和Runnable的区别和联系?
Thread 类描述了⼀个线程.
Runnable 描述了⼀个任务.
在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run ⽅法, 也可以使⽤Runnable 来描述这个任务.
12. 多次start⼀个线程会怎么样
第⼀次调⽤ start 可以成功调⽤.
后续再调⽤ start 会抛出 java.lang.IllegalThreadStateException 异常
13. 有synchronized两个⽅法,两个线程分别同时⽤这个⽅法,请问会发⽣什么?
synchronized 加在⾮静态⽅法上, 相当于针对当前对象加锁.
如果这两个⽅法属于同⼀个实例: 线程1 能够获取到锁, 并执⾏⽅法. 线程2 会阻塞等待, 直到线程1 执⾏完毕, 释放锁, 线程2 获取到锁之后才能执⾏⽅法内容.
如果这两个⽅法属于不同实例:两者能并发执⾏, 互不⼲扰.
14. 进程和线程的区别?
进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
http://www.dtcms.com/a/322050.html

相关文章:

  • linux 一次性查看所有docker容器网络模式和端口映射
  • 打破枷锁:Python GIL下的并发突围之路
  • 两个函数 quantize() 和 dequantize() 可用于对不同的位数进行量化实验
  • 睿抗开发者大赛国赛-24
  • 【深度学习】动手深度学习PyTorch版——安装书本附带的环境和代码(Windows11)
  • 【实证分析】地区市场公平竞争程度数据集-含do代码(2012-2024年)
  • JAVA接口请求测试及调用
  • 直播美颜SDK快速上手指南:从API调用到美白滤镜效果调优
  • Godot ------ 制作属于自己的卡牌
  • 从伪造的验证码到远程攻击工具 (RAT):2025 年网络欺骗威胁趋势
  • 同一局域网下,vmwear为啥xshell连不上,ssh也安装了
  • 加密流量论文复现:《Detecting DNS over HTTPS based data exfiltration》(下)
  • 【2025】AutoDock最新保姆级安装教程(附安装包+永久使用方法)
  • 项目历程—画图板
  • C语言学习笔记——编译和链接
  • Vue 自定义水印指令实现方案解析
  • ClickHouse集群部署实践---3分片2副本集群
  • 主成分分析加强版:MP-PCA
  • fio文件读写io带宽测试工具
  • 从零构建TransformerP2-新闻分类Demo
  • Spring AI 系列之三十九 - Spring AI Alibaba-集成百炼知识库
  • 【Python-Day 38】告别通用错误!一文学会创建和使用 Python 自定义异常
  • 【Nginx基础①】 | VS Code Remote SSH 环境下的静态资源与反向代理配置实践
  • 明厨亮灶场景下误检率↓76%:陌讯多模态融合算法实战解析
  • 蓝桥杯----大模板
  • 【NFTurbo】基于DockerCompose一键部署
  • Redis中String数据结构为什么以长度44为embstr和raw实现的分界线?
  • 【大模型实战篇】部署GPT-OSS-120B踩得坑(vllm / ollama等推理框架)
  • 数据库索引创建的核心原则与最佳实践
  • JAVA 分布式锁的5种实现方式