Java并发总结

wait()、notify()、notifyAll()

Object是全部类的基类,它有5个方法组成了等待、通知机制的核心:notify()、notifyAll()、wait()、wait(long) 和wait(long,int)。在java中,全部的类都是从Object继承而来,所以,全部的类都拥有这些共同的方法可供使用。java

wait()

public final void wait()  throws InterruptedException,IllegalMonitorStateException

该方法用来将当前线程置入休眠状态,直到接到通知或中断为止。在调用wait()以前,线程必需要得到对象的对象级别的锁,即只能在同步方法或同步代码块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其余线程竞争从新得到锁。若是调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,所以不须要try-catch结构。git

notify()

public final native void notify() throws IllegalMonitorStateException

该方法也要在同步方法或同步代码块中调用,即在调用前,线程也必需要得到该对象的对象级别锁,若是调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。github

该方法用来通知那些等待该对象的对象锁的其余线程。若是有多个线程等待,则线程规划器任意挑选其中一个wait()状态得线程来发出通知,并使它们等待获取该对象的对象锁。(notify后,当前线程不会立刻释放对象锁,wait所在的线程并不能立刻获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程才能够得到该对象锁)。编程

但不惊动其余一样等待该对象notify的线程们,当第一个得到了该对象的wait线程运行完毕以后,它会释放该对象的锁,此时若是该对象没有再次使用notify语句,则即使对象已经空闲,其余wait状态等待的线程因为没有获得该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。数组

wait状态等待的是被notify而不是锁。缓存

notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException

该方法与notify()方法的工做方式相同,重要的一点差别:
notifyAll使全部的方法在该对象wait的线程通通退出wait状态,变成等待获取该对象上的锁,一旦该对象锁被释放(即notifyAll线程退出同步代码块时),它们就会去竞争。若是其中一个线程得到了该对象的锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其余的已经被唤醒的线程将会继续竞争该锁,一直进行下去,直到全部被唤醒的线程都执行完毕。安全

