Java哪些地方使用了CAS

Java提供的API中使用CAS的地方有很多,比较典型的使用场景有原子类、AQS、并发容器。

  • 对于原子类,以AtomicInteger为例,它的内部提供了诸多原子操作的方法。如原子替换整数值、增加指定的值、加1,这些方法的底层便是采用操作系统提供的CAS原子指令来实现的。
  • 对于AQS,在向同步队列的尾部追加节点时,它首先会以CAS的方式尝试一次,如果失败则进入自旋状态,并反复以CAS的方式进行尝试。此外,在以共享方式释放同步状态时,它也是以CAS方式对同步状态进行修改的。
  • 对于并发容器,以ConcurrentHashMap为例,它的内部多次使用了CAS操作。在初始化数组时,它会以CAS的方式修改初始化状态,避免多个线程同时进行初始化。在执行put方法初始化头节点时,它会以CAS的方式将初始化好的头节点设置到指定槽的首位,避免多个线程同时设置头节点。在数组扩容时,每个线程会以CAS方式修改任务序列号来争抢扩容任务,避免和其他线程产生冲突。在执行get方法时,它会以CAS的方式获取头指定槽的头节点,避免其他线程同时对头节点做出修改。

CAS的实现离不开操作系统原子指令的支持,Java中对原子指令封装的方法集中在Unsafe类中,包括:原子替换引用类型、原子替换int型整数、原子替换long型整数。这些方法都有四个参数:var1、var2、var4、var5,其中var1代表要操作的对象,var2代表要替换的成员变量,var4代表期望的值,var5代表更新的值。

  1. public final native boolean compareAndSwapObject( Object var1, long var2, Object var4, Object var5);
  2. public final native boolean compareAndSwapInt( Object var1, long var2, int var4, int var5);
  3. public final native boolean compareAndSwapLong( Object var1, long var2, long var4, long var6);

请你说说JUC

JUC是java.util.concurrent的缩写,这个包是JDK 1.5提供的并发包,包内主要提供了支持并发操作的各种工具。这些工具大致分为如下5类:原子类、锁、线程池、并发容器、同步工具。

  1. 原子类 从JDK 1.5开始,并发包下提供了atomic子包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。
  2. 锁 从JDK 1.5开始,并发包中新增了Lock接口以及相关实现类用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。
  3. 线程池 从JDK 1.5开始,并发包下新增了内置的线程池。其中,ThreadPoolExecutor类代表常规的线程池,而它的子类ScheduledThreadPoolExecutor对定时任务提供了支持,在子类中我们可以周期性地重复执行某个任务,也可以延迟若干时间再执行某个任务。此外,Executors是一个用于创建线程池的工具类,由于该类创建出来的是带有无界队列的线程池,所以在使用时要慎重。
  4. 并发容器 从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。
    1. 第一类是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。
    2. 第二类是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。
    3. 第三类是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。
  5. 同步工具 从JDK 1.5开始,并发包下新增了几个有用的并发工具类,一样可以保证线程安全。
    1. 其中,Semaphore类代表信号量,可以控制同时访问特定资源的线程数量;
    2. CountDownLatch类则允许一个或多个线程等待其他线程完成操作;
    3. CyclicBarrier可以让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。

      说说你对ThreadLocal的理解

      ThreadLocal,即线程变量,它将需要并发访问的资源复制多份,让每个线程拥有一份资源。由于每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。
      ThreadLocal提供了线程安全的共享机制,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。 在实现上,Thread类中声明了threadLocals变量,用于存放当前线程独占的资源。
      ThreadLocal类中定义了该变量的类型(ThreadLocalMap),这是一个类似于Map的结构,用于存放键值对。ThreadLocal类中还提供了set和get方法,set方法会初始化ThreadLocalMap并将其绑定到Thread.threadLocals,从而将传入的值绑定到当前线程。
      在数据存储上,传入的值将作为键值对的value,而key则是ThreadLocal对象本身(this)。get方法没有任何参数,它会以当前ThreadLocal对象(this)为key,从Thread.threadLocals中获取与当前线程绑定的数据。
      注意,ThreadLocal不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式。而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。 一般情况下,如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制。如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。

      说说volatile的用法及原理

      volatile是轻量级的synchronized,它保证了共享变量的“可见性”。
      可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile使用恰当的话,它比synchronized的执行成本更低,因为它不会引起线程上下文的切换和调度。
      简而言之,volatile变量具有以下特性:
  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对单个volatile变量的读写具有原子性,对“volatile变量++”这种复合操作则不具有原子性。
  • 写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存中。
  • 读内存语义:当读一个volatile变量时,JMM会把该线程本地内存置为无效,使其从主内存中读取共享变量。

