锁概述内锁:synchronized显式锁:LockMemoryBarrier轻量级同步机制:volatile关键字单例模式线程安全问题CASstatic和Final锁概述一个线程在访问共享数据时必须申请获得相应的锁(相当于一个license),线程只有在获得相应的“license”后才能访问共享数据。一个“许可证”只能同时被一个线程访问。访问之后,线程需要释放对应的锁(returnthelicense),方便其他线程访问。从申请锁到释放锁的代码称为临界区。Internallock:synchronizeddisplaylock:ReentrantLock可见性由写线程刷新到处理器缓存和由读线程刷新处理器缓存通过两个动作来保证。使用锁时,获取锁前会刷新处理器缓存,释放锁后会刷新处理器缓存。虽然锁可以保证顺序,但是临界区中的操作仍然有可能被重新排序,因为临界区中的操作对于其他线程是不可见的,这意味着即使临界区中的操作会被重新排序,它们不会造成订单问题。可重入性:a线程在拥有锁的情况下能否继续获取锁?如果是这样,我们称该锁为可重入锁voidmetheadA(){acquireLock(lock);//申请锁//省略其他代码methodB();释放锁(锁);//释放锁}voidmetheadB(){acquireLock(lock);//申请锁//省略其他代码releaseLock(lock);//释放锁}lockleak:获取锁后,一直没有释放。内部锁:synchronized内部锁用于同步代码块synchronized(lock){......}===============================同步方法publicsynchronizedvoidmethod(){......}等价于publicvoidmethod(){synchronized(this){......}}=================================同步静态方法类示例{publicsynchronizedstaticvoidmethod(){...}}相当于classExample{publicstaticvoidmethod(){synchronized(Example.class){......}}}内部锁不会造成锁泄漏,因为java编译器(javac)会在同步的时候代码块被编译成字节码,对可能在临界区抛出但没有被程序代码捕捉到的异常进行特殊处理,这样即使临界区代码块抛出异常也不会影响内部锁.正常释放。java虚拟机为每一个内部锁维护一个入口集(EntrySet),以维护申请这把锁的等待线程集合。当多个线程同时申请锁时,只有一个线程会申请成功,其他线程都会申请失败。它不会抛出异常,而是会被挂起(变成Blocked状态)等待,并存储在这个入口集中。拥有锁的线程执行完毕后,java虚拟机会从入口集中随机唤醒一个Blocked线程。去申请这把锁,但是它不一定能够获取到这把锁,因为这时候它可能会面临其他新的活动线程(Runnable)去竞争这把锁。显式锁:Lockinternallock只支持非公平锁。显式锁可以同时支持公平锁和非公平锁(默认非公平锁)。公平锁往往会带来额外的开销,因为,为了“公平”的原则,在大多数情况下virtual增加线程切换的机会,这会比非公平锁增加更多的上下文切换所以公平锁适用于线程占用锁时间比较长的任务,以免造成部分线程饿死。锁的使用方法lock.lock()try{......}catch(Exceptione){。.....}finally{lock.unlock()}synchronized和Lock的区别Synchronized是java内置的关键字,属于jvm级别,而Lock是java的一个类。Lock.tryLock()可以尝试获取锁,synchronized不能,Synchronized可以自动释放锁,Lock还得手动解锁。同步是一种非公平锁。锁可以设置为公平或不公平。Lock适用于大量同步代码,synchronized适用于少量同步代码。其他读线程获取读锁,写线程不允许获取锁。当写入线程持有锁时,不允许其他线程获取锁。读写锁使用classApple{ReadWriteLocklock=newReentrantReadWriteLock();锁定writeLock=lock.writeLock();锁定readLock=lock.readLock();私有BigDecimal价格;publicdoublegetPrice(){双p;readLock.lock();尝试{p=price.divide(newBigDecimal(100)).doubleValue();}catch(Exceptione){...}finally{readLock.unLock();}返回双;}publicvoidsetPrice(doublep){writeLock.lock();试试{价格=newBigDecimal(p);}catch(Exceptione){...}finally{writeLock.unLock();读写锁适用于以下场景:读操作比写操作频繁读线程持有时间长锁.publicclassReadWriteLockDowngrade{privatefinalReadWriteLockrwLock=newReentrantReadWriteLock();privatefinalLockreadLock=rwLock.readLock();privatefinalLockwriteLock=rwLock.writeLock();publicvoidoperationWithLockDowngrade(){booleanreadLockAcquired=false;写锁.lock();//申请写锁try{//更新共享数据//...//当前线程申请读锁readLock,同时持有写锁readLock.lock();readLockAcquired=true;}最后{writeLock.unlock();//释放写锁}if(readLockAcquired){try{//读取共享数据并根据它执行其他操作//...}finally{readLock.unlock();//释放读锁}}else{//...}}}不支持锁升级的原因——因为有多个线程同时持有读锁,在锁升级过程中可能会出现死锁。假设有两个读线程A和B获取了同一个读锁,那么线程A想升级为写锁,线程B释放读锁后,线程B才能升级成功。但是如果线程A要升级,B也想升级,那么两者都会同时等待对方释放读锁,就会造成对抗的情况,即典型的死锁。Memorybarriermemorybarrier是指两条指令插入一条指令的两边,起到“Barrier”编译器处理器重排序的作用。内锁申请及释放对应字段代码指令是MonitorEnter和MonitorExit。内存屏障按可见性可以分为加载屏障(LoadBarrier)和存储屏障(StoreBarrier)。加载屏障的作用是刷新处理器缓存,存储屏障的作用是刷新处理器缓存。.java虚拟机在MonitorEnter指令后的临界区开始前插入了一个loadingbarrier,以保证其他线程对共享变量的更新能够同步到该线程所在处理器的缓存中。同时,一个存储屏障保证了临界区的代码能够及时同步共享变量的变化。按照顺序,内存屏障可以分为获取屏障(AcquireBarrier)和释放屏障(ReleaseBarrier)。区之前的代码指令重新排序,释放屏障会禁止临界区指令和临界区之后的代码指令重新排序。java虚拟机在MonitorEnter指令后插入一个acquisitionbarrier,在MonitorExit指令前插入一个releasebarrier。内存屏障下排序规则(实线代表可排序,虚线代表不可排序)轻量级同步机制:volatile关键字volatile关键字的作用包括:保证可见性,保证顺序,保证long/double变量的读写操作原子性long和double这两种基本类型的写操作之所以是非原子的,是因为它们在32位的java虚拟机中的写操作被分成了两个32位的操作,所以在java字节码中,一个long或double变量write操作是执行两步字节码指令。volatile变量不会被编译器分配到寄存器中存储,volatile的简写操作就是内存访问。volatile关键字只保证被修饰变量本身读写的原子性。如果要涉及其修饰变量的赋值原子性,那么这个赋值操作就不能涉及任何共享变量,否则它的操作就不是原子的。A=B+1如果A是volatile修饰的共享变量,那么赋值操作实际上是Read-modify-write操作,如果B是共享变量,B可能在赋值过程中已经被修改,所以可能会出现线程安全问题,但是如果B是局部变量,那么赋值操作将是原子的。volatile保证变量读写顺序的原理和synchronized基本相同——在写操作前后添加相关的内存屏障(硬件基础和内存模型文章中的详细内容)。如果volatile被数组修饰,那么volatile只对数组本身的操作起作用,对数组元素的操作不起作用。//nums被volatileintnum=nums[0]修改;//1nums[1]=2;//2volatileint[]newNums=nums;//3比如操作1其实就是两个子步骤①读取数组引用,这个子步骤属于读取操作,其中数组操作是volatile的,所以可以读取到nums数组的相对新值。步骤②是在①的基础上计算偏移量得到nums[0]的值,不是volatile操作,所以不保证读到的是一个比较新的值。操作2可分为①对数组的读操作和②对数组元素的写操作。同样,①是volatile读操作,但是②的写操作可能会导致相应的问题。操作3相当于用一个volatile数组更新另一个volatile数组的引用。所有的操作都是在数组级别进行的,所以不会有并发问题。volatile的开销volatile变量的读写操作不会引起上下文切换,所以volatile的开销比锁小。写入volatile变量使得操作以及它之前的任何写入的结果可同步到其他处理器,因此写入volatile变量的成本介于写入普通变量和写入临界区之间。读取volatile变量的成本也低于读取临界区变量(无锁申请和释放以及上下文切换开销),但其成本可能高于读取普通变量。这是因为volatile变量的值每次都需要从缓存或者主存中读取,不能暂存到寄存器中,访问效率无法发挥。单例模式的线程安全问题下面是典型的双检锁单例实现publicclassSingleton{//保存该类的唯一实例privatestaticSingletoninstance=null;/***私有构造函数防止其他类直接通过new创建该类的实例*/privateSingleton(){//什么都不做}/***获取单例的main方法*/publicstaticSingletongetInstance(){if(null==instance){//操作1:首先检查synchronized(Singleton.class){//操作2if(null==instance){//操作3:第二次检查instance=newSingleton();//操作4}}}returninstance;}}首先我们分析一下操作1和操作2为什么起作用。如果没有操作1和操作2,此时线程1调用getInstance()方法。在执行操作4时,线程2也调用了这个方法。由于操作4还没有执行,线程2可以顺利通过操作3的判断,所以就会出现一个问题,newSingleton()被执行了两次,违背了单例模式的初衷。由于以上问题,在操作3之前加一个操作2,可以保证一次只有一个线程执行操作4。但是这样会导致每次调用getInstance()都要申请/释放锁,会造成极大的性能消耗,所以需要在操作2之前加一个操作1来避免此类问题。另外,static修饰的变量保证只会加载一次。所以看起来这个双重检查锁是完美的?上面的操作4可以分为以下三个子操作objRef=allocate(IncorrectDCLSingletion.class);//子操作1:分配对象所需的存储空间invokeConstructor(objRef);//子操作2:初始化objRef=objRef;引用的对象实例;//子操作3:将对象引用写入共享变量synchronized的临界区允许重新排序,JIT编译器可能会将上述操作重新排序为子操作1→子操作3→子操作2,所以可能会发生的情况是,当一个线程执行重排后的操作4(1→3→2)时,当该线程刚执行完子操作3(子操作2未执行),其他线程执行到操作1,则instance≠null会直接返回,但是这个instance还没有初始化,所以会有问题。如果用volatile关键字修饰实例,就不会出现这种情况。volatile解决了子操作2和子操作3的重排序问题。volatile还可以防止这个共享变量存储在寄存器中,避免可见性问题。另外,静态内部类和枚举类也可以安全的实现单例模式新单例();}publicstaticSingletongetInstance(){returnInstanceHolder.INSTANCE;}}以上是内部静态类的实现,调用时会加载InstanceHolder,所以这也是一个惰性单例。CASCAS是一个轻量级的锁,它的主要实现是通过赋值前的比较,比如i=i+1操作,线程在将i+1的结果赋值给i之前会比较当前i是否相同i的旧值(i+1之前记录的值),如果相同,则认为i在这个过程中没有被其他线程修改过,否则必须丢弃前面的i+1操作再次。这种更新机制是基于CAS操作是一个原子操作,由处理器直接保证。但是CAS只能保证操作的原子性,不能保证操作的可见性(可见性不能保证,顺序自然也不能保证)。CAS可能存在一个ABA问题,即当i初值为0,执行i+1操作时,另一个线程将i变量修改为10,然后第三个线程在此过程中将i修改回0,然后线程在比较的时候发现i还是初始值,就把i+1的运算结果赋值给i,这显然不是我们想要的。解决方法是在操作这个变量的时候加上一个版本号,每次修改,版本都会+1,这样我们就可以清楚的看到这个变量是否被其他线程改过了。常用原子类的实现原理是CAS分组类基本数据类型AtomicInteger、AtomicLong、AtomicBoolean数组类型AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray字段更新器AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater引用类型AtomicReference、AtomicStampedReference、AtomicReference加载后的虚拟类machineArray、AtomicStampedReference,AtomicMarkableReference该类的静态变量的值仍然是默认值(引用类型变量为null,boolean类型为false),静态代??码块和变量不会被初始化,直到一个静态变量被初始化第一次访问。publicclassInitStaticExample{staticclassInitStatic{staticStrings="helloworld";static{System.out.println("初始化....");整数a=100;}}publicstaticvoidmain(String[]args){System.out.println(InitStaticExample.InitStatic.class.getName());System.out.println(InitStatic.s);}}=================结果==================io.github.viscent.mtia.ch3.InitStaticExample$InitStaticinit.....helloworld引用静态变量,任何线程都在到达这个变量时初始化(这个和double-checkedlock的报错不同,虽然实例是静态变量,但是Singleton对象双重检查锁的不是静态类,所以newSingleton()没有初始化的风险)。但是,static的这种可见性和顺序保证仅在线程首次读取静态变量时有效。当一个对象被释放给其他线程时,这个对象中的final变量总是被初始化(也保证引用该变量时是初始化的对象),保证其他线程读取的值不是默认值。final只能解决顺序问题,即保证获取到的变量被初始化,但不能保证可见性。
