Java面试题-并发编程
1.线程和进程的区别?
- 进程:
进程是程序中正在运行的一个程序,系统是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。 - 线程:
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程。
2.并行和并发有什么区别?
- 并行:多个处理器或多核处理器同时处理多个任务。
- 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
3.守护线程是什么?
守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
4.创建线程有哪几种方式?
创建线程有三种方式:
- 继承 Thread 重写 run 方法;
- 实现 Runnable 接口;
- 实现 Callable 接口。
5.说一下 runnable 和 callable 有什么区别?
runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
6.线程有哪些状态?
- NEW 尚未启动
- RUNNABLE 正在执行中
- BLOCKED 阻塞的(被同步锁或者IO锁阻塞)
- WAITING 永久等待状态
- TIMED_WAITING 等待指定的时间重新被唤醒的状态
- TERMINATED 执行完成
7.sleep() 和 wait() 有什么区别?
- 类的不同:sleep() 来自 Thread,wait() 来自 Object;
- 释放锁:sleep() 不释放锁;wait() 释放锁;
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒;
- 使用场景:sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于线程间的通信;
8.Thread.yield 方法有什么用?
yield 方法会让掉当前线程 CPU 的时间片,使正在运行中的线程重新编程就绪状态,并重新竞争 CPU 的调度权。
9.yield 和 sleep 有什么区别?
- yield,sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CUP 的时间片划分;
- yield,sleep 两个在暂停过程中,如果已经持有锁,则都不会释放锁资源;
- yield 不能被中断,而 sleep 可以接受中断;
10.notify()和 notifyAll()有什么区别?
notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
11.线程的 run() 和 start() 有什么区别?
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
12.创建线程池有哪几种方式?使用线程池有什么好处?
线程池创建有七种方式,最核心的是最后一种:
-
newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
-
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
-
newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
-
newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
-
newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
-
newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
-
ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
13.线程池的核心参数?执行过程?
-
corePoolSize:核心线程数
核心线程会一直存活,及时没有任务需要执行。
当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。 -
queueCapacity:任务队列容量(阻塞队列)
当核心线程数达到最大时,新任务会放在队列中排队等待执行。 -
maxPoolSize:最大线程数
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。 -
keepAliveTime:线程空闲时间
当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。
如果allowCoreThreadTimeout=true,则会直到线程数量=0。 -
allowCoreThreadTimeout:允许核心线程超时
-
rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:(1)当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务。(2)当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。 -
执行过程:
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满。(1)若线程数小于最大线程数,创建线程。(2)若线程数等于最大线程数,抛出异常,拒绝任务。
14.线程池都有哪些状态?
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 * TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
15.线程池中 submit() 和 execute() 方法有什么区别?
- execute():只能执行 Runnable 类型的任务。
- submit():可以执行 Runnable 和 Callable 类型的任务。
16.为什么阿里不让用 Executors 创建线程池?
-
newSingleThreadExecutor 和 newFixedThreadPool 在 workQueue 参数直接使用了new LinkedBlockingQueue
() 无界队列理论上可以无限添加任务到线程池。如果提交到线程池的任务有问题,比如 sleep 永久,会造成内存泄漏,最终导致 OOM。 -
CachedThreadPool 和 ScheduledThreadPool允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
17.线程池的拒绝策略有哪几种?
-
CallerRunsPolicy :当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
-
AbortPolicy:丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
-
DiscardPolicy:直接丢弃,其他啥都没有
-
DiscardOldestPolicy:当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
18.在 Java 程序中怎么保证多线程的运行安全?
- 方法一:使用安全类,比如 Java. util. concurrent 下的类。
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。
手动锁 Java 示例代码如下:
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
19.多线程中 synchronized 锁升级的原理是什么?
-
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
-
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
20.什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
21.怎么防止死锁?
- 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
- 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
- 尽量减少同步的代码块。
22.ThreadLocal 是什么?有哪些使用场景?
-
含义:ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
-
使用场景:ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
-
副作用:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 的 value 就会导致内存泄漏,而不是因为弱引用。所以每次使用完ThreadLocal,都调用它的 remove() 方法,清除数据。
23.说一下 Synchronized 底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
24.Synchronized 和 volatile 的区别是什么?
- volatile 是变量修饰符;Synchronized 是修饰类、方法、代码段。
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 Synchronized 则可以保证变量的修改可见性和原子性。
- volatile 不会造成线程的阻塞;Synchronized 可能会造成线程的阻塞。
25.Synchronized 和 Lock 有什么区别?
- Synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- Synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 Synchronized 却无法办到。
26.synchronized 和 ReentrantLock 区别是什么?
Synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 Synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 Synchronized 可用于修饰方法、代码块等。
27.说一下 Atomic 的原理?
Atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免 Synchronized 的高开销,执行效率大为提升。
28.join 方法有什么用?什么原理?
- 作用:Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行;
- 原理:join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。
29.什么是 CAS?
- CAS,全称Compare And Swap(比较与交换),解决多线程并行情况下使用锁造成性能损耗的一种机制。
- CAS(V, A, B),V为内存地址、A为预期原值,B为新值。如果内存地址的值与预期原值相匹配,那么将该位置值更新为新值。否则,说明已经被其他线程更新,处理器不做任何操作;无论哪种情况,它都会在 CAS 指令之前返回该位置的值。而我们可以使用自旋锁,循环CAS,重新读取该变量再尝试再次修改该变量,也可以放弃操作。
30.为什么不推荐 stop 停止线程?
-
stop方法是过时的:从Java编码规则来说,已经过时的方式不建议采用.
-
stop方法会导致代码逻辑不完整:stop方法是一种"恶意" 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的.
-
stop方法会破坏原子逻辑:多线程为了解决共享资源抢占的问题,使用了锁的概念,避免资源不同步,但是正是因为此原因,stop方法却会带来更大的麻烦,它会丢弃所有的锁,导致原子逻辑受损.
31.如何优雅的终止一个线程?
使用 volatile 修饰的 flag 变量来控制线程中的逻辑,来达到停止线程的效果同样可以。
public class ThreadInterruptVolatileDemo {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptTask());
thread.start();
thread.sleep(500);
flag = true;
}
static class InterruptTask implements Runnable {
@Override
public void run() {
int count = 0;
while (!flag) {
count++;
}
System.out.println("循环次数:" + count + ",线程中断");
}
}
}
32.什么是重入锁(ReentrantLock)?
就像下面这样:一个类中,同步方法之间的调用就需要重复获取this锁
public class Demo1 {
public synchronized void functionA(){
System.out.println("iAmFunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("iAmFunctionB");
}
}
- 含义:当某个线程获取到锁时,该线程还能继续获取该锁,也就是说线程可以重复获取同一把锁;
- 意义:在某个线程需要重复获取同一把锁的情况下,不会导致死锁的发生;
- 常见的重入锁:Synchronized,ReentrantLock;
- 实现原理:通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁;
33.什么是读写锁?
- 读写锁允许同一时刻多个读线程访问,但是写线程和其他线程均被阻塞。
- 读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。
- Java 中提供读写锁的实现类是:ReentrantReadWriteLock。
34.公平锁和非公平锁的区别?
- 公平锁 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程;
- 非公平锁 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待;
- 公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多,java中的synchronized时非公平锁,ReentranLock默认的lock方法采用的时非公平锁;
35.有哪些锁优化的方式?
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- 锁消除
36.什么是偏向锁?
- 含义:偏向锁是一种针对加锁操作的优化手段。如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。
- 效率:对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次极有可能是同一个线程请求相同的锁。而对于锁竞争比较激烈的场合,其效果不佳。因为在竞争激烈的场合,最有可能的情况是每次都是不同的线程来请求相同的锁。
- JDK1.6为什么引入偏向锁:经过 HotSpot 的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
37.什么是轻量级锁?
- 含义:如果偏向锁失败,那么虚拟机并不会立即挂起线程,它还会使用一种称为轻量级锁的优化手段。
- 实现:轻量级锁的操作也很方便,它只是简单地将对象头部作为指针指向持有锁的线程堆栈的头部,来判断一个线程是否持有对象锁。
- 过程:如果线程获得轻量级锁成功,则可以顺利进入临界区,如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。
38.什么是自旋锁?
- 自旋锁:锁膨胀后,为了避免线程真实地在操作系统层面挂起,而是升级为自旋锁。
- 当前线程暂时获取不到锁,但是如果简单粗暴地将这个线程挂起是一种得不偿失的操作,因此虚拟机会让当前线程做几个空循环,在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。
39.什么是锁消除?
- 含义:锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
- 原理:锁消除是由于逃逸分析带来的优化,它消除了多余的同步。当内部同步代码没有逃逸到外部时,runtime就可以完全消除同步了。
40.volatile 有什么用?有哪些应用场景?
- 作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
- 应用场景:状态的转变、读多写少的情况
41.Java 中原子操作的类有哪些?
- AtomicBoolean:原子更新布尔类型。
- AtomicInteger:原子更新整型。
- AtomicLong: 原子更新长整型。
- AtomicIntegerArray:原子更新整形数组。
- AtomicLongArray:原子更新长整型数组。
- AtomicReferenceArray:原子更新引用类型数组里的元素。
42.什么是 ABA 问题,怎么解决?
- 什么是 ABA 问题:
因为 CAS 需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检测时会发现值没有发生变更,其实是变过的。 - 解决方案:
添加版本号,每次更新的时候追加版本号,A-B-A——> 1A - 2B - 3A
从 JDK 1.5 开始,Atomic 包提供了一个 AtomicStampedReference 类来解决 ABA 的问题。
43.什么是阻塞队列?有哪些应用场景?
- 什么是阻塞队列:
阻塞队列是一个支持阻塞的插入和移除方法的队列。即当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列空时,获取元素的线程会等待队列变为非空。 - 应用场景:
常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。 - 原理:采用通知模式。这有点类似于操作系统中学过的信号量。当生产者着往满的队列中添加元素会阻塞住生产者,直到消费者消费了一个元素后,通知生产者队列可用。ArrayBlockingQueue使用了Condition实现。
44.Java 中的阻塞队列有哪些?
- ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】
- LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。
- PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
- DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)
- SynchronousQueue: 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
45.什么是幂等性?
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)