public class WaitAndNotify {
        public static void main(String[] args) throws InterruptedException{
            WaitAndNotify wan = new WaitAndNotify();
    //        synchronized(wan){
    //            wan.wait();
    //            System.out.println("wait");
    //        }
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        try {
                            wan.wait();
                            System.out.println("wait");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        try {
                            wan.wait();
                            System.out.println("wait");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
            new Thread(new Runnable(){
                public void run(){
                    synchronized(wan){
                        wan.notifyAll();        //当notify方法时只执行一个wait、而notifyAll方法将执行两个wait
                        System.out.println("notify");
                    }
                }
            }).start();
        }
    }

线程的状态及其转换

clipboard.png

新建

建立以后未启动服务器

就绪状态

调用了start方法,不过还未被OS调用,或者正在等待CPU时间片。多线程

运行状态

正在被OS执行。架构

阻塞状态

阻塞状态分三种:

  • 同步阻塞:线程正在等待一个排它锁。
  • 限期阻塞:调用Thread.sleep()、join()方法等待sleep时间结束或join线程执行完毕。
  • 无限期阻塞:经过wait()方法进入,等待notify()或notifyAll()方法唤醒。

终止状态

线程执行完毕或出现异常退了run()。

volatile

做用

volatile能够保证线程的可见性并提供必定的有序性,可是没法保证原子性。

原理

在JVM底层,volatile是采用内存屏障来实现的。

内存屏障

内存屏障是一个cpu指令,他的做用:

  • 确保一些特定操做执行的顺序
  • 影响一些数据的可见性(多是某些指令执行后的结果)。编译器和cpu在保证输出结果同样的状况下对指令重排序,使性能获得优化。插入一个内存屏障,至关于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

内存屏障跟volatile的关系

若是字段是volatile,java内存模型将在写操做后插入一个写屏障指令,在读操做前插入一个读屏障指令。这就意味着若是你对一个volatile字段进行写操做,那么,一旦你完成写入,任何访问这个字段的线程都将会获得最新的值;在你写入以前,会保证以前发生的事情已经发生,而且任何更新过的数据值都是可见的。

任务执行

串行执行任务:在单个线程中串行地执行各项任务。

显式地为任务建立线程:经过为每一个线程建立一个新的线程来提供服务,从而实现更高的响应性。这种方式存在必定缺陷:

  • 线程生命周期的开销很是高:线程的建立于销毁须要消耗计算资源。
  • 资源消耗:活跃的线程会消耗系统资源,尤为是内存。若是在可用处理器的数量小于可运行的线程数量时,那么有些线程将会被闲置。
  • 稳定性:在可建立线程的数量上存在一个限制,若是破坏了这些限制,那么可能会抛出OutOfMemoryError异常。

线程池

Executor框架:

Executor虽然是一个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,应用Runnable来表示任务。同时它还提供了对生命周期的支持。基于生产者-消费者模式,提交任务的操做至关于生产者,执行任务的线程则至关于消费者。

Executor的生命周期

  • Executor的实现一般会建立线程来执行任务,但若是没法正确关闭Executor,那么jvm也将没法关闭。
  • 为了解决执行服务的生命周期问题,Executor扩展了ExecutorSerivce借口,添加了一些用户生命周期管理的方法。
  • ExecutorService的生命周期有三种状态:运行、关闭和已终止。

    • ExecutorService在初始化建立时为运行状态。
    • Shutdown方法将执行平缓的关闭过程:不在接受新的任务,同时等待已经提交的任务执行完成——包括还未开始执行的任务。
    • ShutdownNow方法将执行粗暴的关闭过程:它将尝试取消全部运行的任务,而且不在启动队列中还没有开始执行的任务。

Callable跟Future

  • Runnable是必定有很大局限的抽象,它不能返回一个值或抛出一个受检查的异常。
  • Callable是一种更好的抽象,它认为主入口点(cell)可以返回一个值,并可能抛出一个异常。
  • Future表示一个任务的生命周期,并提供相应的方法来判断是否完成或取消。Executor执行的任务有4个生命周期:建立、提交、开始和完成。任务的生命周期只能前进不能后退。Future的get方法的行为取决于任务的状态,若是完成,那么get会当即返回或者抛出一个Exception,若是任务没有完成,那么get将阻塞并指导任务完成,若是任务抛出异常,那么get将该异常封装为ExecutionException并从新抛出。
Future的建立方式:
  • ExecutorService的全部submit方法都返回一个Future,从而将一个Runnable或Callable提交给Executor,并获得一个Futrue用来获取任务的执行结果或者取消任务。
  • 显式的为某个指定的Runnable或Callable实例化一个FutureTask。(FutureTask实现了Runnable,所以能够将它交给Executor来执行或者直接调用它的run方法)

线程中断:

Java并无提供任何机制来安全的终止线程,但它提供了中断,这是一种协做机制,可以使一个线程终止另外一个线程的当前工做。

Thread的中断方法:

public class Thread{
        public void interrupt(){….}
        public Boolean isInterrupted(){….}
        public static Boolean interrupted(){….}
}

对中断的正确理解:他并不会真正的中断一个正在运行的线程,而只是发出中断请求,而后由线程在下一个合适的时刻中断本身。

  • Interrupt():中断目标线程,
  • IsInterrupt():放回目标线程的中断状态
  • Interrupted():清除当前线程的中断状态,并返回它以前的值,这也是清除中断状态的惟一方法。

Thread.sleep()和Object.wait()都会检查线程什么时候中断,而且在发现中断时提早放回。它们在响应中断时执行的操做:清除中断状态,抛出InterruptedException,表示阻塞操做因为中断而提早结束。可以中断处于阻塞、限期等待、无限期等待等状态,但不能中断I/O阻塞跟synchronized锁阻塞。

在调用interrupted()时返回true,除非像屏蔽这个中断,否则就必须对它进行处理。若是一个线程的 run() 方法执行一个无限循环,而且没有执行 sleep() 等会抛出 InterruptedException 的操做,那么调用线程的 interrupt() 方法就没法使线程提早结束。
可是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。所以能够在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提早结束线程。

经过ExecutorService中断
  • shutdownNow跟shutdown
经过Future中断
  • Future拥有一个cancel方法,该方法带有一个boolean类型参数mayInterruptIfRunning,表示取消操做是否成功,若是mayIterruptIfRunning为true而且任务当前正在某个线程中执行,那么这个线程能被中断,若是mayInterruptIfRunning为false,那么意味着,若任务尚未启动,就不要执行它。

线程池

线程池:管理一组同构工做线程的资源池,经过重用现有的线程而不是建立新线程,能够在处理多个请求时分摊在线程建立和销毁过程当中产生巨大开销。

类库提供了一个灵活的线程池以及一些有用的默认配置。能够用过调用Executors中的静态工厂方法之一来建立一个线程池:

  • newFixedThreadPool: 建立一个固定长度的线程池,每当提交一个任务时就建立一个线程,直到达到线程池的最大数量。newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中的值,而且建立的线程池不会超时。
  • newCachedThreadPool: 建立一个可缓存的线程池,若是线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求添加时,则能够添加新的线程,线程池的规模不存在任何限制。newCacheThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为0,并将超时大小设置为1分钟。
  • newSingleThreadPool: 一个单线程Executor,建立单个工做线程来执行任务,若是这个线程异常结束,会建立另外一个线程来替代。newSingleThreadPool可以确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)

ThreadPoolExecutor

ThreadPoolExecutor为一些Executor提供了基本实现,这些Executor时由Executors中的newFixedThreadPool、newCachedThreadPool和newScheduledThreadExecutor等工厂方法返回的。

ThreadPoolExecutor是一个灵活的、稳定的线程池,容许进行各类定制。若是默认的执行策略不能知足需求,那么能够经过ThreadPoolExecutor的构造参数来实例化一个对象,并根据本身的需求来定制。

public ThreadPoolExecutor( int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler){…}

线程池的基本大小,最大大小以及存活时间等因素公共负责线程的建立和销毁。

