美文网首页
10、synchronized与volatile

10、synchronized与volatile

作者: 满城风絮1 | 来源:发表于2021-08-03 09:35 被阅读0次

    1、可见性与原子性

    可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

    要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。

    原子性

    原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后,

    那么线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++至少包含了三个CPU指令!

    2、volatile详解

    volatile特性

    可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

    10.2.1 volatile特性

    可以看成

    10.2.2 volatile特性

    所以volatile变量自身具有下列特性:

    1)、可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

    2)、原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

    volatile虽然能保证执行完及时把变量刷到主内存中,但对于count++这种非原子性、多指令的情况,由于线程切换,线程A刚把count=0加载到工作内存,线程B就可以开始工作了,这样就会导致线程A和B执行完的结果都是1,都写到主内存中,主内存的值还是1不是2

    volatile的实现原理

    volatile关键字修饰的变量会存在一个“lock:”的前缀。

    Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

    同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

    volatile能否保证线程安全?在DCL上的作用是什么?

    不能保证,在DCL的作用是:volatile是会保证被修饰的变量的可见性和 有序性,保证了单例模式下,保证在创建对象的时候的执行顺序一定是

    1.分配内存空间

    2.实例化对象instance

    3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null了

    的步骤,从而保证了instance要么为null 要么是已经完全初始化好的对象。

    3、synchronized的实现原理

    Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

    对同步块,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁:

    1)、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

    2)、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

    3)、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

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

    synchronized使用的锁是存放在Java对象头里面,Java对象的对象头由 mark word 和  klass pointer 两部分组成:

    1)、mark word存储了同步状态、标识、hashcode、GC状态等等。

    2)、klass pointer存储对象的类型指针,该指针指向它的类元数据

    另外对于数组而言还会有一份记录数组长度的数据。

    10.3.1 Java对象头

    锁信息则是存在于对象的mark word中,MarkWord里默认数据是存储对象的HashCode等信息:

    10.3.2 所信息

    但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式

    10.3.3 Java中的锁

    sychronied修饰普通方法和静态方法的区别?

    对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

    Synchronized做了哪些优化 

    引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析

    等技术来减少锁操作的开销。

    逃逸分析

    如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化:

    同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。

    锁消除和粗化

    锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。

    锁粗化:将临近的代码块用同一个锁合并起来。

    消除无意义的锁获取和释放,可以提高程序运行性能。

    4、volatile和synchronize 的区别

    volatile是最轻量的同步机制。

    volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。

    关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

    5、Synchronized的原理以及与ReentrantLock的区别

    synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。

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

    ReentrantLock 原理:任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,使用nonfairTryAcquire方法增加了再次获取同步状态(state)的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。

    如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

    相关文章

      网友评论

          本文标题:10、synchronized与volatile

          本文链接:https://www.haomeiwen.com/subject/ykgjvltx.html