volatile的底层是采用内存屏障来实现的,就是在编译器生成字节码时,会在指令序列中插入内存屏障来禁止处理器重排序。内存屏障就是一段与平台相关的代码,Java中的内存屏障代码都在Unsafe类中定义,共包含三个方法:LoadFence()、storeFence()、fullFence()。
volatile只能保证单个变量读写的原子性,而锁则可以保证对整个临界区的代码执行具有原子性。所以,在功能上锁比volatile更强大,在性能上volatile更优优势。

说说你对线程池的理解

线程池可以有效地管理线程:它可以管理线程的数量,可以避免无节制的创建线程,导致超出系统负荷直至崩溃。它还可以让线程复用,可以大大地减少创建和销毁线程所带来的开销。 线程池中最重要的参数有:corePoolSize(核心线程数)、workQueue(等待队列)、maxinumPoolSize(最大线程数)、handler(拒绝策略)、keepAliveTime(空闲线程存活时间)。
当我们向线程池提交一个任务之后,线程池按照如下步骤处理这个任务:

  1. 判断线程数是否达到corePoolSize,若没有则新建线程执行该任务,否则进入下一步。
  2. 判断等待队列是否已满,若没有则将任务放入等待队列,否则进入下一步。
  3. 判断线程数是否达到maxinumPoolSize,如果没有则新建线程执行任务,否则进入下一步。
  4. 采用初始化线程池时指定的拒绝策略,拒绝执行该任务。

新建的线程处理完当前任务后,不会立刻关闭,而是继续处理等待队列中的任务。如果线程的空闲时间达到了keepAliveTime,则线程池会销毁一部分线程,将线程数量收缩至corePoolSize。

  1. 第2步中的队列可以有界也可以无界。若指定了无界的队列,则线程池永远无法进入第3步,相当于废弃了maxinumPoolSize参数。这种用法是十分危险的,如果任务在队列中产生大量的堆积,就很容易造成内存溢出。JDK为我们提供了一个名为Executors的线程池的创建工具,该工具创建出来的就是带有无界队列的线程池,所以一般在工作中我们是不建议使用这个类来创建线程池的。
  2. 第4步中的拒绝策略主要有4个:让调用者自己执行任务、直接抛出异常、丢弃任务不做任何处理、删除队列中最老的任务并把当前任务加入队列。这4个拒绝策略分别对应着RejectedExecutionHandler接口的4个实现类,我们也可以基于这个接口实现自己的拒绝策略。

