diff --git "a/week_03/49/Synchronized\350\247\243\346\236\220" "b/week_03/49/Synchronized\350\247\243\346\236\220" new file mode 100644 index 0000000000000000000000000000000000000000..e45ef9486752949353e7fcf4b505debf6a6a7513 --- /dev/null +++ "b/week_03/49/Synchronized\350\247\243\346\236\220" @@ -0,0 +1,48 @@ +一、Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个: +(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。 + +二、Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: +1、普通同步方法,锁是当前实例对象 +2、静态同步方法,锁是当前类的class对象 +3、同步方法块,锁是括号里面的对象 + +三、synchronize底层原理: +Java 虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。 +在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的, +而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的。 +同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。 +任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。 +线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁; +四、Java虚拟机对synchronize的优化: +锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。 +1、偏向锁: + +偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得, +因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。 +偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, +无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。 +所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。 +但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失, +需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 + +2、轻量级锁 + +倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。 +轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。 +需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。 + +3、自旋锁 + +轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长, +如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态, +这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁, +因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环, +在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。 +最后没办法也就只能升级为重量级锁了。 + +4、锁消除 + +消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译), +通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间, +比如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用, +因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除 diff --git "a/week_03/49/volatile\350\247\243\346\236\220" "b/week_03/49/volatile\350\247\243\346\236\220" new file mode 100644 index 0000000000000000000000000000000000000000..621576c0335dd716a9eab9d10d1cbed33b62eb7d --- /dev/null +++ "b/week_03/49/volatile\350\247\243\346\236\220" @@ -0,0 +1,32 @@ +一、volatile的特性 +一个volatile变量自身具有以下三个特性: +1、可见性:即当一个线程修改了声明为volatile变量的值,新值对于其他要读该变量的线程来说是立即可见的。 +而普通变量是不能做到这一点的,普通变量的值在线程间传递需要通过主内存来完成。 +2、有序性:volatile变量的所谓有序性也就是被声明为volatile的变量的临界区代码的执行是有顺序的,即禁止指令重排序。 +3、受限原子性:这里volatile变量的原子性与synchronized的原子性是不同的,synchronized的原子性是指只要声明为synchronized的方法或代码块儿在执行上就是原子操作的。 +而volatile是不修饰方法或代码块儿的,它用来修饰变量,对于单个volatile变量的读/写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。 +所以volatile的原子性是受限制的。并且在多线程环境中,volatile并不能保证原子性。 +(概况起来简单来说就是两大作用:1、内存可见性。2、禁止指令重排序) +二、 volatile写-读的内存语义 +volatile写的内存语义:当写线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。 +volatile读的内存语义:当读线程读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量。 +三、volatile语义实现原理: +1、volatile可见性实现原理: +olatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则: +①写volatile时处理器会将缓存写回到主内存。 +②一个处理器的缓存写回到内存会导致其他处理器的缓存失效 +2、volatile有序性的实现原理 +volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。 +那么禁止指令重排序又是如何实现的呢?答案是加内存屏障。JMM为volatile加内存屏障有以下4种情况: +①在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。 +②在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。 +③在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。 +④在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。 +上述内存屏障的插入策略是非常保守的,比如一个volatile的写操作后面需要加上StoreStore和StoreLoad屏障,但这个写volatile后面可能并没有读操作, +因此理论上只加上StoreStore屏障就可以,的确,有的处理器就是这么做的。但JMM这种保守的内存屏障插入策略能够保证在任意的处理器平台,volatile变量都是有序的。 +四、总结: +小结: +1、Java重排序的前提:在不影响 单线程运行结果的前提下进行重排序。也就是在单线程环境运行,重排序后的结果和重排序之前按代码顺序运行的结果相同。 +2、指令重排序对单线程没有什么影响,它不会影响程序的运行结果,反而会优化执行性能,但会影响多线程的正确性。 +3、Java因为指令重排序,优化我们的代码,让程序运行更快,也随之带来了多线程下,指令执行顺序的不可控。 +4、volatile的底层是通过lock前缀指令、内存屏障来实现的。