并发编程面试题4

简介: 并发编程面试题4

ThreadLocal原理

ThreadLocal中的几个主要方法:


void set(Object value)设置当前线程的线程局部变量的值。

public Object get()该方法返回当前线程所对应的线程局部变量。

public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。


/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }
/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    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类通过操作每一个线程特有的ThreadLocalMap副本来实现线程隔离,ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。


ThreadLocal的应用场景

1、方便同一个线程使用某一对象,避免不必要的参数传递;

2、线程间数据隔离(每个线程在自己线程里使用自己的局部变量,各线程间的ThreadLocal对象互不影响);

3、获取数据库连接、Session、关联ID(比如日志的uniqueID,方便串起多个日志);

其中spring中的事务管理器就是使用的ThreadLocal:

Spring的事务管理器通过AOP切入业务代码,在进入业务代码前,会依据相应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,

就会从DataSource中获取一个连接对象,通过一定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了当中的getConnection()方法,或者说

该方法的返回将由Spring来控制,这样Spring就能让线程内多次获取到的Connection对象是同一个。


什么是线程局部变量?

线程局部变量是线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。


但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程局部变量的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。


ThreadLocal内存泄漏分析与解决方案

ThreadLocal造成内存泄漏的原因?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key 为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。


ThreadLocal内存泄漏解决方案?

解决方案就是每次使用完ThreadLocal,都调用它的remove()方法,清除数据

注意事项

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal 就跟加锁完要解锁一样,用完就清理。


Java里面的锁有哪些?

1、乐观锁和悲观锁

i、乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。


ii、悲观锁


总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。


2、独享锁(独占锁)和共享锁

i、共享锁


共享锁是一种思想: 可以有多个线程获取读锁,以共享的方式持有锁。该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。


Java中用到的共享锁: ReentrantReadWriteLock的读锁。


ii、独享锁(独占锁)


独占锁是一种思想: 只能有一个线程获取锁,以独占的方式持有锁。对于Synchronized而言,当然是独享锁。


Java中用到的独占锁: synchronized,ReentrantLock


3、互斥锁和读写锁

上面讲的独享锁/共享锁就是一种概念,互斥锁/读写锁是具体的实现


i、互斥锁(独占锁的实现)


在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。


如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源


互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock是JDK1.5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,更加灵活。


ii、读写锁(共享锁的实现)


读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。


读锁: 允许多个线程获取读锁,同时访问同一个资源。


写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。


读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。


读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态


读写锁在Java中的具体实现就是ReadWriteLock


一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。


4、可重入锁(递归锁)

广义上的可重入锁指的是可重复可递归调用的锁,任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,例如在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。可重入锁的作用是避免死锁,ReentrantLock和synchronized都是可重入锁


实现原理


每一个可重入锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功或获取锁后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增1;当线程退出同步代码块时,计数器会递减1,如果计数器为 0,则释放该锁。


5、分段锁

分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。


线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全


容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。


比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。


6、偏向锁 & 轻量级锁 & 重量级锁

JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。


i、重量级锁


重量级锁是一种称谓: 这种依赖于操作系统 Mutex Lock来实现的锁称为重量级锁。synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本身依赖底层的操作系统的 Mutex Lock来实现。为了优化synchonized,引入了轻量级锁,偏向锁。


Java中的重量级锁: synchronized


iii、轻量级锁


轻量级锁是JDK6时加入的一种锁优化机制: 轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量。轻量级是相对于使用操作系统互斥量来实现的重量级锁而言的。轻量级锁在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不会有效,必须膨胀为重量级锁。


优点: 如果没有竞争,通过CAS操作成功避免了使用互斥量的开销。


缺点: 如果存在竞争,除了互斥量本身的开销外,还额外产生了CAS操作的开销,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。


iii、偏向锁


研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁。当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。


优点: 把整个同步都消除掉,连CAS操作都不去做了,优于轻量级锁。


缺点: 如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。


volatile 关键字的作用,volatile 关键字是什么原理?

1、volatile 的主要作用

  • 保证变量的内存可见性
  • 禁止指令重排序


2、volatile 主要作用的实现原理

i、保证变量的内存可见性的实现原理


当对volatile修饰的共享变量执行写操作后,JMM会把工作内存(本地内存)中的最新变量值强制刷新到主内存,并且通过 CPU 总线嗅探机制( CPU 总线嗅探机制其实就是一种用来实现缓存一致性的常见机制)告知其他线程该变量副本已经失效,需要重新从主内存中读取。这样volatile就保证了不同线程对共享变量操作的可见性


嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。


可见性


多个线程共同访问共享变量时,某个线程修改了此变量,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值(要看懂这句话就要知道JMM模型)


Java 内存模型


JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。


JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存(也叫工作内存),本地内存中存储了该线程以读/写共享变量的副本。


JMM 的规定

所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。


每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。

不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

JMM 的抽象示意图

然而,JMM 这样的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。正因为 JMM 这样的机制,就出现了可见性问题。


ii、禁止指令重排序的实现原理


使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。说白了就是靠这个内存屏障来实现volatile 禁止指令重排序


什么是指令重排序?


为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令重新排序,也就是不一定会根据编译时的指令顺序(可以理解为不会按照你写代码的顺序从上到下的依次执行),而是它自己会重新排序以达到提高性能的目的


什么是volatile 重排序规则表?

JMM 针对编译器制定了 volatile 重排序规则表,JMM 会限制特定类型的编译器和处理器重排序。如下所示:

其实就是JMM为了实现volatile 禁止指令重排序这个功能针对编译器和处理器制定的规则来限制特定类型的编译器和处理器重排序

什么是内存屏障指令?

内存屏障指令是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。

JMM 把内存屏障指令分为下列四类:


StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。

下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:



从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:


在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。

在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。

也就是说volatile 禁止指令重排序就是靠上面两条内存屏障规则来实现的,这样编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。


java内部有哪些同步的机制?

(1)synchronized关键字实现的重量级同步机制,synchronized是一种同步锁,可以修饰代码块、方法(包括静态方法)、类


synchronized详细内容看这篇:Java同步机制 - 简书


(2)Lock接口及其实现类,如ReentrantLock.ReadLock和ReentrantReadWriteLock.WriteLock。


(3)volatile关键字实现的轻量级同步机制


什么是可重入锁?为什么需要可重入锁? 可重入锁的实现原理?

什么是可重入锁?

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。可重入锁的作用就是为了避免死锁,java中synchronized和ReentrantLock都是可重入锁


package com.test.reen;
// 演示可重入锁是什么意思,可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        synchronized (this) {
          System.out.println("第1次获取锁,这个锁是:" + this);
          int index = 1;
          while (true) {
            synchronized (this) {
              System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
            }
            if (index == 10) {
              break;
            }
          }
        }
      }
    }).start();
  }
}

我的理解就是,某个线程已经获得某个锁,可以无需等待而再次获取锁,并且不会出现死锁(不同线程当然不能多次获得锁,需要等待)。

简单的说,就是某个线程获得锁,之后可以不用等待而再次获取锁且不会出现死锁。

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("iAmFunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("iAmFunctionB");
    }
}

代码解释

functionA()和functionB()都是同步方法(就是要获取类的对象锁才可以执行这个方法),当线程进入funcitonA()会获得该类的对象锁,然后又在functionA()对方法functionB()做了调用,但是functionB()也是同步的,funcitonA()并没有释放这个类的对象锁,但是因为synchronized是可重入锁,然后funcitonB()又是同一个class的方法(也就是同一个线程),所以这个funcitonB()是可以执行的,但是如果synchronized不是可重入锁,那就会发生死锁,因为funcitonA()并没有释放锁,而funcitonB()又需要这个锁才可以执行,但是由于funcitonA()没有执行完funcitonB()所以就一直不释放这个锁,所以就死锁了。这也就是为啥能够防止死锁的原因


可重入锁的实现原理?

每一个可重入锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功或获取锁后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增1;当线程退出同步代码块时,计数器会递减1,如果计数器为 0,则释放该锁。

synchronized关键字的底层实现原理

synchronized关键字修饰的地方

  1. 修饰实例方法上,锁对象是当前的 this 对象。
  2. 修饰代码块,也就是synchronized(object){},锁对象是()中的对象,一般为this或明确的对象。也就是锁住的是你指定的对象
  3. 修饰静态方法上,锁对象是方法区中的类对象,是一个全局锁。
  4. 修饰类,即直接作用一个类。


底层实现原理

synchronized修饰的地方不同,实现的原理不同


synchronized用来修饰方法和静态方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。ACC_SYNCHRONIZE:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor对象,获取成功之后才能执行方法体,方法执行完后再释放monitor对象。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

用来修饰代码块和类时,是通过monitorenter和monitorexit指令来完成。monitorenter: 该指令表示获取锁对象的 monitor 对象,这时 monitor 对象中的 count 会加+1,如果 monitor 已经被其他线程所获取,该线程会被阻塞住,直到 count = 0,再重新尝试获取monitor对象。

monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。


synchronized用来修饰方法和静态方法时


synchronized用来修饰方法和静态方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。


public class SyncTest {
    public synchronized void sync(){
    }
}

通过javap -verbose xxx.class查看反编译结果:

从反编译的结果来看,我们可以看到sync()方法中多了一个标识符。JVM就是根据该ACC_SYNCHRONIZED标识符来实现方法的同步,即:


当方法被执行时,JVM 调用指令会去检查方法上是否设置了ACC_SYNCHRONIZED标识符,如果设置了ACC_SYNCHRONIZED标识符,则会获取锁对象的 monitor 对象,线程执行完方法体后,又会释放锁对象的 monitor对象。在此期间,其他线程无法获得锁对象的 monitor 对象。


修饰代码块和类时,是通过monitorenter和monitorexit指令来完成


public class SyncTest {
    private static int count;
    public SyncTest() {
        count = 0;
    }
    public void sync() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        SyncTest s = new SyncTest();
        Thread t0 = new Thread(new Runnable() {
            @Override
            public void run() {
                s.sync();
            }
        });
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s.sync();
            }
        });
        t0.start();
        t1.start();
    }
}

查看字节码信息:

我们可以看到sync()字节码指令中会有两个monitorenter和monitorexit指令:


monitorenter: 该指令表示获取锁对象的 monitor 对象,这时 monitor 对象中的 count 会加+1,如果 monitor 已经被其他线程所获取,该线程会被阻塞住,直到 count = 0,再重新尝试获取monitor对象。


monitorexit: 该指令表示该线程释放锁对象的 monitor 对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象。


实现原理的核心


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次为发生异步退出释放锁;


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


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


通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。


目录
相关文章
|
5月前
|
Java 开发者
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
Java面试题:请解释内存泄漏的原因,并说明如何使用Thread类和ExecutorService实现多线程编程,请解释CountDownLatch和CyclicBarrier在并发编程中的用途和区别
61 0
|
安全 算法 Java
去某东面试遇到并发编程问题:如何安全地中断一个正在运行的线程
一个位5年的小伙伴去某东面试被一道并发编程的面试题给Pass了,说”如何中断一个正在运行中的线程?,这个问题很多工作2年的都知道,实在是有些遗憾。 今天,我给大家来分享一下我的回答。
101 0
|
资源调度
JUC并发编程之同步器(Semaphore、CountDownLatch、CyclicBarrier、Exchanger、CompletableFuture)附带相关面试题
1.Semaphore(资源调度) 2.CountDownLatch(子线程优先) 3.CyclicBarrier(栅栏) 4.Exchanger(公共交换区) 5.CompletableFuture(异步编程)
184 0
|
6月前
|
算法 Java 调度
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
《面试专题-----经典高频面试题收集四》解锁 Java 面试的关键:深度解析并发编程进阶篇高频经典面试题(第四篇)
76 0
|
5月前
|
Java 测试技术 开发者
Java面试题:解释CountDownLatch, CyclicBarrier和Semaphore在并发编程中的使用
Java面试题:解释CountDownLatch, CyclicBarrier和Semaphore在并发编程中的使用
80 11
|
6月前
|
存储 缓存 算法
【面试宝藏】Go并发编程面试题
探索Go语言并发编程,涉及Mutex、RWMutex、Cond、WaitGroup和原子操作。Mutex有正常和饥饿模式,允许可选自旋优化。RWMutex支持多个读取者并发,写入者独占。Cond提供goroutine间的同步,WaitGroup等待任务完成。原子操作保证多线程环境中的数据完整性,sync.Pool优化对象复用。了解这些,能提升并发性能。
103 2
|
5月前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
50 0
|
5月前
|
Java
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
Java面试题:Java内存模型与并发编程知识点,解释Java中“happens-before”的关系,分析Java中的内存一致性效应(Memory Consistency Effects)及其重要性
33 0
|
6月前
|
安全 Java API
《面试专题-----经典高频面试题收集三》解锁 Java 面试的关键:深度解析并发编程基础篇高频经典面试题(第三篇)
《面试专题-----经典高频面试题收集三》解锁 Java 面试的关键:深度解析并发编程基础篇高频经典面试题(第三篇)
48 0
|
7月前
|
机器学习/深度学习 数据采集 自然语言处理
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程
2024年Python最新【python开发】并发编程(下),2024年最新字节跳动的面试流程

热门文章

最新文章