线程池的生命周期包含5个状态:RUNNING、SHUTDOWN、STOP、TIDING、TERMINATED。这5种状态的状态值分别是:-1、0、1、2、3。在线程池的生命周期中,它的状态只能由小到大迁移,是不可逆的。

  1. RUNNING:表示线程池正在运行。
  2. SHUTDOWN:执行shutdown()时进入该状态,此时队列不会清空,线程池会等待任务执行完毕。
  3. STOP:执行shutdownNow()时进入该状态,此时现线程池会清空队列,不再等待任务的执行。
  4. TIDING:当线程池及队列为空时进入该状态,此时线程池会执行钩子函数,目前该函数是一个空的实现。
  5. TERMINATED:钩子函数执行完毕后,线程进入该状态,表示线程池已经死亡。

    说说你了解的线程通信方式

    在Java中,常用的线程通信方式有两种,分别是利用Monitor实现线程通信、利用Condition实现线程通信。
    线程同步是线程通信的前提,所以究竟采用哪种方式实现通信,取决于线程同步的方式。 如果是采用synchronized关键字进行同步,则需要依赖Monitor(同步监视器)实现线程通信,Monitor就是锁对象。
    在synchronized同步模式下,锁对象可以是任意的类型,所以通信方法自然就被定义在Object类中了,这些方法包括:wait()、notify()、notifyAll()。
    一个线程通过Monitor调用wt()时,它就会释放锁并在此等待。当其他线程通过Monitor调用notify()时,则会唤醒在此等待的一个线程。当其他线程通过Monitor调用notifyAll()时,则会唤醒在此等待的所有线程。
    JDK 1.5新增了Lock接口及其实现类,提供了更为灵活的同步方式。如果是采用Lock对象进行同步,则需要依赖Condition实现线程通信,Condition对象是由Lock对象创建出来的,它依赖于Lock对象。Condition对象中定义的通信方法,与Object类中的通信方法类似,它包括await()、signal()、signalAll()。当通过Condition调用await()时当前线程释放锁并等待,当通过Condition调用signal()时唤醒一个等待的线程,当通过Condition调用signalAll()时则唤醒所有等待的线程。
    线程同步是基于同步队列实现的,而线程通信是基于等待队列实现的。当调用等待方法时,即将当前线程加入等待队列。当调用通知方法时,即将等待队列中的一个或多个线程转移回同步队列。因为synchronized只有一个Monitor,所以它就只有一个等待队列。
    而Lock对象可以创建出多个Condition,所以它拥有多个等待队列。多个等待队列带来了极大的灵活性,所以基于Condition的通信方式更为推荐。 比如,在实现生产消费模型时,生产者要通知消费者、消费者要通知生产者。相反,不应该出现生产者通知生产者、消费者通知消费者这样的情况。如果使用synchronized实现这个模型,由于它只有一个等待队列,所以只能把生产者和消费者加入同一个队列,这就会导致生产者通知生产者、消费者通知消费者的情况出现。采用Lock实现这个模型时,由于它有多个等待队列,可以有效地将这两个角色区分开,就能避免出现这样的问题。

    说说怎么保证线程安全

    Java保证线程安全的方式有很多,其中较为常用的有三种,按照资源占用情况由轻到重排列,这三种保证线程安全的方式分别是原子类、volatile、锁。
  • JDK从1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,无论原子更新哪种类型,都要遵循“比较和替换”规则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败。
  • volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全。可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步。volatile具有如下的内存语义:
    • 当写一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;
    • 当读一个volatile变量时,该线程本地内存会被置为无效,迫使线程直接从主内存中读取共享变量。
  • 原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全,Java中加锁的方式有两种,分别是synchronized关键字和Lock接口。
  • 实现线程安全的方式有很多,除了上述三种方式之外,还有如下几种方式:
      1. 无状态设计 线程安全问题是由多线程并发修改共享变量引起的,如果在并发环境中没有设计共享变量,则自然就不会出现线程安全问题了。这种代码实现可以称作“无状态实现”,所谓状态就是指共享变量。
      1. 不可变设计 如果在并发环境中不得不设计共享变量,则应该优先考虑共享变量是否为只读的,如果是只读场景就可以将共享变量设计为不可变的,这样自然也不会出现线程安全问题了。具体来说,就是在变量前加final修饰符,使其不可被修改,如果变量是引用类型,则将其设计为不可变类型(参考String类)。
      1. 并发工具 java.util.concurrent包提供了几个有用的并发工具类,一样可以保证线程安全:
        • Semaphore:就是信号量,可以控制同时访问特定资源的线程数量。
        • CountDownLatch:允许一个或多个线程等待其他线程完成操作。
        • CyclicBarrier:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。
      1. 本地存储 我们也可以考虑使用ThreadLocal存储变量,ThreadLocal可以很方便地为每一个线程单独存一份数据,也就是将需要并发访问的资源复制成多份。这样一来,就可以避免多线程访问共享变量了,它们访问的是自己独占的资源,它从根本上隔离了多个线程之间的数据共享。

        说说wt()和sleep()的区别

        wt()和sleep()方法主要有如下三个区别:
  1. 所属的类型不同
    1. wt()是Object类的实例方法,调用该方法的线程将进入WTING状态。
    2. sleep()是Thread类的静态方法,调用该方法的线程将进入TIMED_WTING状态。
  2. 对锁的依赖不同
    1. wt()依赖于synchronized锁,它必须通过监视器进行调用,在调用后线程会释放锁。
    2. sleep()不依赖于任何锁,所以在调用后它也不会释放锁。
  3. 返回的条件不同
    1. 调用wt()进入等待状态的线程,需要由notify()/notifyAll()唤醒,从而返回。
    2. 调用sleep()进入超时等待的线程,需要在超时时间到达后自动返回。

