首页 > 试题广场 >

在监视器(Monitor)内部,是如何做线程同步的?程序应该

[问答题]
在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
监视器和锁在java中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程能访问同步代码块。每一个对象都与一个监视器相关联,没有获取到锁的对象不能执行这块同步代码块
发表于 2018-04-12 21:30:13 回复(0)
更多回答
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。本质都是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器

代码块同步是使用monitorenter和monitorexit指令实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
发表于 2017-03-03 14:57:29 回复(1)
java 虚拟机中, 每个对象( Object class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁.
 
一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码
 
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
发表于 2015-12-18 11:02:31 回复(2)
监视器是一种同步机制。
java中提供了这种同步机制的实现
1、Object类中提供的wait notify notifyall方法
2、synchronized关键字隐式锁 或 Lock显示锁
3、每个对象自带一个锁
4、显示锁的Condition条件,用于协作

程序员可以使用显示锁或隐式锁实现互斥,wait notify notifyall condition 实现协作
发表于 2018-04-19 11:32:44 回复(0)
在 java 虚拟机中, 每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁.   一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码   另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
发表于 2016-03-04 19:39:07 回复(0)
在 java 虚拟机中, 每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁 .   一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码 , 线程在获取锁之前不允许执行该部分的代码   另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
发表于 2017-05-11 11:26:30 回复(0)
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
发表于 2015-10-29 12:10:41 回复(0)

1.什么是锁?

在并发环境下,多个线程会对同一资源进行争抢,那么可能会导致数据不一致的问题,为了解决这种问题,很多编程语言都引入了锁机制。
锁.png

那么,Java锁机制是如何设计的呢?

在谈锁之前,我们需要简单了解一些Java虚拟机内存结构的知识。如下图所示,JVM运行时的内存结构主要包含了程序计数器、JVM栈、Native方法栈、堆、方法区。红色的区域是各个线程所私有的,这些区域的数据,不会出现线程竞争的问题,而蓝***域的数据被所有线程所共享。其中,Java堆中存放的是所有对象,方法区中存放类信息、常量、静态变量等数据。并发环境下,需要锁机制对其限制,确保共享区域内的数据正确性。

素材0.png

2.Java代码中锁的实现

在Java中,每个对象Object都拥有一把锁,这把锁存放在对象头中,锁中记录了当前对象被哪个线程所占用。
下图是对象的结构,其中,对象头存放了一些对象本身的运行时信息,对象头包含两部分,Mark word和Class point,相较于实例数据,对象头属于一些额外的存储开销,所以它被设计的极小(32bit)来提高效率。

Java对象结构.png

  • Class point是一个指针,它指向了当前对象类型所在方法区中的类型数据。
  • Mark Word存储了很多和当前对象运行时状态有关的数据,比如hashcode,锁状态标志,指向锁记录的指针等,如下图所示。其中,最主要的就是四种锁状态,这四种锁又是什么呢?往下看。

素材3.png

3.synchronized关键字

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。

为什么呢?

因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

3.1 synchronized 关键字的底层原理

看下面的例子,num为共享变量,用synchronized标记的同步代码块去更改共享变量不会出问题,那么底层原理是什么呢?

TestSync.java

package com.conghuhu.sync;

/**
 * @author conghuhu
 * @create 2021-10-21 19:04
 */
public class TestSync {
    private int num = 0;
    public void test(){
        for(int i=0; i<800; i++){
            synchronized (this){
                System.out.println("thread:" + Thread.currentThread().getId()+", num:" + num++);
            }
        }
    }
}

main.java

package com.conghuhu;
import com.conghuhu.sync.TestSync;
/**
 * @author conghuhu
 * @create 2021-10-20 18:51
 */
public class main {
    public static void main(String[] args) {
        TestSync testSync = new TestSync();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                testSync.test();
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                testSync.test();
            }
        });
        t1.start();
        t2.start();
    }
}

我们通过 JDK 自带的 javap 命令查看 TestSync 类的相关字节码信息,如下图所示:

素材1.png

素材2.png

从上面我们可以看出,synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

Monitor:管程/监视器

如下图所示,首先,Entry Set中聚集了一些想要进入monitor的线程,处于waiting状态。假设某个线程A经过2进入monitor,那么它被标记为actived状态,激活。当A由于某个原因执行3,暂时让出执行权,那么它将进入Wait Set,状态也被标记为waiting,此时entry set中的线程就有机会进入monitor。
素材4.png
假设某个线程B进入monitor,并顺利执行,那么它可以通过notify的形式来唤醒wait set中的线程A,线程A再次进入monitor ,执行完后,便可以退出。

这就是synchronized的同步机制

3.2 synchronized的性能问题

上文我们知道,synchronized 同步语句块的实现使用的是 monitorentermonitorexit 两个字节码指令,而monitor是依赖于操作系统的mutex lock来实现的。Java线程实际上是对操作系统线程的映射,所以每当挂起或者唤醒一个线程,都要去切换操作系统用户态内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

所以,使用synchronized将会对程序的性能产生很严重的影响。

但是,从Java6开始,synchronized进行了优化,引入了偏向锁轻量级锁

3.3 synchronized优化

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

继续看下文四种锁状态,你就知道synchronized是怎么优化的了。

4.锁的四种状态

4.1 无锁

无锁,就是对资源没有锁定,所有线程都能够访问到同一资源。

有两种情况满足无锁:

  • 第一种:某个对象不会出现在多线程的环境下,或者说即使出现了多线程环境下也不会出现竞争的情况。此时无须对这个对象进行任何保护。

  • 第二种:资源会被竞争,但是我们不想对资源进行锁定,还是想通过某些机制来控制多线程。比如说,如果有多个线程想要修改同一个值,通过某种方式限制,只有一个线程能修改成功,其他修改失败的线程将会不断重试,直到修改成功,这就是耳熟能详的CASCAS在操作系统中通过一条指令实现,原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值,所以它能保证原子性,通过诸如CAS这种方式,我们可以实现无锁编程。

