Java后端高频面经——并发编程

一、多线程

  1. 进程和线程的区别?(美团、腾讯、字节、百度)

    进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

    线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。CPU时间片是直接分配给线程的。

    上图(Java运行时数据区域)可以看出:一个进程中可以有多个线程,多个线程共享进程的 和**方法区 (JDK1.8 之后的元空间)*资源,但是每个线程有自己的*程序计数器虚拟机栈本地方法栈 。总的来说: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

  2. 创建线程的方法?描述一下线程的生命周期,以及之间的状态转化过程;阻塞态到运行态是如何进行转化的?(美团)

    1.创建线程的方法

    一般来说,创建线程有很多种方式,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。

    java 复制代码
    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }

    newRunnable对象,接着再new一个Thread对象,然后把Runnable丢给Thread,接着调用start()方法,此时才能真正意义上创建一条线程。

    2.线程的生命周期和状态?

    Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

    • NEW: 初始状态,线程被创建出来但没有被调用 start()
    • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
    • BLOCKED:阻塞状态,需要等待锁释放。
    • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
    • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
    • TERMINATED:终止状态,表示该线程已经运行完毕。

    线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

    • 线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
    • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
    • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
    • 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。
    • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

    3.阻塞态到运行态是如何进行转化的?

  3. 进程间通信机制?(美团)

  4. 谈谈多线程,缓存行是什么?伪共享是什么?Netty,BIO,NIO的原理与区别?(B站)

  5. ThreadLocal存在的意义,底层是怎么实现的?实现了map接口吗?(滴滴)ThreadLocal 内存泄露问题是怎么导致的?

    1.ThreadLocal存在的意义

    • ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

    • 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

    2.底层实现/原理

    • Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

    • 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

      每个Thread中都具备一个ThreadLocalMapThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

      java 复制代码
      ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
          //......
      }

    3.ThreadLocal内存泄漏

    • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

      这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法。

  6. 有个场景,需要异步去执行操作,如何将threadLocal中的用户信息同步到异步的线程中去?(得物)一个线程里面,再开一个线程池,怎么把threadlocalmap的数据传给里面的线程?(京东)

    • TransmittableThreadLocal 是Alibaba开源的,用于在不同线程之间传递数据,尤其是在使用线程池时。与标准的 ThreadLocal 不同,TransmittableThreadLocal 可以在父线程和子线程之间共享数据。
    java 复制代码
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import com.alibaba.ttl.TransmittableThreadLocal;
    
    public class TransmittableThreadLocalExample {
        // 定义一个 TransmittableThreadLocal 变量
        private static TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
    
        public static void main(String[] args) {
            // 在主线程中设置数据
            transmittableThreadLocal.set("Main Thread Value");
            // 创建线程池
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            // 提交任务到线程池
            executorService.submit(() -> {
                // 在线程中获取并打印 TransmittableThreadLocal 的值
                System.out.println("Thread 1: " + transmittableThreadLocal.get());
            });
            executorService.submit(() -> {
                // 在线程中获取并打印 TransmittableThreadLocal 的值
                System.out.println("Thread 2: " + transmittableThreadLocal.get());
            });
            // 关闭线程池
            executorService.shutdown();
        }
    }
  7. 对线程池的理解?(快手、众安)对线程池各个参数的理解?(B站、滴滴、众安、哈啰)

    1.对线程池的理解(什么是线程池?为什么要用线程池)

    • 线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
    • 线程池 提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池 还维护一些基本统计信息,例如已完成任务的数量。
      • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
      • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
      • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    2.线程池的参数?

    • 创建线程池有两种方式,一种是通过ThreadPoolExecutor构造函数来创建(推荐) ,一种是通过 Executor 框架的工具类 Executors 来创建(不推荐) 。后者Executors 返回线程池对象会堆积大量请求或者创建大量线程从而导致OOM(Out of memory)。而通过 ThreadPoolExecutor 构造函数的方式,让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

    • ThreadPoolExecutor 3 个最重要的参数:

      • corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
      • maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
      • workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

      ThreadPoolExecutor其他常见参数 :

      • keepAliveTime:当线程池中的线程数量大于 corePoolSize ,即有非核心线程(线程池中核心线程以外的线程)时,这些非核心线程空闲后不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
      • unit : keepAliveTime 参数的时间单位。
      • threadFactory :executor 创建新线程的时候会用到。
      • handler :拒绝策略。


      线程池各个参数的关系

  8. 线程池参数如何衡量,如何设置,如何知道你设置的线程数多了还是少了?(B站)

  9. 对于数据量特别大的情况下,你如何保证你的线程池扛得住?你如何对线程池的参数达到一定阈值?如何进行监控?系统如何搭建?线程池里面你如何保证线程安全问题?(美团)

  10. 拒绝策略有哪些?如果任务很多被拒绝之后想记录异常,如何实现?(滴滴)

    1.拒绝策略有哪些?

    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolExecutor 定义一些策略:

    • ThreadPoolExecutor.AbortPolicy(默认):抛出 RejectedExecutionException来拒绝新任务的处理。
    • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
    • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
    • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

    2.如果任务很多被拒绝之后想记录异常,如何实现?

    • 如果内置的拒绝策略不满足需求,可以自定义拒绝策略。自定义拒绝策略需要实现RejectedExecutionHandler接口,并重写rejectedExecution方法。在这个方法中,要记录异常,可以在自定义的拒绝策略中捕获并记录RejectedExecutionException
  11. 线程切换时切换哪些上下文信息?(寄存器、线程的堆栈指针)还有吗?线程的栈空间的具体大小?为什么要给进程分配栈空间?进程里只有一个线程,线程的栈大小就是进程的栈大小吗?进程的默认栈大小?栈大小可以修改吗?(腾讯)

  12. 线程跟协程的区别?为什么协程切换开销小?协程切换是怎么实现的?(区别线程的抢占式调度算法,协程使用协同调度算法)不是说算法,底层过程知道吗?(腾讯)