wt()方法也支持超时参数,线程调用带有超时参数的wt()会进入TIMED_WTING状态,在此状态下的线程可以通过notify()/notifyAll()唤醒从而返回,若在达到超时时间后仍然未被唤醒则自动返回。
如果采用Lock进行线程同步,则不存在同步监视器,此时需要使用Condition的方法实现等待。Condition对象是通过Lock对象创建出来的,它的awt()方法会导致线程进入WTING状态,它的带超时参数的awt()方法会导致线程进入TIMED_WTING状态,当调用它的signal()/signalAll()方法时,线程会被唤醒从而返回。

说说线程的状态

Java线程在运行的生命周期中,在任意给定的时刻,只能处于下列6种状态之一:

  • NEW :初始状态,线程被创建,但是还没有调用start方法。
  • RUNNABLE:可运行状态,线程正在JVM中执行,但是有可能在等待操作系统的调度。
  • BLOCKED :阻塞状态,线程正在等待获取监视器锁。
  • WTING :等待状态,线程正在等待其他线程的通知或中断。
  • TIMED_WTING:超时等待状态,在WTING的基础上增加了超时时间,即超出时间自动返回。
  • TERMINATED:终止状态,线程已经执行完毕。

线程在创建之后默认为初始状态,在调用start方法之后进入可运行状态,可运行状态不代表线程正在运行,它有可能正在等待操作系统的调度。
进入等待状态的线程需要其他线程的通知才能返回到可运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,除了他线程的唤醒,在超时时间到达时也会返回运行状态。此外,线程在执行同步方法时,在没有获取到锁的情况下,会进入到阻塞状态。线程在执行完run方法之后,会进入到终止状态。
Java将操作系统中的就绪和运行两个状态合并为可运行状态(RUNNABLE)。线程阻塞于synchronized的监视器锁时会进入阻塞状态,而线程阻塞于Lock锁时进入的却是等待状态,这是因为Lock接口实现类对于阻塞的实现均使用了LockSupport类中的相关方法。

说说线程的创建方式

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

  1. 通过继承Thread类来创建线程的步骤如下
    1. 定义Thread类的子类,并重写该类的run()方法,该方法将作为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象。
    3. 用线程对象的start()方法来启动该线程。
  2. 通过实现Runnable接口来创建线程的步骤如下
    1. 定义Runnable接口的实现类,并实现该接口的run()方法,该方法将作为线程执行体。
    2. 创建Runnable实现类的实例,并将其作为参数来创建Thread对象,Thread对象为线程对象。
    3. 调用线程对象的start()方法来启动该线程。
  3. 通过实现Callable接口来创建线程的步骤如下
    1. 定义Callable接口的实现类,并实现call()方法,该方法将作为线程执行体。
    2. 创建Callable实现类的实例,并以该实例作为参数,创建FutureTask对象。
    3. 使用FutureTask对象作为参数,创建Thread对象,然后启动线程。
    4. 调用FutureTask对象的get()方法,获得子线程执行结束后的返回值。