大部分情况下,无锁的效率是很高的。

4.2 偏向锁

假设一个对象A被加锁了,但在实际运行中只有一个线程会获取这个对象锁,我们最理想的方式,不通过线程切换,也不通过CAS来获得锁,因为这两种多多少少会耗费一些资源。我们设想的是,最好对象能够认识这个线程,只要这个对象过来,那么对象直接把锁交出去,我们就认为这个锁偏爱这个线程,也就是偏向锁

偏向锁的实现:

  1. 在对象头的Mark Word中,当锁标志位为01时,去判断倒数第三个bit是否为1,为1当前对象的锁状态就是偏向锁,否则为无锁。
  2. 确认当前状态为偏向锁,于是再去读前23个bit,这个值就是线程ID
  3. 通过线程ID来测试是否指向当前想要获得对象锁的这个线程,如果是,直接执行步骤6。如果不是继续步骤4.
  4. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行6;如果竞争失败,执行5。
  5. 如果CAS获取偏向锁失败,则表示有竞争,偏向锁升级为轻量级锁。
  6. 交出锁,可以访问对象的资源

素材3.png

4.3 轻量级锁

素材0.png

当一个线程想要获得某个对象的锁时,假如看到锁标志位为00,那么就知道它是轻量级锁,这时线程会在虚拟机栈中开辟一块被称为Lock Record的空间。
Lock Record中存的是对象头中Mark word的副本以及ownner指针。线程通过CAS去尝试获取锁,一旦获得那么将会赋值该对象头中的Mark word到Lock Record中,并且将Lock Record中的ownner指针指向该对象,另一方面,对象的Mark Word的前30个bit,将会生成一个指针,指向线程虚拟机栈中的Lock Record。
这样就实现了线程和对象锁的绑定,获取了锁的线程就可以执行同步代码块了。如果此时还有其他线程想要获取这个对象,将自旋等待。
自旋,可以理解为轮询,线程不断尝试着去看一下目标对象的锁有没有被释放。如果释放,那么就获取,如果没有释放就进入下一轮循环。这种方式区别于被操作系统挂起阻塞,因为对象的锁很快被释放的话,自旋就不需要进行系统中断和现场恢复,效率更高。
自旋,相当于CPU空转,如果长时间自旋会浪费CPU资源,于是出现了一种叫做适应性自旋的优化。简单来说,就是自旋的时间不再固定了,而是由上一次,在同一个锁上的自旋时间以及锁状态,这两个条件来进行决定。

假如此时有一个线程在自旋等待,又有其他线程同时也来请求资源,自旋等待。一旦自旋等待的线程数>1,那么轻量级锁将升级为重量级锁

4.4 重量级锁

synchronized在早期Java版本中,就是重量级锁,也就是上文提到的moniter对线程进行控制,此时将完全锁定资源,对线程的管控最为严格。
素材4.png

总结

本文主要介绍了几个问题:

  • 什么是锁?
  • 对象头
  • Mark Word
  • synchronized
  • monitor
  • 四种锁状态:无锁、偏向锁、轻量锁

参考资料:寒食君哔站视频

发表于 2021-10-27 09:12:48 回复(0)

Java虚拟机中利用监视器实现同步功能,当方法或代码块有synchronized关键字时就会和监视器关联,监视器会确保该代码在执行时不会与其他操作竞争。

发表于 2020-02-24 21:28:56 回复(0)
使用代码同步块
发表于 2020-08-30 11:18:22 回复(0)

监视器和锁是一起在jvm中使用的,监视器监视着一个同步代码块,确保一次一个同步代码块只被一个线程执行。每个监视器个一个对象引用相关联。每个线程获得所之前不允许执行同步代码块

编辑于 2020-03-27 17:44:40 回复(0)

监视器和锁在jvm中是一块使用的

监视器监视一块区域 在没有获取到锁之前不允许其他线程进入

每个监视器关联着一个对象(object或者class)引用 为了实现监视器互斥又都关联了一个锁


lock 显式监视器

synchronized 隐式监视器 被其修饰就会该区域代码就会被监视器监视

编辑于 2019-12-30 10:20:21 回复(0)
监视器
发表于 2019-10-14 17:24:23 回复(0)

对象和类关联监视器,为了实现互斥功能,每个对象都关联一把锁,一旦方法或代码块被synchronized修饰,那么就进入了监听区域,确保只有一个线程执行该部分的代码

编辑于 2019-10-13 16:38:24 回复(0)

每个监视器都和一个对象相关联,每个对象都有过内置锁,监视器和锁是一起使用的,当代码块被synchronized修饰后,监视器就会监视该代码块,该代码块一次只能被一个线程调用,只有获得锁的线程才能调用

编辑于 2019-09-16 19:55:06 回复(0)

监视器和锁在虚拟机中是一起使用的 监视器监视同步代码块中的代码,以确保在同一时刻之后一个线程可以进入

编辑于 2019-09-05 09:53:26 回复(0)
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
发表于 2019-08-18 17:00:13 回复(0)

监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取之前不允许执行同步代码。

发表于 2019-08-16 14:24:56 回复(0)

在java中每个对象都有一把隐形锁,***和锁相关联。

发表于 2019-08-15 13:10:40 回复(0)
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。本质都是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器
发表于 2019-04-30 22:30:51 回复(0)
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
发表于 2019-04-28 15:46:52 回复(0)