二、并发安全,推荐文章https://tech.meituan.com/2018/11/15/java-lock.html

  1. 有哪些常用的锁?(小米)怎么在实践中用锁?

    (1)有哪些常用的锁?

    Java中的锁是用于管理多线程并发访问共享资源的关键机制,可以确保在任意给定时间内只有一个线程可以访问特定的资源。

    • 内置锁(synchronized ):synchronized关键字是内置锁机制的基础,当一个线程进入synchronized代码块或方法时,会获取关联对象的锁;当线程离开改代码块或者方法时,释放锁。在这期间,如果其他锁尝试获取同一个对象的锁,他们将被阻塞,直至锁被释放。
    • ReentranLock:java.util.concurrent.locks.ReentrantLock是一个显式的锁类,提供了比synchronized更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock使用lock()unlock()方法来获取和释放锁。
    • 读写锁(ReadWriteLock):java.util.concurrent.locks.ReadWriteLock接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。
    • 乐观锁和悲观锁:读写锁通常用于读取远多于写入的情况,以提高并发性。synchronizedReentrantLock都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用CAS、版本号或时间戳来实现。
    • 自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。

    (2)怎么在实践中用锁?

    • `synchronized'

      java 复制代码
      public class Counter {
      	private int count = 0;
          public synchronized void increment() {
              count++;
          }
      }
    • 使用Lock接口

      Lock接口提供了比synchronized更灵活的锁操作,包括尝试锁、可中断锁、定时锁等。ReentrantLockLock接口的一个实现。

      java 复制代码
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      public class Counter {
          private Lock lock = new ReentranLock();
          private int count = 0;
          
          public void increment() {
              lock.lock();
              try {
                  count++;
              } finally {
                  lock.unlock();
              }
          }
      }
    • 使用ReadWriteLock

      ReadWriteLock接口提供了一种读写锁的实现,允许多个读操作同时进行,但写操作是独占的。

      java 复制代码
      public class Cache {
      	private ReadWriteLock lock = new ReentrantReadWriteLock();
          private Lock readLock = lock.readLock();
          private Lock writeLock = lock.writeLock();
          private Object data;
          
          public Object readDate() {
              readLock.lock();
              try {
                  return data;
              } finally {
                  readLock.unlock();
              }
          }
          
          public void writeData(Object newData) {
              writeLock.lock();
              try {
                  data = newData;
              }
              finally {
                  write.unlock();
              }
          }
          
      }
  2. 介绍乐观锁的实现方式CAS?(小米)

    乐观锁能做到不锁定同步资源也能正确的实现线程同步,是因为乐观锁的主要实现方式 "CAS" (Compare And Swap)是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

    CAS算法涉及到三个操作数:

    • 需要读写的内存值 V。
    • 进行比较的值 A。
    • 要写入的新值 B。

    当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值("比较+更新"整体是一个原子操作),否则不会执行任何操作。一般情况下,"更新"是一个不断重试的操作。

    CAS虽然很高效,但是它也存在三大问题,

    1. ABA问题

      内存值原来是A,后来变成了B,然后又变成了A。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从"A-B-A"变成了"1A-2B-3A"。

      • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
    2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

    3. 只能保证一个共享变量的原子操作

      。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

      • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
  3. sysnchronized的工作原理?synchronized关键字可以作用在哪里(作用域)?使用Synchronized实现死锁的思路?Synchronized锁String或者null会不会出现问题?(滴滴)

    1.工作原理

    synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁

    使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时则锁释放,处于等待队列中的线程再继续竞争锁。

    synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由FJava中的线程和操作系统原生线程是一一对应的,线程被阳塞或者唤醒时时会从用户态切换到内核态这种转换非常消耗性能。

    从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。

    2.Synchronized关键字怎么用?

    synchronized 关键字的使用方式主要有下面 3 种:

    1. 修饰实例方法(锁当前对象实例,进入同步代码前要获得 当前对象实例的锁
    2. 修饰静态方法(锁当前类,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁
    3. 修饰代码块(锁指定对象/类)
      • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
      • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

    3.使用Synchronized实现死锁的思路?

    线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

    如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

    下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

    重要:

    java 复制代码
    public class DeadLockDemo {
        private static Object resource1 = new Object();//资源 1
        private static Object resource2 = new Object();//资源 2
    
        public static void main(String[] args) {
            //线程B
            new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "线程 1").start();
    	    //线程A
            new Thread(() -> {
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource1");
                    synchronized (resource1) {
                        System.out.println(Thread.currentThread() + "get resource1");
                    }
                }
            }, "线程 2").start();
        }
    }

    对线程 2 的代码修改成下面这样就不会产生死锁了。

    java 复制代码
    new Thread(() -> {
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get resource2");
                    synchronized (resource2) {
                        System.out.println(Thread.currentThread() + "get resource2");
                    }
                }
            }, "线程 2").start();

    输出:

    Thread[线程 1,5,main]get resource1
    Thread[线程 1,5,main]waiting get resource2
    Thread[线程 1,5,main]get resource2
    Thread[线程 2,5,main]get resource1
    Thread[线程 2,5,main]waiting get resource2
    Thread[线程 2,5,main]get resource2
    
    Process finished with exit code 0
    

    4.Synchronized锁String或者null会不会出现问题?

    • JVM中,字符串常量池具有缓存功能,就会导致会加锁在同一个对象。synchronized一般不要使用String对象作为锁,如果使用,一定要做好对象区分,否则就会导致对象一致的问题。下面的r1和r2本质上是一个对象,所以synchronized (r1) 和 synchronized (r2) 是一样的。

      java 复制代码
       private static String r1="a";
       private static String r2="a";
    • 对一个为null的对象引用进行synchronized(obj) ,是没有意义的,因为null根本就不是一个对象,会报错java.lang.NullPointerException。

  4. synchronized锁升级过程?(蚂蚁)

    无锁->偏向锁->轻量级锁->重量级锁。

    (1)锁的概念

    • 无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。CAS原理及应用即是无锁的实现。
    • 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
    • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
    • 重量级锁:若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

    (2)升级过程

    • 无锁:是没有开启偏向锁的时候的状态,在JDK1.6之后偏向锁默认在JVM启动后的几秒开启。
    • 偏向锁:是偏向锁开启之后的锁的状态,当一个线程拿到偏向锁的时候,下次想要竞争锁只需要拿线程ID跟MarkWord当中存储的线程ID进行比较,如果线程ID相同则直接获取锁,不需要进行CAS操作和将线程挂起的操作。
    • **轻量级锁:**当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会尝试竞争这把锁,CAS操作失败,线程将进入自旋状态,不断尝试获取锁,不会阻塞,从而提高性能。
    • 重量级锁:当有两个以上的线程获取锁的时候轻量级锁就会升级为重量级锁,因为CAS如果没有成功的话始终都在自旋,非常消耗CPU的,升级为重量级锁之后,线程会被操作系统调度然后挂起,这可以节约CPU资源。
  5. ReentrantLock工作原理(AQS)?(蚂蚁)

    ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作,不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑:

    • 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。

    • 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。这是通过内部的 tryAcquireNanos 方法来实现的。

    • 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。公平锁可以通过在创建 ReentrantLock 时传入 true 来设置。

      java 复制代码
      ReentrantLock fairLock = new ReentrantLock(true);
    • 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。这使得线程可以更灵活地进行等待和唤醒操作,而不仅仅是基于对象监视器的 wait() 和 notify()。多个条件变量的实现依赖于 Condition 接口,例如:

      java 复制代码
      ReentrantLock lock = new ReentrantLock();
      Condition condition = lock.newCondition();
      condition.await();
      condition.signal();
    • 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。这是通过内部的 holdCount 计数来实现的。当一个线程多次获取锁时,holdCount 递增,释放锁时递减,只有当 holdCount 为零时,其他线程才有机会获取锁。

  6. synchronized 和 Reentrantlock 的区别和应用场景?(蚂蚁、嘉立创)

    (1)区别

    • 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块。
    • 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁。
    • **锁类型不同:**synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
    • **响应中断不同:**ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
    • **底层实现不同:**synchronized 是JM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

    (2)应用场景

    synchronized:

    • 简单同步需求:不需要额外的资源管理,因为锁会自动释放。
    • **代码块同步:**可以对特定代码段进行同步,而不是整个方法,更精细地控制同步的范围,从而减少锁的持有时间,提高并发性能。
    • **内置锁的使:**使用对象的内置锁(也称为监视器锁),在需要使用对象作为锁对象的情况下很有用。

    ReentranLock:

    • **高级锁功能需求:**ReentrantLock 提供了 synchronized 所不具备的高级功能,如公平锁、响应中断定时锁尝试、以及多个条件变量。
    • **性能优化:**在高度竞争的环境中, ReentrantLock 可以提供比 synchronized 更好的性能,因为它提供了更细粒度的控制,如尝试锁定和定时锁定,可以减少线程阻塞的可能性。
    • **复杂同步结构:**需要更复杂的同步结构,如需要多个条件变量来协调线程之间的通信时,ReentrantLock 及其配套的 condition 对象可以提供更灵活的解决方案。
  7. 为什么要用volatile关键字/volatile有什么作用?(小米、嘉立创、字节)volatilesynchronized`比较?

    (1)volatile的作用

    • 保证变量对所有线程的可见性:确保了对该变量的读取和写入操作直接从主内存中获取,而不是线程的本地缓存,这可以有效防止线程间的缓存不一致问题。

    • 禁止指令重排序优化:对于 volatile 变量,JVM 保证在写操作前后的指令不会被重排序。这保证了写入 volatile 变量的操作在程序中保持顺序。

      volatile关键字在Java中主要通过内存屏障(写-写、读-写、写-读屏障)来禁止特定类型的指令重排序。

    指令重排序:在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是重排序要满足下面 2 个条件才能进行:

    • 在单线程环境下不能改变程序运行的结果
    • 存在数据依赖关系的不允许重排序。

    (2)volatilesynchronized`比较

    Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题,保证了线程安全性。Volatile解决了变量在多线程环境下的可见性和有序性问题,确保了变量的修改对其他线程是可见的。

    • Synchronized: Synchronized是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加Synchronized关键字来实现同步。
    • Volatile: Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为Volatile时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,同时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中。
  8. 当线程运行出问题的时候,如何定位线程代码?项目中有很多类似的线程创建,如何排查出是哪个业务的线程?(阿里)

    • 线程命名:thread.setName()。

    • 日志记录:在线程入口处添加日志来记录线程名、任务ID、当前业务操作。

      java 复制代码
      log.info("Thread started: {} for order: {}", Thread.currentThread().getName(), orderId);
    • 线程池监控:ThreadPoolExecutor的监控功能来查看当前线程池的状态

  9. 定时任务如何防止并发执行?(阿里)

    • 单体系统中使用互斥锁(如synchronized)确保定时任务的互斥执行。
    • 分布式系统中使用分布式锁来确保只有一个实例可以执行定时任务。比如Redission分布式锁。
相关推荐
FreemanGordon5 分钟前
Java volatile 关键字
java
北京_宏哥6 分钟前
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
java·前端·selenium
Nu1110 分钟前
weakMap 和 weakSet 原理
前端·面试
鲤籽鲲13 分钟前
C# Enumerable类 之 数据排序
开发语言·c#·c# 知识捡漏
北京_宏哥14 分钟前
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
java·selenium·前端工程化
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧14 分钟前
C语言_数据结构总结6:链式栈
c语言·开发语言·数据结构·算法·链表·visualstudio·visual studio
刘鹏37821 分钟前
深入浅出Java中的CAS:原理、源码与实战应用
后端
Enddme22 分钟前
「面试必问!Proxy对比defineProperty的六大核心差异与底层原理」
前端·面试
当归102424 分钟前
微服务与消息队列RabbitMQ
java·微服务
IT猿手25 分钟前
2025最新群智能优化算法:云漂移优化(Cloud Drift Optimization,CDO)算法求解23个经典函数测试集,MATLAB
开发语言·数据库·算法·数学建模·matlab·机器人