归纳起来,创建线程的方式实际只有两种:
继承父类和实现接口。而使用Runnable接口和Callable接口的方式,区别在于前者不能获得线程执行结束的返回值,后者可以获得线程执行结束的返回值。
而继承父类和实现接口这两种方式的优缺点是:

  • 采用接口的方式创建线程,优点是线程类还可以继承于其他类,并且多个线程可以共享一个线程体,适合多个线程处理同一份资源的情况。缺点是编程稍微麻烦一点点。
  • 采用继承的方式创建线程,优点是编程稍微简单一点点。缺点是因为线程类已经继承了Thread类,所以就不能继承其他的父类了。 所以,通常情况下,更推荐采用接口的方式来创建线程。如果需要返回值,就使用Callable接口,否则使用Runnable接口即可。

    请你说说多线程

    线程是操作系统调度的最小单元,它可以让一个进程并发地处理多个任务,也叫轻量级进程。所以,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源。
    由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。 总的来说,操作系统可以同时执行多个任务,每个任务就是一个进程。
    进程可以同时执行多个任务,每个任务就是一个线程。一个程序运行之后至少有一个进程,而一个进程可以包含多个线程,但至少要包含一个线程。

    请你说说进程间的通信方式

    进程间通信主要包括:管道、命名管道、消息队列、共享内存、内存映射、信号量、Socket
  1. 管道:管道本质是内核中维护的一块内存缓冲区。
  2. 命名管道:因为无名管道只适用于具有亲缘关系的线程,所以就衍生出来命名管道,这样就可以实现了非亲缘关系进程之间的通信。
  3. 信号量:一种通知机制。
  4. 消息队列:是一个消息链表,既可以读消息,也可以写消息。
  5. 共享内存:多个线程共享一片内存区域。
  6. 内存映射:就是将磁盘文件数据映射到内存,通过修改内存就能修改磁盘文件。
  7. socket接口,socket接口一般用于不同主机上进程之间的通信

    请你说说线程和协程的区别

  8. 一个线程可以多个协程

  9. 线程进程都是同步机制,而协程则是异步。
  10. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
  11. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  12. 协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行,但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池

    请你说说死锁定义及发生的条件

    两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。这些永远在互相等待的进程称为死锁进程。
    产生死锁的四个必要条件:

  13. 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;

  14. 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
  15. 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
  16. 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

    说说你对AQS的理解

    AQS(AbstractQueuedSynchronizer)是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。
    AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。
    AQS的同步队列,是一个双向链表,AQS则持有链表的头尾节点。对于尾节点的设置,是存在多线程竞争的,所以采用CAS的方式进行修改。对于头节点设置,则一定是拿到了同步状态的线程才能处理,所以修改头节点不需要采用CAS的方式。
    AQS的同步状态,是一个int类型的整数,它在表示状态的同时还能表示数量。通常情况下,状态为0时表示无锁,状态大于0时表示锁的重入次数。另外,在读写锁的场景中,这个状态标志既要记录读锁又要记录写锁。于是,锁的实现者就将状态表示拆成高低两部分,高位存读锁、低位存写锁。
    同步状态需要在并发环境下修改,所以需要保证其线程安全。由于AQS本身就是锁的实现工具,所以不适合用锁来保证其线程安全,同步状态是被volatile修饰的,该关键字可以保证状态变量的内存可见性,从而解决了线程安全问题。

    说说synchronize的用法及原理

    synchronized可以作用在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同。不同的锁对象,意味着不同的锁粒度,据要锁定的范围,准确的选择锁对象,准确地确定锁的粒度,能够降低锁带来的性能开销。

  17. 作用在静态方法上,则锁是当前类的Class对象。

  18. 作用在普通方法上,则锁是当前的实例(this)。
  19. 作用在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。

