什么是守护线程,和普通线程有什么区别?

典型回答 在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。用户线程一般用于执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务,守护线程最典型的应用就是GC(垃圾回收器)。 这两种线程其实是没有什么区别的,唯一的区别就是Java虚拟机在所有<用户线程>都结束后就会退出,而不会等<守护线程>执行完。 扩展知识 创建守护线程 我们可以通过使用setDaemon()方法通过传递true作为参数,使线程成为一个守护线程。我们必须在启动线程之前调用一个线程的setDaemon()方法。否则,就会抛出一个java.lang.IllegalThreadStateException。 可以使用isDaemon()方法来检查线程是否是守护线程。 1 2 3 4 5 6 7 8 9 10 11 12 13 /** * @author Hollis */ public class Main { public static void main(String[] args) { Thread t1 = new Thread(); System.out.println(t1.isDaemon()); t1.setDaemon(true); System.out.println(t1.isDaemon()); t1.start(); t1.setDaemon(false); } } 以上代码输出结果: 1 2 3 4 5 false true Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1359) at com.hollis.Main.main(Main.java:16) 我们提到,当JVM中只剩下守护线程的时候,JVM就会退出,那么写一段代码测试下: ...

March 22, 2026 · 2 min · santu

run_start、wait_sleep、notify_notifyAll区别_

run方法和start方法区别 我们创建好线程之后,想要启动这个线程,则需要调用其start方法。所以,start方法是启动一个线程的入口。 如果在创建好线程之后,直接调用其run方法,那么就会在单线程中直接运行run方法,不会起到多线程的效果。 sleep和wait区别 sleep()方法可以在任何地方使用;而wait()方法则只能在同步方法或同步块中使用。 wait 方法会释放对象锁,但 sleep 方法不会。 wait的线程会进入到WAITING状态,直到被唤醒;sleep的线程会进入到TIMED_WAITING状态,等到指定时间之后会再尝试获取CPU时间片。 因为Java锁的目标是对象,而wait需要释放锁,所以针对的目标都是对象,所以把他定义在Object类中。 而sleep()不需要释放锁,所以他针对的目标是线程,所以定义在Thread类中。 notify和notifyAll区别 当一个线程进入wait之后,就必须等其他线程notify()/notifyAll(),才会从等待队列中被移出。 使用notifyAll,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个。 但是唤醒的这些线程只是进入争夺队列,并不表示立即就可以获得CPU开始执行,因为wait方法被调用的时候,线程已经释放了对象锁。 所以,被notify()/notifyAll()唤醒的线程,只是表示他们可以竞争锁了,竞争到锁之后才有机会被CPU调度。 那么notifyAll虽然可以把所有线程都唤醒,让他们都可以竞争锁,但是最终也只有一个可以获得锁并执行。 notify和notifyAll因为也是操作对象的,所以把他们定义在Object类中。

March 22, 2026 · 1 min · santu

创建线程有几种方式?

典型回答 在Java中,共有四种方式可以创建线程,分别是 继承Thread类创建线程 实现Runnable接口创建线程 通过Callable和FutureTask创建线程 通过线程池创建线程 其实,归根结底最终就两种,一个是继承Thread类,一个是实现Runnable接口,至于其他的。也是基于这两个方式实现的。但是有的时候面试官更关注的是实际写代码过程中,有几种方式可以实现。所以一般回答4种也没啥毛病。 扩展知识 Runnable和Callable区别 Runnable接口和Callable接口都可以用来创建新线程,实现Runnable的时候,需要实现run方法;实现Callable接口的话,需要实现call方法。 Runnable的run方法无返回值,Callable的call方法有返回值,类型为Object Callable中可以够抛出checked exception,而Runnable不可以。 Callable和Runnable都可以应用于executors。而Thread类只支持Runnable。 Future Future是一个接口,代表了一个异步执行的结果。接口中的方法用来检查执行是否完成、等待完成和得到执行的结果。当执行完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。如果想取消,那么调用cancel()方法。 FutureTask是Future接口的一个实现,它实现了一个可以提交给Executor执行的任务,并且可以用来检查任务的执行状态和获取任务的执行结果。 FutureTask和Callable示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class FutureAndCallableExample { public static void main(String[] args) throws InterruptedException, ExecutionException { Callable<String> callable = () -> { System.out.println("Entered Callable"); Thread.sleep(2000); return "Hello from Callable"; }; FutureTask<String> futureTask = new FutureTask<>(callable); Thread thread = new Thread(futureTask); thread.start(); System.out.println("Do something else while callable is getting executed"); System.out.println("Retrieved: " + futureTask.get()); } } 线程池 ✅什么是线程池,如何实现的? ...

March 22, 2026 · 1 min · santu

线程同步的方式有哪些?