  • 基本大小(corePoolSize):线程池的目标大小,即在没有任务执行时线程池的大小,而且只有 在工做队列满了的状况下才会建立超过这个数量的线程。
  • 最大大小(maximumPoolSize):表示可同时活动的线程数量的上限。
  • 存活时间(keepAliveTime):当某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,而且当线程的当前大小超过了基本大小时,这个线程将被终止。

管理队列任务

在线程池中,若是新请求的到达速率超过线程池的处理速率,那么新到来的请求将积累起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待。

ThreadPoolExecutor容许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有三种:有界队列、无界队列和同步移交。队列的选择与其余的配置参数有关,例如线程池的大小等。

  • newFixedThreadPool和newSingleThreadPool在默认状况下使用一个无界的LinkedBlockingQueue。
  • 一个更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueu、PriorityBlockingQueue。经过饱和策略来解决队列填满以后,新任务到来的状况。
  • 对于很是大的或者无界的线程池,能够经过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者持戒交给工做者线程。SynchronousQueue并非一个真正的队列,而是一种在线程之间队形移交的机制。要将线程一个元素放入SynchronousQueue中,必须有另外一个线程在等待接受。若是没有线程等待,而且线程池的当前大小小于最大值,那么ThreadPoolExecutor将建立一个新的线程。不然根据饱和策略,这个任务将被拒绝。使用直接移交更高效。只有线程池时无界或者能够拒绝任务时,SynchronousQueue才有实际价值。在newCacheThreadPool工厂方法中就使用了SynchronousQueue。

饱和策略

当有界队列被填满以后,饱和策略开始发挥做用。JDK提供了集中不一样的RejectedExecutionHadler来实现。