原理 synchronized的底层是采用Java对象头来存储锁信息的,并且还支持锁升级。 Java对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。如果对象不是数组类型,则没有Array length信息。
synchronized的锁信息包括锁的标志和锁的状态,这些信息都存放在对象头的Mark Word这一部分。 Java 6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。所以,在Java 6中,锁一共被分为4种状态,级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。随着线程竞争情况的升级,锁的状态会从无锁状态逐步升级到重量级锁状态。锁可以升级却不能降级,这种只能升不能降的策略,是为了提高效率。
synchronized的早期设计并不包含锁升级机制,所以性能较差,那个时候只有无锁和有锁之分。是为了提升性能才引入了偏向锁和轻量级锁,所以需要重点关注这两种状态的原理,以及它们的区别。

  • 偏向锁,顾名思义就是锁偏向于某一个线程。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时就不需要做加锁和解锁操作了,只需要简单地测试一下Mark Word里是否存储着自己的线程ID即可。
  • 轻量级锁,就是加锁时JVM先在当前线程栈帧中创建用于存储锁记录的空间,并将Mark Word复制到锁记录中。然后线程尝试以CAS方式将Mark Word替换为指向锁记录的指针,如果成功则当前线程获得锁,如果失败则表示其他线程竞争锁,此时当前线程就会通过自旋来尝试获取锁。

下面,我们再从实际场景出发,来具体说说锁升级的过程:

  1. 开始,没有任何线程访问同步块,此时同步块处于无锁状态。
  2. 然后,线程1首先访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时没有竞争,所以偏向锁加锁成功,此时Mark Word里存储的是线程1的ID。
  3. 然后,线程2开始访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁。由于此时存在竞争,所以偏向锁加锁失败,于是线程2会发起撤销偏向锁的流程(清空线程1的ID),于是同步块从偏向线程1的状态恢复到了可以公平竞争的状态。
  4. 然后,线程1和线程2共同竞争,它们同时以CAS方式修改Mark Word,尝试加轻量级锁。由于存在竞争,只有一个线程会成功,假设线程1成功了。但线程2不会轻易放弃,它认为线程1很快就能执行完毕,执行权很快会落到自己头上,于是线程2继续自旋加锁。
  5. 最后,如果线程1很快执行完,则线程2就会加轻量级锁成功,锁不会晋升到重量级状态。也可能是线程1执行时间较长,那么线程2自旋一定次数后就放弃自旋,并发起锁膨胀的流程。届时,锁被线程2修改为重量级锁,之后线程2进入阻塞状态。而线程1重复加锁或者解锁时,CAS操作都会失败,此时它就会释放锁并唤醒等待的线程。 总之,在锁升级的机制下,锁不会一步到位变为重量级锁,而是根据竞争的情况逐步升级的。当竞争小的时候,只需以较小的代价加锁,直到竞争加剧,才使用重量级锁,从而减小了加锁带来的开销。

    synchronized和Lock有什么区别

    说说你了解的线程同步方式

    synchronized和Lock都是锁,都是线程同步的手段,它们的区别主要体现在如下三个方面:

  6. 使用方式的区别

    1. synchronized关键字可以作用在静态方法、实例方法和代码块上,它是一种隐式锁,即我们无需显式地获取和释放锁,所以使用起来十分的方便。在这种同步方式下,我们需要依赖Monitor(同步监视器)来实现线程通信。
      1. 若关键字作用在静态方法上,则Monitor就是当前类的Class对象;
      2. 若关键字作用在实例方法上,则Monitor就是当前实例(this);
      3. 若关键字作用在代码块上,则需要在关键字后面的小括号里显式指定一个对象作为Monitor。
    2. Lock接口是显式锁,即我们需要调用其内部定义的方法显式地加锁和解锁,相对于synchronized来说这显得有些繁琐,但是却提供了更大的灵活性。在这种同步方式下,我们需要依赖Condition对象来实现线程通信,该对象是由Lock对象创建出来的,依赖于Lock。每个Condition代表一个等待队列,而一个Lock可以创建多个Condition对象。相对而言,每个Monitor也代表一个等待队列,但synchronized只能有一个Monitor。所以,在实现线程通信方面,Lock接口具备更大的灵活性。
  7. 功能特性的区别
    1. synchronized是早期的API,Lock则是在JDK 1.5时引入的。在设计上,Lock弥补了synchronized的不足,它新增了一些特性,均是synchronized不具备的,这些特性包括:
      1. 可中断地获取锁:线程在获取锁的过程中可以被中断。
      2. 非阻塞地获取锁:该方法在调用后立刻返回,若能取到锁则返回true,否则返回false。
      3. 可超时地获取锁:若线程在到达超时时间后仍未获得锁,并且线程也没有被中断,则返回false。
  8. 实现机制的区别
    1. synchronized的底层是采用Java对象头来存储锁信息的,对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。
    2. AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。