典型回答 线程同步指的就是让多个线程之间按照顺序访问同一个共享资源,避免因为并发冲突导致的问题,主要有以下几种方式: synchronized:Java中最基本的线程同步机制,可以修饰代码块或方法,保证同一时间只有一个线程访问该代码块或方法,其他线程需要等待锁的释放。 1 2 3 public synchronized void synchronizedMethod() { // 同步的代码块 } ReentrantLock:与synchronized关键字类似,也可以保证同一时间只有一个线程访问共享资源,但是更灵活,支持公平锁、可中断锁、多个条件变量等功能。 1 2 3 4 5 6 7 8 9 10 11 12 import java.util.concurrent.locks.ReentrantLock; private final ReentrantLock lock = new ReentrantLock(); public void someMethod() { lock.lock(); try { // 同步的代码块 } finally { lock.unlock(); } } Semaphore:允许多个线程同时访问共享资源,但是限制访问的线程数量。可以用于控制并发访问的线程数量,避免系统资源被过度占用。 ...

March 22, 2026 · 1 min · santu

什么是死锁,如何解决?

典型回答 死锁是指两个或两个以上的进程(或线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 比如:丈母娘要求先买房才能结婚,但是女婿说先结婚买房 扩展知识 产生死锁的四个必要条件 **(1) 互斥条件:**一个资源每次只能被一个进程使用。 **(2) 占有且等待:**一个进程因请求资源而阻塞时,对已获得的资源保持不放。 **(3)不可强行占有:**进程已获得的资源,在未使用完之前,不能强行剥夺。 **(4) 循环等待条件:**若干进程之间形成一种头尾相接的循环等待资源关系。 如何解除死锁 想好解除和预防死锁,就避免4个条件同时发生就行了,一般从以下几个方面入手 破坏不可抢占:设置优先级,使优先级高的可以抢占资源 破坏循环等待:保证多个进程(线程)的执行顺序相同即可避免循环等待。 如执行顺序都是:A->B->C,那就可以避免循环等待。 最常用的避免方法就是破坏循环等待,就是当我们有多个事务的时候,最好让这几个事务的执行顺序相同。 如事务1:A->B->C ,事务2:C->D->A,这种情况就有可能导致死锁。 即事务1占有了A,等待C,而事务2占有了C在等待A。 所以,要避免死锁就把事务2改为:A -> D-> C。 数据库死锁的发生 在数据库中,如果有多个事务并发执行,也是可能发生死锁的。当事务1持有资源A的锁,但是尝试获取资源B的锁,而事务2持有资源B的锁,尝试获取资源A的锁的时候,这时候就会发生死锁的情况。 发生死锁时,会发生如下异常: 1 2 3 Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] Deadlock found when trying to get lock; 一般对于数据库的死锁,主要是避免发生并发修改。或者可以考虑保证操作的顺序,比如多个事务都是先操作资源A、再操作资源B,这样就能有效的避免死锁。 ✅死锁问题排查过程

March 22, 2026 · 1 min · santu

synchronized锁的是什么?

典型回答 ✅synchronized是怎么实现的? 上面的题目中介绍过synchronized的实现机制,但是很多人还是不明白synchronized到底锁的是什么。因为synchronized在使用的时候,可以锁定对象,也可以锁定当前类,所以很多人会产生误会: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //同步方法,对象锁 public synchronized void doSth(){ System.out.println("Hello World"); } //同步方法,类锁 public synchronized static void doSth(){ System.out.println("Hello World"); } //同步代码块,类锁 public void doSth1(){ synchronized (Demo.class){ System.out.println("Hello World"); } } //同步代码块,对象锁 public void doSth1(){ synchronized (this){ System.out.println("Hello World"); } } 但是,不管是上面哪种用法,** synchronized最终锁的都是对象!**Java中一切皆对象,类最终也是通过对象的方式呈现的。所以,synchronized的不同用法,只不过锁的对象不同而已。 ...

March 22, 2026 · 4 min · santu

并发编程中的原子性和数据库ACID的原子性一样吗?

典型回答 不一样。原子性在并发编程中,和在数据库中两种不同的概念。 在数据库中,事务的ACID中原子性指的是"要么都执行要么都回滚"。 在并发编程中,原子性指的是"操作不可拆分、不被中断"。所以在并发编程中,我们要保证原子性指的就是一段代码需要不可拆分,不被中断。 比如经典的i++问题,就是原子性的一个很好地体现。 如Redis,它既是一个数据库系统,也是一个有并发的系统,那么看一下他对原子性的支持,就理解了: ✅为什么Lua脚本可以保证原子性?

March 22, 2026 · 1 min · santu

synchronized是如何保证原子性、可见性、有序性的?

synchronized如何保证原子性 原子性是指一个操作是不可中断的。 ✅并发编程中的原子性和数据库ACID的原子性一样吗? 我们知道,线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。 比如线程1获得时间片执行,但是执行过程中,CPU时间片耗尽,他就需要让出CPU,这时线程2获得了时间片开始执行。但是对于线程1来说,他的操作并没有全部执行完成,也没有全都不执行,这就是原子性问题。 那么,synchronized如何保证的原子性呢? synchonized其实是通过 monitorenter 和 monitorexit 这两个字节码指令实现的。 当线程执行到 monitorenter 的时候要先获得锁,才能执行后面的方法。当线程执行到 monitorexit 的时候则要释放锁。 在未释放之前,其他线程是无法再次获得锁的,所以,通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。 其他线程即使拿到到了CPU时间片,也没办法处理,下一个时间片还是只能被他(加锁线程)自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。 ✅什么是时间片 这里的时间片是大家一起抢的,并不是因为这个线程加锁了他就一定能抢到,而是因为别的线程抢到没关系,因为别人没有锁,所以这部分代码也无法被执行,最终还是会被持有锁的线程拿到时间片然后继续执行,这也是sync能保证原子性的原因。 synchronized如何保证有序性 有序性即程序执行的顺序按照代码的先后顺序执行。 我们知道,计算机硬件层面做了很多优化,除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。 那么,想要彻底解决有序性问题,最好的办法就是直接禁止指令重排和处理器优化,但是,synchronized是做不到的,那么为什么我们还说synchronized也提供了有序性保证呢? 这里就需要把有序性的概念扩展一下了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。 以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。 as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。 这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。 所以呢,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。 synchronized如何保证可见性 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。 被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。 所以,synchronized关键字锁住的对象,其值是具有可见性的。

March 22, 2026 · 1 min · santu

synchronized的重量级锁很慢,为什么还需要重量级锁?

典型回答 ✅synchronized的锁升级过程是怎样的? 这里提到过synchronized的锁升级过程,其中介绍了偏向锁、轻量级锁以及重量级锁,同时我们也提到过,重量级锁是一种效率比较低的锁,所以很多人说他很慢,所以他叫"重量级锁"。但是既然慢,为啥还要用重量级锁呢? 其实,这个问题有点搞错了synchronized的偏向锁、轻量级锁,和重量级锁之间的目的和原理。synchronized本身是一个重量级锁,然后为了让他有些并发不高的情况下不要这么重才引入的偏向锁和轻量级锁。 为什么本身是一个重量级锁?先说说啥是所谓的重量级锁。 重量级锁是指在多线程竞争激烈的情况下,synchronized锁膨胀为一种系统级别的锁机制。与偏向锁和轻量级锁不同,重量级锁会阻塞线程,并会在加锁和解锁过程中频繁地与操作系统交互。 确实,**重量级锁相比轻量级锁和偏向锁更慢,原因是重量级锁会引入线程的阻塞与唤醒。**阻塞和唤醒一个线程需要CPU从用户态切换到内核态,这涉及操作系统的调度,开销较大。 也就是说,当多个线程竞争同一锁时,重量级锁会导致其他线程阻塞,所以他慢,但是其实这种阻塞是有必要的。因为并发抢锁的时候,只有阻塞才能保证了线程对共享资源的安全访问,能够避免数据竞争和不一致性问题。 而且,在非常激烈竞争的场景下,轻量级锁的自旋会导致CPU资源浪费,重量级锁通过阻塞来避免这些资源浪费。对于任务较重的操作,重量级锁能提高系统稳定性,避免性能因频繁自旋而下降。 相信很多人看到这还是迷迷糊糊地,举个例子 你去食堂买煎饼果子,就只有一个窗口在卖,你去了之后,发现有人在排队了,那你是不是得排队? 这不就是重量级锁么,就是大家都想买煎饼果子,但是窗口只有一个,那为了解决这种并发冲突,就只有排队了。 那么,如果你去的时候,你发现前面没人,那你就不用排队了,就直接买就行了,这不就是针对重量级锁的所谓优化么。 也就是说,在并发比较高的时候,或者有激烈的所争抢的时候,必须要用重量级锁才行。(这里谈的是Java原生的关键字中,不考虑reentrantlock这种Java后来出来的类)

March 22, 2026 · 1 min · santu

volatile能保证原子性吗?为什么?

典型回答 volatile通常被比喻成”轻量级的synchronized“,也是Java并发编程中比较重要的一个关键字。和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。 volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。 但是, volatile在线程安全方面,可以保证有序性和可见性,但是是不能保证原子性的 。 synchronized可以保证原子性 ,因为被synchronized修饰的代码片段,在进入之前加了锁,只要他没执行完,其他线程是无法获得锁执行这段代码片段的,就可以保证他内部的代码可以全部被执行。进而保证原子性。 那么,为什么volatile不能保证原子性呢?因为他不是锁,他没做任何可以保证原子性的处理。当然就不能保证原子性了。 比如有如下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Test { volatile int number = 0; public void increase() { number++; } public static void main(String[] args) { Test volatileAtomDemo = new Test(); for (int j = 0; j < 10; j++) { new Thread(() -> { for (int i = 0; i < 1000; i++) { volatileAtomDemo.increase(); } }, String.valueOf(j)).start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + " final number result = " + volatileAtomDemo.number); } } Thread.yield()方法被用来暂停当前正在执行的线程,以允许其他线程获得执行机会,以此来提高并发竞争。 ...

March 22, 2026 · 1 min · santu

留言给博主