  • 终止(AbortPolicy):默认的饱和策略,将策略将抛出未检查的RejectExecutionException,调用者能够捕获这个异常进行处理。
  • 抛弃(DiscardPolicy):将悄悄抛弃该任务。
  • 抛弃最旧的(DiscardOldestPolicy):抛弃下一个将被执行的任务,而后尝试从新提交新的任务。(若是工做队列是一个优先队列,那么将抛弃优先级最高的任务)
  • 调用者执行(CallerRunsPolicy):将任务会退给调用者,从而下降任务的流量。它不是在线程池中的线程执行新提交的任务,而是在一个调用了execute的线程执行该任务。在WebServer中使用有界队列且”调用者运行”饱和策略时,而且线程池全部的线程都被占用,队列已满的状况下,下一个任务将会由调用execute的主线程执行,此时主线程在执行任务期间不会accept请求,故新到来的请求被保存在TCP层的队列中而不是应用程序的队列中,若是持续过载,那么TCP层的请求队列最终将被填满,由于一样会开始抛出请求。

线程工厂:

每当线程池须要建立一个线程时,都是经过线程工厂方法来完成的,默认的线程工厂方法将建立一个新的、非守护的线程,而且不包含特殊的配置信息。经过制定一个特定的工厂方法,能够定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池须要建立一个新线程时都会调用这个方法。

public interface ThreadFactory {
    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

显式锁

Java5.0增长了一种新的机制:ReentranLock。与内置加锁机制不一样的时,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操做,全部加锁和解锁的方法都是显式的。

Lock lock = new ReentrantLock();
        //...
        lock.lock();
        try {
            //更新对象状态
            //捕获异常,并在必要时恢复不变形条件
        } finally {
            lock.unlock();  //不会自动清除锁
        }

当程序的执行控制单元离开被保护的代码块时,并不会自动清除锁。

轮询锁与定时锁

可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具备更完善的错误恢复机制。在内置锁中,死锁是一个很严重的问题,恢复程序的惟一方式是从新启动程序,而防止死锁的惟一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁提供了另外一种选择:避免死锁的发生。

若是不能得到全部须要的锁,那么可使用可定时的或可轮询的锁获取方式,从而使你从新得到控制权,它会释放已经得到的锁,而后从新尝试获取全部锁。(或其余操做)

在实现具备时间限制的操做时,定时锁一样费用有用:当在带有时间限制的操做中调用一个阻塞方法时,它能根据剩余时间来提供一个时限,若是不能在制定的时间内给出结果,那么就会使程序提早结束。

可中断锁

内置锁是不可中断的,故有时将使得实现可取消得任务变得复杂,而显示锁能够中断,lockInterruptibly方法可以得到锁的同时保持对中断的响应。LockInterruptibly优先考虑响应中断,而不是响应锁的普通获取或重入获取,既容许线程在还等待时就中断。(lock优先考虑获取锁,待获取锁成功以后才响应中断)

公平性

在ReentrantLock的构造函数中提供了两种公平性选择:建立一个非公平的锁(默认)或者一个公平的锁,在公平的锁上,线程讲按照它们发出的请求的顺序来获取锁,但在非公平锁上,则容许插队(当一个线程请求非公平锁时,同时该锁的状态变为可用,那么这个线程将跳过队列中全部的等待线程并得到这个锁)。

在公平锁中,若是有一个线程持有这个锁或者有其余线程在队列中等待这个锁,那么新发的请求的线程将会放入队列中,而在非公平锁中,只有当锁被某个线程持有时,新发出的请求的线程才会放入队列中。

大多数状况下,非公平锁的性能要高于公平锁

  • 公平性锁在线程的挂起以及恢复中须要必定开销
  • 假设线程A持有一个锁,而且线程B请求这个锁,那么在A释放时,讲唤醒B,这时C也请求这个锁,那么可能C极可能会在B被彻底唤醒以前就执行完了。

java内存模型

Java虚拟机规范试图定义一种java内存模型(Java Memory Model,JMM)来屏蔽掉各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。

主内存和工做内存

计算机系统中处理器上的寄存器的读写速度比内存要快几个数量级别,为了解决这种矛盾,在它们之间加入高速缓存。

加入高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,可是也为计算机系统带来了更高的复杂度,由于它引入了一个新的问题:缓存一致性。

多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存,当多个处理器的运算任务涉及到同一块主内存区域时,将可能致使各自的缓存不一致。

为了解决一致性问题,须要各个处理器访问缓存时都遵循一些协议,在读写时根据协议来进行操做。

clipboard.png

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存以及从内存中取出变量的底层细节。

JMM将全部变量(实例字段、静态字段、数组对象的元素,线程共享的)都存储到主内存中,每一个线程有本身的工做内存,工做内存中保存了被线程使用到的变量的主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行。

clipboard.png

内存间的交互操做

Java内存模型定义了8个操做来完成主内存和工做内存间的交互操做。

clipboard.png

  • read:把一个变量从主内存传输到工做内存。
  • load:在read以后执行,把read获得的值放入工做内存的变量副本中。
  • use:把工做内存中的一个变量的值传递给执行引擎。
  • assign:把一个从执行引擎接收到的值赋给工做内存的变量。
  • store:把工做内存的一个变量传送到主内存中。
  • write:在store以后执行,把store获得的值放入主内存的变量中。
  • lock:做用与主内存变量,它把一个变量标识为一条线程独占的状态。
  • unlock:把处于锁定状态得变量释放出来。

内存模型的三大特性

原子性

Java内存模型保证了read、load、use、assign、store、write、lock和unlock操做具备原子性,例如对一个int类型的变量执行assign复制操做,这个操做就是原子性的。可是Java内存模型容许虚拟机将没有被volatile修饰的64位数据(long,double)的读写操做划分为两个32位的操做来进行,即load、store、read和write操做能够不具有原子性。

虽然Java内存模型保证了这些操做的原子性,可是int等原子性操做在多线程中仍是会出现线程安全性问题。
可经过两个方式来解决:

