多线程

概述

并发编程有什么缺点

并发编程的目的是为了能提高程序执行效率,提高程序运行速度,但并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏,上下文切换,线程安全,死锁等问题。

并发编程三个必要因素

原子性:原子,即一个不可再被分割的颗粒。原子性是指一个或多个操作要么全部执行成功,要么全部执行失败。

可见性:一个线程对公有变量的修改,另一个线程能立即看到。

有序性:程序执行的顺序按照代码的先后顺序执行。

并行与并发的区别

并发:多个任务在CPU同一个核上,按CPU分配的时间片轮流执行,从逻辑上看这些任务是同时执行的。

并行:多个处理器同时处理多个任务,真正意义上的同时进行。

串行:有n个任务,有一个线程按顺序执行。

线程与进程的区别

进程 一个在内存中进行的应用程序。每个正在系统上执行的程序都是一个进程。

线程 进程中的一个执行单元

区别:

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

  • 资源开销:每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;线程可以看做轻量级的线程,同一类线程共享代码和数据空间,每个线程都有自己的独立运行栈和程序计数器,线程之间切换的开销小

  • 包含关系:线程是进程的一部分,一个进程至少有一个线程。

  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源相互独立

  • 影响关系:一个进程崩溃后,一般不会影响到其他线程,一个线程崩溃有可能导致整个进程崩溃。

什么是上下文切换

一个线程的时间片用完后,其他线程获取到时间片开始执行。这一过程属于一次上下文切换。

怎么保证多线程的运行安全

出现线程安全问题的原因一般是三个原因:

  • 线程切换带来的原子性问题 解决方法:使用锁

  • 缓存导致的可见性问题 解决办法:锁 volatile

  • 编译优化带来的有序性问题 解决办法:Happens-Before规则

线程

创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。

通过继承Thread类来创建并启动线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。

通过实现Runnable接口来创建并启动线程的步骤如下:

  1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
  2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
  3. 调用线程对象的start()方法来启动该线程。

通过实现Callable接口来创建并启动线程的步骤如下:

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

Runnable 和 callable 区别

runnable接口run方法无返回值;callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以获取异步执行的结果。

runnable接口run方法只能抛出运行时异常,且无法捕获处理,callable接口call方法允许抛出异常,可以获取异常信息

为什么不能直接执行run方法

new一个Thread,线程进入新建状态。调用start()方法,会启动线程,使线程进入到就绪状态,等待cpu分配时间片后就开始执行。会自动执行run方法。

直接运行run方法,会在当前线程下当一个普通方法进行执行。

run()和start()有什么区别?

run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。

调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

线程状态 / 线程生命周期

新建 new:新创建一个线程对象。

就绪 runnable:线程创建后,当调用线程对象的start方法,该线程处于就绪状态,等待被线程调度选中,获取CPU的使用权。

运行 running:就绪状态的线程获取到CPU的时间片,执行程序代码。

阻塞 block:处于运行状态中的线程,由于某种原因,暂时放弃对CPU的使用权,进入到阻塞状态。

等待阻塞:运行状态中的线程执行wait方法,该线程会进入等待队列,使本线程进入到等待阻塞状态。

同步阻塞:线程在获取锁失败,该线程会被放入锁池中,线程会进入同步阻塞状态

其他阻塞:调用线程的sleep或join方法,线程进入阻塞状态,当sleep结束,join等待线程终止或超时,线程重新进入就绪状态

死亡 dead:线程运行结束,或因异常退出了run方法。该线程结束生命周期。

sleep 和 wait 区别

sleep是由Thread线程类的静态方法,wait是object类的方法。

sleep不释放锁,wait释放锁。

wait通常用于线程间交互通信,sleep被用于暂停执行。

wait方法,未设置超时时间,被调用后,线程不会自动苏醒,需要别的线程调用同一个类上的notify方法。

为什么wait notify必须在同步方法或者同步块中被调用

当一个线程需要调用对象的wait方法时,这个线程必须拥有该对象的锁,接着会释放锁进入阻塞状态,直到其他线程调用这个对象上的notify方法。

同样,当一个线程需要调用对象的notify方法,会释放这个对象的锁,以便其他在等待的线程可以获取到锁。

由于这些方法都需要线程只有对象的锁,只能通过同步来实现,所以只能在同步方法或者同步块中使用。

Java如何实现多线程之间的通讯和协作

1.可以通过中断和共享变量的方式实现线程间的通讯和协作

2.wait notify方法

守护线程是什么

守护线程是运行在后台的一种特殊进程,独立于控制终端并且周期性执行某种任务,或等待处理某些发生的事件,在Java中垃圾回收器就是特殊的守护线程

Java内存模型

  • Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

线程类的构造方法。静态块是被哪个线程调用的