早期的synchronized性能较差,不如Lock。后来synchronized在实现上引入了锁升级机制,性能上已经不输给Lock了。所以,synchronized和Lock的区别主要不在性能上,因为二者性能相差无几。
Java 6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。所以,从Java 6开始,锁一共被分为4种状态,级别由低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。随着线程竞争情况的升级,锁的状态会从无锁状态逐步升级到重量级锁状态。锁可以升级却不能降级,这种只能升不能降的策略,是为了提高效率。
synchronized的早期设计并不包含锁升级机制,那个时候只有无锁和有锁之分。是为了提升性能才引入了偏向锁和轻量级锁,所以需要重点关注这两种状态的原理,以及它们的区别。

  • 偏向锁,顾名思义就是锁偏向于某一个线程。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时就不需要做加锁和解锁操作了,只需要简单地测试一下Mark Word里是否存储着自己的线程ID即可。
  • 轻量级锁,就是加锁时JVM先在当前线程栈帧中创建用于存储锁记录的空间,并将Mark Word复制到锁记录中,官方称之为Displaced Mark Word。然后线程尝试以CAS方式将Mark Word替换为指向锁记录的指针,如果成功则当前线程获得锁,如果失败则表示其他线程竞争锁,此时当前线程就会通过自旋来尝试获取锁。

    说说Java中常用的锁及原理

    Java中加锁有两种方式,分别是synchronized关键字和Lock接口,
    Lock接口的经典实现是ReentrantLock。另外还有ReadWriteLock接口,它的内部设计了两把锁分别用于读写,这两把锁都是Lock类型,它的经典实现是ReentrantReadWriteLock。
    synchronized的实现依赖于对象头,Lock接口的实现则依赖于AQS。 synchronized的底层是采用Java对象头来存储锁信息的,对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。
    AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。
    ReentrantLock通过内部类Sync定义了锁,它还定义了Sync的两个子类FrSync和NonfrSync,这两个子类分别代表公平锁和非公平锁。
    Sync继承自AQS,它不仅仅使用AQS的同步状态记录锁的信息,还利用同步状态记录了重入次数。同步状态是一个整数,当它为0时代表无锁,当它为N时则代表线程持有锁并重入了N次。
    ReentrantReadWriteLock支持重入的方式与ReentrantLock一致,它也定义了内部类Sync,并定义出两个子类FrSync和NonfrSync来实现公平锁和非公平锁。此外,ReentrantReadWriteLock内部包含读和写两把锁,这两把锁都是由Sync来实现的。区别在于读锁支持共享,即多个线程可以同时加读锁成功,而写锁是互斥的,即只能有一个线程加锁成功。