锁从宏观上分类,只分为两种:悲观锁与乐观锁。java
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。Java中的乐观锁基本都是经过CAS操做实现的,CAS是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。git
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock自旋锁。程序员
Java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统介入,须要在户态与核心态之间切换,这种切换会消耗大量的系统资源,由于用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做。数组
synchronized会致使争用不到锁的线程进入阻塞状态,因此说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。缓存
明确java线程切换的代价,是理解java中各类锁的优缺点的基础之一。安全
自旋锁原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。网络
可是线程自旋是须要消耗cup的,说白了就是让cup在作无用功,若是一直获取不到锁,那线程也不能一直占用cup自旋作无用功,因此须要设定一个自旋等待的最大时间。多线程
若是持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会致使其它争用锁的线程在最大等待时间内仍是获取不到锁,这时争用线程会中止自旋进入阻塞状态。并发
自旋锁尽量的减小线程的阻塞,这对于锁的竞争不激烈,且占用锁时间很是短的代码块来讲性能能大幅度的提高,由于自旋的消耗会小于线程阻塞挂起再唤醒的操做的消耗,这些操做会致使线程发生两次上下文切换!app
可是若是锁的竞争激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,由于自旋锁在获取锁前一直都是占用cpu作无用功,占着XX不XX,同时有大量线程在竞争一个锁,会致使获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操做的消耗,其它须要cup的线程又不能获取到cpu,形成cpu的浪费。因此这种状况下咱们要关闭自旋锁;
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁当即进行处理。可是如何去选择自旋的执行时间呢?若是自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响总体系统的性能。所以自旋的周期选的额外重要!
JVM对于自旋周期的选择,jdk1.5这个限度是必定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷状况作了较多的优化
若是平均负载小于CPUs则一直自旋
若是有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
若是正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
若是CPU处于节电模式则中止自旋
自旋时间的最坏状况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
自旋时会适当放弃线程优先级之间的差别
JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
在JDK1.5以前都是使用synchronized关键字保证同步的,Synchronized的做用相信你们都已经很是熟悉了;
它能够把任意一个非NULL的对象看成锁。
实现以下图所示;
它有多个队列,当多个线程一块儿访问某个对象监视器的时候,对象监视器会将这些线程存储在不一样的容器中。
Contention List:竞争队列,全部请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
Owner:当前已经获取到所资源的线程被称为Owner;
!Owner:当前释放锁的线程。
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),可是并发状况下,ContentionList会被大量的并发线程进行CAS访问,为了下降对尾部元素的竞争,JVM会将一部分线程移动到EntryList中做为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(通常是最早进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck须要从新竞争锁。这样虽然牺牲了一些公平性,可是能极大的提高系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有获得锁资源的仍然停留在EntryList中。若是Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻经过notify或者notifyAll唤醒,会从新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操做系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,若是获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,若是在运行过程当中,同步锁只有一个线程访问,不存在多线程争用的状况,则线程是不须要触发同步的,这种状况下,就会给线程加一个偏向锁。
若是在运行过程当中,遇到了其余线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它经过消除资源无竞争状况下的同步原语,进一步提升了程序的运行性能。
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤5,不然进入步骤3。
若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行5;若是竞争失败,执行4。
若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会致使stop the word)
执行同步代码。
注意:第四步中到达安全点safepoint会致使stop the word,时间很短。
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
始终只有一个线程在执行同步块,在它没有执行完释放锁以前,没有其它线程去执行同步块,在锁无竞争的状况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候须要撤销偏向锁,撤销偏向锁的时候会致使stop the word操做;
在有锁的竞争时,偏向锁会多作不少额外操做,尤为是撤销偏向所的时候会致使进入安全点,安全点会致使stw,致使性能降低,这种状况下应当禁用;
要查看安全点停顿,能够打开安全点日志,经过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统中止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,能够查看到使用偏向锁致使的停顿,时间很是短暂,可是争用严重的状况下,停顿次数也会很是多;
注意:安全点日志不能一直打开:
1. 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件若是不在/dev/shm,可能被锁。
2. 对于一些很短的停顿,好比取消偏向锁,打印的消耗比停顿自己还大。
3. 安全点日志是在安全点内打印的,自己加大了安全点的停顿时间。
因此安全日志应该只在问题排查时打开。
若是在生产系统上要打开,再再增长下面四个参数:
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。
此日志分三部分:
第一部分是时间戳,VM Operation的类型
第二部分是线程概况,被中括号括起来
total: 安全点里的总线程数
initially_running: 安全点开始时正在运行状态的线程数
wait_to_block: 在VM Operation开始前须要等待其暂停的线程数
第三部分是到达安全点时的各个阶段以及执行操做所花的时间,其中最重要的是vmop
可见,那些不少但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程:
在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
拷贝对象头中的Mark Word复制到锁记录中;
拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤4,不然执行步骤5。
若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。
释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,以前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候若是它发如今它持有锁的期间有其余线程来尝试获取锁了,而且该线程对markword作了修改,二者比对发现不一致,则切换到重量锁。
由于重量级锁被修改了,全部display mark word和原来的markword不同了。
怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其余线程持有。
此时若是线程已经释放了markword,那么经过CAS后就能够直接进入线程,无需进入mutex,就这个做用。
尝试获取锁线程视角:若是线程尝试获取锁的时候,轻量锁正被其余线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。
还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。
这就是自旋锁,尝试获取锁的线程,在没有得到锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,若是尚未得到锁,则才被挂起,得到锁,则执行代码。
synchronized的执行过程:
1. 检测Mark Word里面是否是当前线程的ID,若是是,表示当前线程处于偏向锁
2. 若是不是,则使用CAS将当前线程的ID替换Mard Word,若是成功则表示当前线程得到偏向锁,置偏向标志位1
3. 若是失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,若是成功,当前线程得到锁
5. 若是失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁。
6. 若是自旋成功则依然处于轻量级状态。
7. 若是自旋失败,则升级为重量级锁。
上面几种锁都是JVM本身内部实现,当咱们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用状况,决定如何执行同步操做;
在全部的锁都启用的状况下线程进入临界区时会先去获取偏向锁,若是已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,若是自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;
偏向锁是在无锁争用的状况下使用的,也就是同步开在当前线程没有执行完以前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,若是轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;
若是线程争用激烈,那么应该禁用偏向锁。
虽然对于以上介绍的锁都不是咱们代码中可以控制的,可是借鉴上面的思想,咱们能够优化咱们本身线程的加锁操做。
不须要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可让锁尽快释放。
它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增长并行度,从而下降锁竞争。它的思想也是用空间来换时间。
大部分状况下咱们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度。
在如下场景下须要粗化锁的粒度:
假若有一个循环,循环内的操做须要加锁,咱们应该把锁放到循环外面,不然每次进出循环,都进出一次临界区,效率是很是差的。
ReentrantReadWriteLock 是一个读写锁,读操做加读锁,能够并发读,写操做使用写锁,只能单线程写。
CopyOnWriteArrayList 、CopyOnWriteArraySet
CopyOnWrite容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此CopyOnWrite容器也是一种读写分离的思想,读和写不一样的容器。
CopyOnWrite并发容器用于读多写少的并发场景,由于,读的时候没有锁,可是对其进行更改的时候是会加锁的,不然会致使多个线程同时复制出多个副本,各自修改各自的。
若是须要同步的操做执行速度很是快,而且线程竞争并不激烈,这时候使用CAS效率会更高,由于加锁会致使线程的上下文切换,若是上下文切换的耗时比同步操做自己更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操做会是很是高效的选择;
除了咱们在代码中使用的同步锁和jvm本身内置的同步锁外,还有一种隐藏的锁就是缓存行,它也被称为性能杀手。
在多核cup的处理器中,每一个cup都有本身独占的一级缓存、二级缓存,甚至还有一个共享的三级缓存,为了提升性能,cpu读写数据是以缓存行为最小单元读写的;32位的cpu缓存行为32字节,64位cup的缓存行为64字节,这就致使了一些问题。
例如,多个不须要同步的变量由于存储在连续的32字节或64字节里面,当须要其中的一个变量时,就将它们做为一个缓存行一块儿加载到某个cup-1私有的缓存中(虽然只须要一个变量,可是cpu读取会以缓存行为最小单位,将其相邻的变量一块儿读入),被读入cpu缓存的变量至关因而对主内存变量的一个拷贝,也至关于变相的将在同一个缓存行中的几个变量加了一把锁,这个缓存行中任何一个变量发生了变化,当cup-2须要读取这个缓存行时,就须要先将cup-1中被改变了的整个缓存行更新回主存(即便其它变量没有更改),而后cup-2才可以读取,而cup-2可能须要更改这个缓存行的变量与cpu-1已经更改的缓存行中的变量是不同的,因此这至关于给几个绝不相关的变量加了一把同步锁;
为了防止伪共享,不一样jdk版本实现方式是不同的:
1. 在jdk1.7以前会 将须要独占缓存行的变量先后添加一组long类型的变量,依靠这些无心义的数组的填充作到一个变量本身独占一个缓存行;
2. 在jdk1.7由于jvm会将这些没有用到的变量优化掉,因此采用继承一个声明了好多long变量的类的方式来实现;
3. 在jdk1.8中经过添加sun.misc.Contended注解来解决这个问题,若要使该注解有效必须在jvm中添加如下参数:
-XX:-RestrictContended
sun.misc.Contended注解会在变量前面添加128字节的padding将当前变量与其余变量进行隔离;
volatile是Java中的轻量级同步机制,使用volatile能够保持内存可见性和防止指令重排序。
内存可见性是指全部线程都能看到共享内存的最新状态。
在Java内存模型中,分为栈内存(线程私有)和堆内存(线程共享),Java中的内存模型依赖于硬件的存储模型。
Java内存模型和硬件存储模型的大体关系以下图所示(图片来源于网络):
而对于多个线程共享的变量,Java内存模型规定,变量存储在主内存当中,每一个线程都有本身独立的工做内存(好比CPU的寄存器),线程只能访问本身的工做内存,不能够访问其它线程的工做内存。
工做内存中保存了主内存共享变量的副本,线程要操做这些共享变量,只能经过操做工做内存中的副原本实现,操做完毕以后再同步回到主内存当中。
如何保证多个线程操做主内存的数据完整性是一个难题,Java内存模型也规定了工做内存与主内存之间交互的协议,定义了8种原子操做:
(1) lock:将主内存中的变量锁定,为一个线程所独占
(2) unclock:将lock加的锁定解除,此时其它的线程能够有机会访问此变量
(3) read:将主内存中的变量值读到工做内存当中
(4) load:将read读取的值保存到工做内存中的变量副本中
(5) use:将值传递给线程的代码执行引擎
(6) assign:将执行引擎处理返回的值从新赋值给变量副本
(7) store:将变量副本的值存储到主内存中
(8) write:将store存储的值写入到主内存的共享变量当中
大体过程以下图所示(图片来源于网络):
经过上面Java内存模型的描述,咱们会注意到这么一个问题,每一个线程在获取锁以后会在本身的工做内存来操做共享变量,操做完成以后将工做内存中的副本回写到主内存,而且在其它线程从主内存将变量同步回本身的工做内存以前,共享变量的改变对其是不可见的。即其余线程的本地内存中的变量已是过期的,并非更新后的值,也就是说其余线程本地内存中保持的是失效数据。
好比以下代码是一个可变整数类:
MutableInteger.java
public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }
MutableInteger
不是线程安全的,由于get
和set
方法都是在没有同步的状况下进行的。若是线程1调用了set方法,那么正在调用的get的线程2可能会看到更新后的value值,也有可能看不到,这就是数据失效问题。
要解决这个问题其实也很简单,只须要将value
声明为volatile
变量便可。
private volatile int value;
volatile的特殊规则就是:
因此,使用volatile变量可以保证:
也就是说,使用volatile关键字修饰的变量看到的随时都是本身的最新值。线程1中对变量value的最新修改,对线程2是可见的。这是由于每当修改本地内存变量的值时,在将更新同步到主内存的同时还会根据MESI清除其余线程中本地变量副本值,迫使变量副本从新同步为主内存的最新值。
指令重排序是JVM为了优化指令,提升程序运行效率,在不影响单线程程序执行结果的前提下,尽量地提升并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的状况下指令重排序就会给程序员带来问题。
不一样的指令间可能存在数据依赖。好比下面计算圆的面积的语句:
double r = 2.5d; //(1)
double pi =3.1415926; //(2)
double area = pi* r * r; //(3)
area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。
as-if-serial语义是指:无论如何重排序(编译器与处理器为了提升并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵照的语义。
虽然,(1) - happensbefore -> (2),(2) - happens before -> (3),可是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并没有区别。编译器、Runtime在优化时能够根据状况重排序(1)与(2),而丝绝不影响程序的结果。
指令重排序包括编译器重排序和运行时重排序。
对于在同一个线程内,这样的改变是不会对逻辑产生影响的,可是在多线程的状况下,指令重排序会带来一些问题。
咱们来看下面这个情景:
在线程A中:
context = loadContext(); init = true;
在线程B中:
while(!init) { // 根据线程A中对init变量的修改决定是否使用context变量 sleep(1000); } doSomethingWithConfig(context);
假设此时在线程A中发生了指令重排序:
init = true; context = loadContext();
那么B中极可能就会拿到一个还没有初始化或还没有初始化完成的context,从而引起程序错误。
以下,是一个懒加载的单例模式,在单线程中这个单例是没有问题的,可是在多线程中,竞态条件
会致使instance
引用被屡次赋值,使用户获得两个不一样的单例。
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() { if ( instance == null ) { // 这里存在竞态条件 instance = new Singleton(); } return instance; } }
为了解决这个问题,可使用synchronized
关键字将getInstance
方法改成同步方法;但这样串行化的单例效率是很低下的。因此就有前辈设计了DCL
(Double Check Lock,双重检查锁)机制,使得大部分请求都不会进入阻塞代码块。
class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { // 当instance不为null时,仍可能指向一个“被部分初始化的对象” synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
“看起来”很是完美:既减小了阻塞,又避免了竞态条件。不错,但实际上仍然存在一个问题——当instance不为null时,仍可能指向一个"被部分初始化的对象"
。
问题出在这行简单的赋值语句:
instance = new Singleton();
它并非一个原子操做。事实上,它能够”抽象“为下面几条JVM指令:
memory = allocate(); // 1:分配对象的内存空间 initInstance(memory); // 2:初始化对象 instance = memory; // 3:设置instance指向刚分配的内存地址
上面操做2依赖于操做1,可是操做3并不依赖于操做2,因此JVM能够以“优化”为目的对它们进行重排序
,通过重排序后以下:
memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化) ctorInstance(memory); //2:初始化对象
能够看到指令重排以后,操做 3 排在了操做 2 以前,即引用instance指向内存memory时,而这段崭新的内存尚未初始化,即引用instance指向了一个"被部分初始化的对象"。此时,若是另外一个线程调用getInstance方法,因为instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户获得了没有完成初始化的“半个”单例。
解决这个问题也很简单,只须要将instance声明为volatile变量便可。
private static volatile Singleton instance;
volatile关键字是经过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
大多数的处理器都支持内存屏障的指令。但对于编译器来讲,发现一个最优布置来最小化插入屏障的总数是不太可能的,为此,Java内存模型采起了保守的策略。
下面是基于保守策略的JMM内存屏障插入策略:
1.volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操做,也就不会执行线程阻塞,所以volatilei变量是一种比synchronized关键字更轻量级的同步机制。
2.因为使用volatile屏蔽掉了JVM中必要的代码优化,因此在效率上比较低,所以必定在必要时才使用此关键字。在两个或者更多的线程须要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用volatile。
3.加锁机制(即同步机制)既能够确保可见性又能够确保原子性,而volatile变量只能确保可见性,缘由是声明为volatile的简单变量若是当前值与该变量之前的值相关,那么volatile关键字不起做用,也就是说以下的表达式都不是原子操做:“count++”、“count = count+1”。
4.在须要同步的时候,第一选择应该是synchronized关键字,这是最安全的方式,尝试其余任何方式都是有风险的。尤为在、jdK1.5以后,对synchronized同步机制作了不少优化,如:自适应的自旋锁、锁粗化、锁消除、轻量级锁等,使得它的性能明显有了很大的提高。
一、volatile不须要加锁,比synchronized更轻量级,不会阻塞线程;而且volatile只能修饰变量,而synchronized能够修饰方法,以及代码块。
二、从内存可见性角度看,volatile读至关于加锁,volatile写至关于解锁。
三、synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,没法保证原子性。
四、关键字volatile解决的是变量在多个线程之间的可见性问题,而synchronized关键字解决的是多个线程之间访问资源的同步性。
一、sleep是Thread的静态方法、wait是Object的方法;
二、sleep不释放锁对象,wait放弃锁对象;
三、sleep暂停线程,但监控状态仍然保持,结束后自动恢复;
四、wait、notify和notifyAll只能在同步控制方法控制块里面使用,而sleep能够在任意地方使用;
五、wait方法致使线程放弃对象锁,只有针对此对象发出notify(或notifyAll)后才进入对象锁定池准备从新得到对象锁,而后进入就绪状态,准备运行。
sleep方法属于Thread类中方法,表示让一个线程进入睡眠状态,等待必定的时间以后,自动醒来进入到可运行状态,不会立刻进入运行状态,由于线程调度机制恢复线程的运行也须要时间,一个线程对象调用了sleep方法以后,并不会释放他所持有的全部对象锁,因此也就不会影响其余进程对象的运行。但在sleep的过程当中有可能被其余对象调用它的interrupt(),产生InterruptedException异常,若是你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,若是你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及之后的代码。
注意sleep()方法是一个静态方法,也就是说他只对当前对象有效,经过t.sleep()让t对象进入sleep,这样的作法是错误的,它只会是使当前线程被sleep 而不是t线程。
wait属于Object的成员方法,一旦一个对象调用了wait方法,必需要采用notify()或notifyAll()方法唤醒该进程;若是线程拥有某个或某些对象的同步锁,那么在调用了wait()后,这个线程就会释放它持有的全部同步资源,而不限于这个被调用了wait()方法的对象。wait()方法也一样会在wait的过程当中有可能被其余对象调用interrupt()方法而产生InterruptedException异常。
其实二者均可以让线程暂停一段时间,可是本质的区别是sleep是线程的运行状态控制,wait是线程之间的通信问题。sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定。比如如说,我要作的事情是"点火->烧水->煮面",而当我点完火以后我不当即烧水,我要休息一段时间再烧。对于运行的主动权是由个人流程来控制。而wait(),首先,这是由某个肯定的对象来调用的,将这个对象理解成一个传话的人,当这我的在某个线程里面说"暂停!",也是 thisOBJ.wait(),这里的暂停是阻塞。仍是"点火->烧水->煮饭",thisOBJ就比如一个监督个人人站在我旁边,原本该线程应该执行1后执行2,再执行3,而在2处被那个对象喊暂停,那么我就会一直等在这里而不执行3,但正个流程并无结束,我一直想去煮饭,但还没被容许,直到那个对象在某个地方说"通知暂停的线程启动!",也就是thisOBJ.notify()的时候,那么我就能够煮饭了,这个被暂停的线程就会从暂停处继续执行。
码云:https://gitee.com/liuge1988/java-demo.git
做者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/ 版权全部,欢迎转载,转载请注明原文做者及出处。