线程类的构造方法和静态块有new所在的线程调用,run方法由线程自身调用。

线程运行时发生异常会怎么样

如果异常没有被捕获,该线程将会被停止执行。

介绍下ThreadLocal和它的应用场景

ThreadLocal顾名思义是线程私有的局部变量存储容器,可以理解成每个线程都有自己专属的存储容器,它用来存储线程私有变量,其实它只是一个外壳,内部真正存取是一个Map。每个线程可以通过set()和get()存取变量,多线程间无法访问各自的局部变量,相当于在每个线程间建立了一个隔板。只要线程处于活动状态,它所对应的ThreadLocal实例就是可访问的,线程被终止后,它的所有实例将被垃圾收集。总之记住一句话:ThreadLocal存储的变量属于当前线程。

请介绍ThreadLocal的实现原理,它是怎么处理hash冲突的?

Thread类中有个变量threadLocals,它的类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。每个线程都有自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是ThreadLocal的引用,也就是当前变量的副本,value就是set的值。代码如下所示:

public class Thread implements Runnable {     /* ThreadLocal values pertaining to this thread. This map is maintained      * by the ThreadLocal class. */     ThreadLocal.ThreadLocalMap threadLocals = null;     }

ThreadLocalMap是ThreadLocal的内部类,每个数据用Entry保存,其中的Entry继承与WeakReference,用一个键值对存储,键为ThreadLocal的引用。为什么是WeakReference呢?如果是强引用,即使把ThreadLocal设置为null,GC也不会回收,因为ThreadLocalMap对它有强引用。代码如下所示:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */     Object value;      Entry(ThreadLocal<?> k, Object v) {         super(k);         value = v;     }
}

ThreadLocal中的set方法的实现逻辑,先获取当前线程,取出当前线程的ThreadLocalMap,如果不存在就会创建一个ThreadLocalMap,如果存在就会把当前的threadlocal的引用作为键,传入的参数作为值存入map中。代码如下所示:

public void set(T value) {     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null) {         map.set(this, value);    } else {         createMap(t, value);     }
}

ThreadLocal中get方法的实现逻辑,获取当前线程,取出当前线程的ThreadLocalMap,用当前的threadlocak作为key在ThreadLocalMap查找,如果存在不为空的Entry,就返回Entry中的value,否则就会执行初始化并返回默认的值。代码如下所示:

public T get() {     Thread t = Thread.currentThread();     ThreadLocalMap map = getMap(t);     if (map != null) {         ThreadLocalMap.Entry e = map.getEntry(this);         if (e != null) {             @SuppressWarnings("unchecked")             T result = (T)e.value;             return result;         }     }     return setInitialValue(); }

ThreadLocal中remove方法的实现逻辑,还是先获取当前线程的ThreadLocalMap变量,如果存在就调用ThreadLocalMap的remove方法。ThreadLocalMap的存储就是数组的实现,因此需要确定元素的位置,找到Entry,把entry的键值对都设为null,最后也Entry也设置为null。其实这其中会有哈希冲突,具体见下文。代码如下所示:

public void remove() {     ThreadLocalMap m = getMap(Thread.currentThread());     if (m != null) {         m.remove(this);     } }

ThreadLocal中的hash code非常简单,就是调用AtomicInteger的getAndAdd方法,参数是个固定值0x61c88647。上面说过ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量。

线程池

介绍一下线程池

系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。

与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。

从Java 5开始,Java内建支持线程池。Java 5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。创建出来的线程池,都是通过ThreadPoolExecutor类来实现的。

  • newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
  • newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
  • newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于调用newFixedThread Pool()方法时传入参数为1。
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
  • ExecutorService newWorkStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
  • ExecutorService newWorkStealingPool():该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。

创建线程的7种方式

线程池创建有七种方式,最核心的是最后一种:

  • 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 的封装

线程池有哪些参数,各个参数的作用是什么?