  • 使用AtomicInteger
  • 经过synchronized互斥锁来保证操做的原子性。

可见性

可见性指一个线程修改共享变量的值,其余线程可以当即得知这个修改。Java内存模型经过在变量修改后将新值同步回主内存中,在变量读取前从主内存刷新变量值来实现可见性。

主要有三种方式实现可见性:

  • volatile
  • synchronized,对一个变量执行unlock操做以前,必须把变量值同步回主内存。
  • final:被final修饰的字段在构造器中一旦初始化完成,而且没有发生this逃逸(其余线程有可能经过这个引用访问到初始化了一半的对象),那么其余线程就可以看见final字段的值。

有序性

有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。在Java内存模型中,容许编译器和处理器对指令进行重排序,重排序过程当中不会影响到单线程程序的执行,却会影响多线程并发执行的正确性。

volatile关键字经过添加内存屏障的方式禁止重排序,即重排序时不能把后面的指令放在内存屏障以前。

也能够经过synchronized来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关于让线程顺序执行同步代码。

线程安全的方法

互斥同步

synchronized和ReentranLock。、

非阻塞同步

互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题,所以这种这种同步烨称为阻塞同步。

互斥同步是一种悲观的并发策略,老是觉得只要不去作正确的同步策略,那就确定会出问题,不管共享数据是否真的会出现竞争,它都须要进行枷锁。

CAS

随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略:先进行操做,若是没有其余线程竞争共享资源,那么操做就成功了,不然采起补偿措施(不断尝试、知道成功为止)。这种乐观的并发策略的许多实现都不须要将线程阻塞,所以这种同步操做称为非阻塞同步。

乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证,只能靠硬件来完成。硬件支持的原子性操做最典型的是:比较和交换(Compare-And-swap,CAS)。CAS指令须要3个操做数,分别是内存地址V、旧的预期值A和新值B。当操做完成时,只有内存地址V等值旧的预期值时,才将V值更新为B。

无同步方案

栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,由于局部变量存储在虚拟机栈中,属于线程私用。

本地线程存储(Thead Local Storage)

若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特色的应用并很多见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽可能在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的普遍应用使得不少 Web 服务端应用均可以使用线程本地存储来解决线程安全问题。

可使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

synchronized的实现

synchronized关键字在通过编译以后,会在同步代码块先后分别造成monitorenter和monitorexit两个字节码指令。

在执行monitorenter指令时,首先尝试获取对象的锁,若是这个对象没有被锁定或者当前线程已经拥有了这个锁,就把锁的计算器+1,相应的执行完monitorexit指令时将锁计算器减1,当计算器为0时,锁就被释放。

锁优化

指JVM对synchronized的优化。

自旋锁

互斥同步进入阻塞状态的开销都很大,应该尽可能避免,在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在共享数据的锁执行忙循环(自旋)一段时间,若是在这段时间内可以得到锁,就能够避免进入阻塞状态。

自旋锁虽然可以避免进入阻塞状态从而减小开销,可是它须要进行忙循环操做占用cpu时间,它只适用于共享数据的锁定状态很短的场景。

在JDK1.6中引入了自适应的自旋锁,自适应意味着自旋次数再也不是固定的,而是由前一次在同一个锁上的自旋次数及锁的拥塞者的状态来决定。

锁清除

锁清除是指对被检测出不存在竞争的共享数据的锁进行清除。

锁清除主要经过逃逸分析来支持,若是堆上的共享数据不可能逃逸出去被其余线程访问到,那么就能够把他们当成私有数据,也就能够将他们的锁清除。

对于一些看起来没有加锁的代码,其实隐式的加了不少锁,例如一下的字符串拼接代码就隐式加了锁:

public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

String是一个不可变的类,编译器会对String的拼接进行自动优化。在JDK1.5以前会转化为StringBuffer对象连续append()操做。

public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

每个append()方法中都有一个同步块,虚拟机观察变量sb,很快发现它的动态做用域被限制在concatString方法内部,也就是说,sb的全部引用永远不会逃逸到contatString()方法以外,其余线程没法访问到它,所以能够进行锁清除。

锁粗化

若是一系列的操做都对同一对象反复加锁和解锁,频繁的加锁操做就会致使性能消耗。

上一节的示例代码中连续的append()方法就属于这种状况。若是虚拟机探测到由这样的一串零碎的操做都是对同一个对象加锁,将会把锁的范围扩展(粗化)到整个操做序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操做以前直至最后一个 append() 操做以后,这样只须要加锁一次就能够了。

轻量级锁

轻量级锁跟偏向锁是java1.6中引入的,而且规定锁只能够升级而不能降级,这就意味着偏向锁升级成轻量级锁后不能下降为偏向锁,这种策略是为了提升得到锁的效率。

Java对象头一般由两个部分组成,一个是Mark Word存储对象的hashCode或者锁信息,另外一个是Class Metadata Address用于存储对象类型数据的指针,若是对象是数组,还会有一部分存储的是数据的长度。

clipboard.png

对象头中的Mark Word布局

clipboard.png

轻量级锁是相对于传统的重量级锁而言,它使用CAS操做来避免重量级锁使用互斥量的开销。对于大部分的锁,在整个同步周期内都是不存在晶振的,所以也就不须要使用互斥量进行同步,能够先采用CAS操做进行同步,若是CAS失败了再改用互斥量进行同步。

clipboard.png

当尝试获取一个锁对象时,若是锁对象标记为0 01,说明锁对象的锁未锁定(unlock)状态,此时虚拟机在当前线程的虚拟机栈建立Lock Record,而后使用CAS操做将对象的Mark Word更新为Lock Record指针。若是CAS操做成功了,那么线程就获取了该对象上的锁,而且对象的Mark Word的锁标记变为00,表示该对象处于轻量级锁状态。

若是CAS操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的虚拟机栈,若是是的话说明当前线程已经拥有了这个锁对象,那就能够直接进入同步块执行,不然说明这个锁对象已经被其余线程抢占了。若是有两个以上的线程竞争同一个锁,那轻量级锁就再也不有效,要膨胀为重量级锁。

轻量级锁的步骤以下:

  1. 线程1在执行同步代码块以前,JVM会先在当前线程的栈帧中建立一个空间用来存储锁记录,而后再把对象头中的Mark Word复制到该锁记录中,官方称之为Displaced Mark Word。而后线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。若是成功,则得到锁,进入步骤(3)。若是失败执行步骤(2)
  2. 线程自旋,自旋成功则得到锁,进入步骤(3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤(3)
  3. 锁的持有线程执行同步代码,执行完CAS替换Mark Word成功释放锁,若是CAS成功则流程结束,CAS失败执行步骤(4)
  4. CAS执行失败说明期间有线程尝试得到锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁以后,还要唤醒等待的线程

偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程,在以后的获取该锁就再也不须要进行同步操做,甚至连CAS操做也不须要。

当锁对象第一次被线程得到的时候,进入偏向状态,标记为1 01。同时使用CAS操做将线程ID记录到Mark Word中,若是CAS操做成功,这个线程之后每次进入这个锁相关的同步块就不须要再进行任何同步操做。

当有另外一个线程去尝试获取这个锁对象,偏向状态就宣告结束,此时偏向取消后恢复到未锁定状态或者轻量级锁状态。

偏向锁得到锁的步骤分为:

  1. 初始时对象的Mark Word位为1,表示对象处于可偏向的状态,而且ThreadId为0,这是该对象是biasable&unbiased状态,能够加上偏向锁进入(2)。若是一个线程试图锁住biasable&biased而且ThreadID不等于本身ID的时候,因为锁竞争应该直接进入(4)撤销偏向锁。
  2. 线程尝试用CAS将本身的ThreadID放置到Mark Word中相应的位置,若是CAS操做成功进入到3),不然进入(4)
  3. 进入到这一步表明当前没有锁竞争,Object继续保持biasable状态,但此时ThreadID已经不为0了,对象处于biasable&biased状态
  4. 当线程执行CAS失败,表示另外一个线程当前正在竞争该对象上的锁。当到达全局安全点时(cpu没有正在执行的字节)得到偏向锁的线程将被挂起,撤销偏向(偏向位置0),若是这个线程已经死了,则把对象恢复到未锁定状态(标志位改成01),若是线程还活着,则把偏向锁置0,变成轻量级锁(标志位改成00),释放被阻塞的线程,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行。
  5. 运行同步代码块

参考

相关文章
相关标签/搜索