线程池主要有如下6个参数:

  1. corePoolSize(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。
  2. maximumPoolSize(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  3. keepAliveTime(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  4. workQueue(队列):用于传输和保存等待执行任务的阻塞队列。
  5. threadFactory(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  6. handler(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略

介绍一下线程池的工作流程

线程池的工作流程如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRTowjhO-1674817044131)(assets/70D6B0404AB5E9C7AF6699C94B4766BF.png)]

  1. 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。
  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列。
  3. 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。

·

线程池有哪些状态

Running 运行 :接受新的任务,处理等待队列中的任务。

shutDown 关闭 :不接受新的任务,但是会继续处理等待队列中的任务

stop 停止 :不接受新的任务提交,不再处理等待队列中的任务,中断正在执行的线程

tidying 整理 :所有任务都销毁了,workCount为0,线程池的状态在转换为tidying时,会执行钩子方法terminated

terminated 销毁 :terminated() 方法结束后,线程池状态变成终止

线程池中submit() 和 execute() 区别

execute() 只能执行Runnable类型的任务

submit 可以执行Runnable 和 Callable类型的任务

谈谈线程池的拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  2. DiscardPolicy:也是丢弃任务,但是不抛出异常。
  3. DiscardOldestPolicy:丢弃队列最前面的任务,然后将该任务添加到阻塞队列。
  4. CallerRunsPolicy:由调用线程处理该任务。

线程池的队列大小你通常怎么设置?

  1. CPU密集型任务

    尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

JUC

概述

JUC

JUC是java.util.concurrent的缩写,该包参考自EDU.oswego.cs.dl.util.concurrent,是JSR 166标准规范的一个实现。JSR 166是一个关于Java并发编程的规范提案,在JDK中该规范由java.util.concurrent包实现。即JUC是Java提供的并发包,其中包含了一些并发编程用到的基础组件。

JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具,大致可以分为以下几类:

  • 原子更新

    Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环 境下,无锁的进行原子操作。在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新 数组,原子更新引用和原子更新字段。

  • 锁和条件变量

    java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。JUC 下的大多数工具类用到了Lock和Condition来实现并发。

  • 线程池

    涉及到的类比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。

  • 阻塞队列

    涉及到的类比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。

  • 并发容器

    涉及到的类比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。

  • 同步器

    剩下的是一些在并发编程中时常会用到的工具类,主要用来协助线程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。

CAS(比较交换)

CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术;

CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false

volatile关键字有什么用?

当一个变量被定义成volatile之后,它将具备两项特性:

  1. 保证可见性

    当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效。

  2. 禁止指令重排

    使用volatile关键字修饰共享变量可以禁止指令重排序,volatile禁止指令重排序有一些规则:

    • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行;
    • 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

    即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

注意,虽然volatile能够保证可见性,但它不能保证原子性。volatile变量在各个线程的工作内存中是不存在一致性问题的,但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。

谈谈volatile的实现原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

说说你对AQS的理解

抽象队列同步器AbstractQueuedSynchronizer (以下都简称AQS),是用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。在不同的同步器中还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。

AQS采用模板方法模式,在内部维护了n多的模板的方法的基础上,子类只需要实现特定的几个方法(不是抽象方法!不是抽象方法!不是抽象方法!),就可以实现子类自己的需求。

基于AQS实现的组件,诸如:

  • ReentrantLock 可重入锁(支持公平和非公平的方式获取锁);
  • Semaphore 计数信号量;
  • ReentrantReadWriteLock 读写锁。

死锁

多个线程在执行过程中,由于竞争资源而造成互相等待的现象。

如何避免死锁

避免一个线程同时获得多个锁

避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源

使用定时锁,超时后自动释放锁

jvm线程调度算法

抢占式:是指优先让可运行池中优先级高的线程占用cpu,如果优先级相同,就随机选择一个线程。处于运行状态的线程会一直运行,直至不得不放弃cpu

synchronized锁升级

锁对象的对象头中有threadID字段,线程第一次获取该对象锁时,默认是偏向锁,并将threadid设置为该线程id,再次进入是会判断threadID是否一致。如果一致,则继续使用,如果不一致,则认为是其他线程来获取该对象锁,会将偏向锁升级成轻量级锁。

在线程持有该对象锁期间,有其他线程也来获取该对象锁,此时会将锁升级成重量级锁。

synchronized实现原理

一、synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。

  • monitorenter:

    每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  • monitorexit:

    执行monitorexit的线程必须是objectref所对应的monitor持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

    monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。

二、方法的同步并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

三、总结

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

synchronized和lock的区别

synchronized 可以给类、方法、代码块加锁;lock只能给代码块加锁。

synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁;而lock需要自己加锁和释放锁。

lock可以知道有没有成功获取锁,synchronized不能

  1. synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
  2. synchronized可以用在代码块上、方法上;Lock只能写在代码里。
  3. synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。
  4. synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。
  5. synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。
  6. synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。

谈谈ReentrantLock的实现原理

ReentrantLock是基于AQS实现的,AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。

在同步队列中,还存在2中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。

AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQS的tryAcquiretryRelease方法实现的lock和unlock。

ReentrantLock 结构如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ezOF7UUz-1674817044132)(assets/2F192BDFAD9C22C62FE4D23692FDE892.png)]

首先ReentrantLock 实现了 Lock 接口,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来公平锁和非公平锁的。通过Sync重写的方法tryAcquire、tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁。

如果不使用synchronized和Lock,如何保证线程安全?

  1. volatile

    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值。需要注意的是,volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

  2. 原子变量

    在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,使用该类可以简化线程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

  3. 本地存储

    可以通过ThreadLocal类来实现线程本地存储的功能。每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。

  4. 不可变的

    只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态,“不可变”带来的安全性是最直接、最纯粹的。Java语言中,如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,由于Java语言目前暂时还没有提供值类型的支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行。String类是一个典型的不可变类,可以参考它设计一个不可变类。

说一说Java中乐观锁和悲观锁的区别

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现的。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

公平锁vs非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

说说你对读写锁的了解

与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥、读写互斥、写写互斥,而一般的独占锁是:读读互斥、读写互斥、写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。 注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。

在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:

  1. 公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
  2. 可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁。
  3. 可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。

多线程与JUC面试题相关推荐

  1. Java多线程系列--“JUC原子类”01之 框架

    2019独角兽企业重金招聘Python工程师标准>>> Java多线程系列--"JUC原子类"01之 框架 根据修改的数据类型,可以将JUC包中的原子操作类可以分 ...

  2. Java多线程系列--“JUC原子类”03之 AtomicLongArray原子类

    概要 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray这3个数组类型的原子类的原理和用法相似.本章以AtomicLongArray对数 ...

  3. Java多线程系列--“JUC线程池”06之 Callable和Future

    转载自  Java多线程系列--"JUC线程池"06之 Callable和Future Callable 和 Future 简介 Callable 和 Future 是比较有趣的一 ...

  4. Java多线程系列--“JUC锁”05之 非公平锁

    转载自:http://www.cnblogs.com/skywang12345/p/3496651.html点击打开链接 概要 前面两章分析了"公平锁的获取和释放机制",这一章开始 ...

  5. Java多线程系列---“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下: 01. Java多线程系列--"JUC锁"01之 框架 02. Java多线程系列--"JUC锁 ...

  6. java run里面定义变量_Java程序员50多道最热门的多线程和并发面试题(答案解析)...

    下面是Java程序员相关的热门面试题,你可以用它来好好准备面试. 1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器 ...

  7. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对"公平锁"的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括: 基本概念 ReentrantLock数据结构 参考代码 获取公平锁(基于JDK1.7.0 ...

  8. 关于多线程中的面试题

    关于多线程中的面试题 常见面试的 1.现在有T1.T2.T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行? 答:使用join就OK了. public class ThreeThr ...

  9. 多线程及相关面试题与拓展

    首先,抛出几个面试题,相信看完介绍就会明白怎么解答了. 你理解的多线程? iOS的多线程方案有哪几种?你更倾向于哪一种? GCD 的队列类型 线程安全的处理手段有哪些? OC你了解的锁有哪些? ios ...

最新文章

  1. 1003 Emergency(Dijkstra,Bellman-Ford,SPFA三种解法)
  2. 解决 Illegal DefaultValue null for parameter type integer 异常
  3. (转)检测到在集成的托管管道模式下不适用的ASP.NET 设置
  4. Unity3d HDR和Bloom效果(高动态范围图像和泛光)
  5. explain ref_数据库查询优化:使用explain分析sql语句执行效率
  6. [转]Oracle update用例
  7. 【大盛】全网首发HTC One/M7 最新本地化TrickDroid9.0/固件升级/永久root/高级,快速设置/稳定,流畅经典ROM...
  8. JAVA 虚拟机 (SE 7)【待更】
  9. Qt学习笔记-简单的UDP广播包聊天室
  10. .html好 还是.asp好,各位说说在ASP.net里 用静态函数的好 还是实例函数出处HTML好???...
  11. 小程序【笔记002】逻辑层简介
  12. 详解MRS CDL整体架构设计
  13. 体操冠军江钰源:妈妈你不要去讨饭了
  14. 谷歌用3亿张图做了个深度学习实验,结论:数据还是越大越好
  15. Arduino温控风扇
  16. ||分享一些百度云下载不限速神器||
  17. linux修改dns地址的三种方法
  18. C++求一个数的最大奇数约数
  19. 电脑蓝牙耳机无法调节用关闭绝对音量来解决
  20. <nav>导航标签 和div标签一样,块属性标签</nav>    <main>内容区域,和section没有区别</main>    <section>内容区域,和main没有区别</sect

热门文章

  1. OTA线下攻防战 | 一点财经
  2. XDOJ 233/237-字符串复制
  3. 项目中手机、姓名、身份证信息等在日志和响应数据中脱敏操作
  4. xz2显示无法连接服务器,微端网页版无法登入问题解决方法
  5. 我的十一Win10之旅
  6. Python Dataloader 多进程报错 num_workers参数设置
  7. srs直播 java开发,通过srs实现直播
  8. 基于AI的自动化处理
  9. 《画解数据结构》九张动图,画解队列
  10. STM32之SWD连接配置说明