JAVA后端知识点碎片化整理 基础篇(三) 并发

因为马上开始2019秋招、平时的学习比较琐碎、JAVA后端博大精深,想在暑假这段时间从头开始整理JAVA知识点查缺补漏,迎战2019秋招。主要参考(微信公众号)JAVA团长与(博客园)五月的仓颉的知识点复习线,对其列出的每一个的知识点再一次的咀嚼并谈谈自己的理解。(平时从这两位学到很多,也非常感谢身边同行的人)

线程

1、创建线程的方式及实现(三种方式)

(1)继承Thread类创建线程:定义Thread类的子类,并重写其中的该类的run方法,该run方法的方法体就代表了线程要完成的任务。创建线程thread的实例,调用thread的start方法来启动该线程。new  thread().start;

(2)通过Runnable接口创建线程类:定义runnable接口的实现类,并重复该接口的run方法,该run方法的方法体同样是该线程的线程执行体。创建Runnable实现类的实例,并以此实例作为Thread的target来创建thread对象,该Thread对象才是真正的线程对象。new Thread(new RunnableThreadTest()).start()

(3)通过Callable和Future创建线程:创建Callable接口的实现类并实现其call方法将作为线程的执行体,并有返回值。创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call方法的返回值。使用FutrueTask对象作为Thread对象的target创建并启动新线程。调用FutureTask对象的get方法获得子线程执行结束后的返回值。

CallableThread ctt = new CallableThread();

FutureTask<Integer> ft = new FutureTask<>(ctt);

new Thread(ft).start;

ft.get();

对比:采用实现Runable、Callable接口的方式去创建多线程时,优势是线程类只是实现了Runable接口和Callable接口,还可以继承于其他类。(接口相对于类的优势)在这种情况下,多线程可以共享同一个target对象,所以非常适合多个相同的线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面对对象的思想。缺点是获取当前类只能用thread。currentThread()方法,而继承Thread时编程相对较为容易。

2、线程所执行的各个方法解析

(1)sleep():sleep方法需要制定等待的时间,他可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级的线程得到执行的机会,可以让低优先级的线程得到执行机会。但是sleep方法并不会释放锁标志,也就是说如果sleep在synchronized同步块中,其他线程仍然不能访问共享数据。(sleep是对于Thread而言,wait是对Object而言。wait notify和notifyall智能在同步方法块和同步控制方法中使用而sleep可以再任何地方使用。sleep和wait都可以让线程暂停一段时间,本质的区别在于一个是线程运行的控制状态的问题,另一个是线程之间通信的问题。

(2)wait方法:wait方法与notify和notifyall方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronize语句块中使用,也就是说wait(),notify(),notifyAll()任何时候调用这些方法首先要占用拥有这个对象的锁。(他们都是Object的方法,这就是为什么我们说锁的本质是对象锁)。wait与sleep的不同之处在与,wait会释放对象的“锁标志”,当调用某一对象的wait方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直道调用了notify方法后,将从对象等待池中移出一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,他们随意都准备夺取锁的控制权。当调用了某个对象的notifyAll()方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。(划重点什么时候在对象等待池什么时候不在对象等待池,除了使用notify和notifyall方法还可以使用wait(毫秒)使暂停的线程被恢复到锁标志等待区)。此外wait notify只能在synchronize语句中使用,如果使用时ReenTrantLock实现同该如何实现呢,将有ReenTrantLock.newConnectioin 如下几个方法通过Connection的await()、signal()、以及signalAll()分别对应上面的三个方法。

(3)yield和sleep方法类似,也不会释放锁标志,它没有参数,即yield方法知识使当前线程重新回到可执行状态,所以执行yield的线程有可能在进入到可执行状态后又被马上执行,另外yield方法只能使同优先级和高优先级的线程得到执行机会,这也是和sleep方法不同的地方。

(4)join()方法会使当前线程等待调用join方法的线程结束之后才能继续执行,threadA在运行时调用Threadb.join(),那么等B执行完后,在执行A。join调用的实际上还是wait方法。

3、分析ReentrantLock的实现原理

ReentrantLock(AQS的独占)和同步工具类的实现基础都是AQS,AQS就是AbstractQueuedSynchronizer,一个用来构架锁的同步工具的框架,包括常用的ReentrantLock、CountDownLatch、Semaphore等。AQS围绕着state提供两种基本的操作,“获取”与“释放”有条双向队列存放阻塞的等待线程,并提供一系列判断与处理方法。主要包括state是独占的还是共享的、state是被获取后,其他线程需要等待、state被释放后,唤醒等待线程、线程等不及时,如何退出等待。

ReentrantLock与Synchronize对比:Synchronize有着自己的局限性1、占有锁的线程等待IO或其他原因导致被阻塞,没有释放锁的情况下,其他线程一直阻塞。2、多个线程同时读写文件的是时候读与读操作会发生冲突。3、synchronize是基于JVM层面的,我没有办法知道当前线程是否获取了锁,只能傻傻的等着。   为了解决这些限制,java中就有了lock接口

 

  • lock()获取锁,没有获取一直等待,无返回值
  • lockInterruptibly()获取锁一直等待,无返回值,但是可以被thread.interrupt()方法中断,抛出异常
  • tryLock()获取锁,不等待,返回是否获取成功
  • tryLock(long time, TimeUnit unit)获取锁,没有获取等待指定时间,返回是否获取成功,等待时候可以被thread.interrupt()方法中断,抛出异常(可中断锁 在等待的过程中可中断,lockInterruptibly()获取的就是可中断锁,中断会抛出异常)
  • unlock()释放锁
  • newCondition()实现线程间的交互,与Object的wait,notify,notify对应

  ReentranLock实现了lock的接口,加锁和解锁都需要显式的去调用,lock与unlock,注意可以重复lock。与synchronize相比,ReentrantLock用起来比较复杂,在基本的加锁与解锁上,两者一样,ReentrantLock优势在于他更加灵活,增加了轮训、超时、中断等高级功能。还有一点ReentrantLock内部类继承了AQS,分为公平锁FairSync和非公平锁NonfairSync,公平锁:线程获取锁的顺序与调用顺序一样,FIFO;非公平锁:线程获取锁的顺序和调用lock的顺序无关,使用非公平锁基于性能的考虑,公平锁保证了线程规规矩矩地排列,需要增加阻塞和唤醒的时间的开销。如果直接插队获取非公平锁,跳过了对队列的处理,处理速度更快。synchronize的释放锁必须满足三个条件之一:1、占有锁的线程执行完毕2、占有锁的线程异常退出3、占有锁的线程进入waiting状态释放锁。Lock必须调用unlock方法。(ReentranLock是其唯一实现类)

ReentranLock的具体实现细节:(这里直接摘自https://www.jianshu.com/p/fe027772e156

尝试获取锁,lock调用AQS的acquire方法,是否成功有ReentranLock的tryAcquire实现。获取锁成功分为两种情况,第一个if判断AQS的state是否等于0.表示锁没有人占有。接着hasQueuedPredecessors判断队列是否排在前面线程等待锁,没有的话调用 compareAndSetState使用cas的方式修改state,传入acquires写死是1,线程获取锁成功,setExclusiveOwnerThread将线程记录为独占锁的线程。         第二个if判断当前线程是否为独占锁的线程,因为ReentrantLock是可以重入的,线程可以不停的lock来增加state的值,对应需要unlock解锁,直至state为0.   如果最后获取失败,下一步需要将线程加入到等待队列

protected final boolean tryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
       if (!hasQueuedPredecessors() &&
           compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0)
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}

线程进入等待队列,AQS中有一条双向的队列存放等待线程,节点是Node对象,每个Node维护了线程、前后Node的指针和等待状态等参数。线程加入队列前会被包装进Node调用方法是addWaiter,每个Node需要标记是独占还是共享,由传入的mode决定,ReentrantLock使用的是独占模式,创建好Node后,如果队列不为空,使用cas方式将Node加入队列尾部意,这里只执行了一次修改操作,并且可能因为并发的原因失败。因此修改失败的情况和队列为空的情况,需要进入enq。enq是个死循环,保证Node一定能插入队列。注意到,当队列为空时,会先为头节点创建一个空的Node,因为头节点代表获取了锁的线程,现在还没有,所以先空着。

private Node addWaiter(Node mode) {
   Node node = new Node(Thread.currentThread(), mode);
   // Try the fast path of enq; backup to full enq on failure
   Node pred = tail;
   if (pred != null) {
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
   }
   enq(node);
   return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

了解了获取锁与线程的入列,相对应的出列和释放锁就与其类似。

4、并发包中的3种并发控制手段CountDownLatch|CyclicBarrier|Semaphore

CountDownLatch:java.util.concurrent.CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。CountDownLatch 以一个给定的数量初始化。countDown()每被调用一次,这一数量就减1。通过调用 await()方法,线程可以阻塞等待这一数量到达零。例如,游戏中,需要8名玩家都准备就绪后,游戏才能开始。也可以利用闭锁计算各个线程执行完成所需的时间。CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。 上述例子,main线程是裁判,5个AWorker是跑步的。运动员先准备,裁判喊跑,运动员才开始跑(这是第一次同步,对应begin)。5个人谁跑到终点了,countdown一下,直到5个人全部到达,裁判喊停(这是第二次同步,对应end),然后算时间。

CyclicBarrier(并发包下的n倍Synchronize) 类是一种同步机制,它能对处理一些算法的线程实现同步。换句话说,它就是一个所有线程必须等待的一个栅栏,直到所有线程都到达这里,然后所有线程才可以继续做其他事情。通过调用 CyclicBarrier 对象的 await()方法,两个线程可以实现互相等待。一旦 N 个线程在等待 CyclicBarrier 达成,所有线程将被释放掉去继续执行。CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有人都得等着。正如上例,只有5个人全部跑到终点,大家才能开喝,否则只能全等着。 

Semaphore类是一个计数信号量,acquire的获取,release的释放,计数信号量由一个指定数量的“许可”初始化,每调用一次acquire,一个许可会被调用线程取走,每调用一次release,一个许可会被还给信号量。因此,在没有任何release,最多有N个线程能够通过acquire方法,N是该信号量初始化时的许可指定数量。Semaphore是一个信号量,他的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最后只有n个线程可以访问,如果超过了n那么请等待。相当于synchronize由1变为n。Semaphore s = new Semaphore(4),这样在new Runable{中调用s.acquire()}保证最多只能由四个线程去调用。

5、ThreadPoolExecutor线程池

主要参数含义:corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有着非常大的关系。在创建线程池后,在默认情况下,线程池中并有任何线程,而是等待任务的到来才创建线程去执行任务,除非调用prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示线程池中最多创建的线程。(与上面有所不同的是如果池中实际线程数小于corePoolSize 无论是否其中有空闲线程都会给新的任务产生新的线程,如果池中的线程数字大于核心线程数且小于最大maximumSize,那么就会将新的任务交给空闲线程,如果没有空闲线程就给将任务交给newThread去使用,最后如果池中线程数字等于maximumPoolSize,有空闲线程则将任务给空闲线程没有空线程,就将任务放入等待队列)

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止,默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize即当线程池中线程数大于corePoolSize,但是如果调用了allowCoreThreadTimeOut方法,在线程池中线程数不大于corePoolSize时,keepAliveTime参数夜壶起作用,知道线程池中的线程数为0。

unit:参数keepAliveTime的时间单位。

workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,对线程池的运行过程有重大影响。ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue的阻塞队列选择1)ArrayBlockingQueue:基于数组实现的一个堵塞队列。在创建ArrayBlockingQueue对象时必须制定容量大小。以便缓存队列中数据对象。而且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先可以訪问队列其所含的对象是以FIFO(先入先出)顺序排序的 2)LinkedBlockingQueue:基于链表实现的一个堵塞队列。在创建LinkedBlockingQueue对象时假设不指定容量大小,则默认大小为Integer.MAX_VALUE。其所含的对象是以FIFO(先入先出)顺序排序 3)SynchronousQueue:一种无缓冲的等待队列,类似于无中介的直接交易。   当中LinkedBlockingQueue和ArrayBlockingQueue比較起来,它们背后所用的数据结构不一样,导致LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量非常大时其性能的可预见性低于ArrayBlockingQueue.

handler:拒绝执行任务时的策略ThreadPoolExecutor.AbortPolicy丢弃任务并抛出异常,ThreadPoolExector..DiscardPolicy丢弃任务不抛出异常,ThreadPoolExector.DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务。ThreadPoolExector.CallerRunsPolicy调用线程处理该任务。

6、线程池的几种方式

newFixedThreadPool创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这个时候线程池的规模不再变化,每当一个线程发生不能估计的错误时,线程池会补充一个新的线程。

newCachedThreadPool创建一个可缓存的线程池,如果线程池的规模超过了批量请求,将自动回收空闲线程,而当需求增加时候,可以自动添加新的线程,线程池的规模不在存在任何限制。

newSingleThreadExecutor 这个一个单线程线程池,他创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来代替他,他的特定能够确保依照任务在任务队列的顺序中串行执行。

newScheduledThreadPool创建一个固定长度的线程池,而已以延迟或定时的方式来执行任务。

7、线程的生命周期

新建(new)、就绪(Runable)、运行(Running)、阻塞(Blocking)和死亡的五种状态

生命周期的五种状态(1) 新建 当一个Thread类的实例被创建时,此线程进入新建状态(未被启动)。(2)就绪(Runable) 线程已经被启动,等待被分配给CPU时间片,也就是说此线程自动放弃CPU资源或者有优先级更高的线程进入,线程一直运行到结束。(3)运行(Runing)线程获得CPU资源正在执行任务,此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程一直运行到结束。(4)死亡(dead)当线程执行完毕,或者被其他线程杀死,线程进入了死亡状态,这时线程不可能在进入就绪状态等待执行。正常终止运行完run方法后,异常终止调用stop方法让线程终止(5)阻塞(blocked)由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,进入阻塞状态。正在睡眠:用sleep方法可以让线程进入睡眠方式,一个睡着的线程在指定的时间过去可进入就绪状态。正在等待:调用wait方法。(notify可以使其回到就绪状态)

8、线程的安全问题:

线程安全是指要控制多个线程对某个资源进行有序访问或者修改,而这些线程之间没有产生冲突,在java里,线程安全一般体现在两个方面:(1)多个Thread对同一个java实例进行访问不会互相干扰,他主要体现在关键字Synchronize,如果ArrayList和Vector,Hashmap与hashtable*(后者的每个方法都是有synchronize)如果你在遍历一个list对象,其他线程remove了一个element那么问题就出现。(2)、每一个线程都有自己字段,而不会在多线线程之间共享,它主要体现在ThreadLocal类。

这里我们主要谈一谈ThreadLocal类:ThreadLocal是线程局部变量,县所谓的线程局部变量,就仅仅只能被本线程访问,不能在线程之间进行共享访问变量。在各个Javaweb的各种框架中ThreadLocal已经被用烂了。这个类给线程提供一个本地变量让这个变量编程线程自己所拥有的。在线程存货和ThreadLocal实例能访问的时候,保存了对这个变量副本的引用。当线程存活和ThreadLocal实例能访问的时候保存了对这个变量副本的引用,当线程消失的时候,所有的本地实例都会被GC。并且建议我们ThreadLocal最好是private static修饰的成员。Synchronized用线程间的数据共享,同一时刻只能被一个线程访问,使用以延长访问时间的方式来换取线程的安全性的策略,而ThreadLocal用于线程之间的数据隔离,为每一个线程都提供一个变量的副本,是一种以空间来换取线程安全性的策略、、、、、、、、从源码中可以看到Thread里面有一个类似于map的东西,默认是空的,在我声明ThreadLocal的时候,ThreadLocal吧这个Map初始化,并且把ThreadLocal和value绑定到map中,也就是说这个map的key值是ThreadLocal,这样的好处是value放入了Thread中,这样线程死了,value也就被回收了。还有就是性能ThreadLocal放入map中比thead放入map中节省的空间会比较多。

9、volatile原理

java内存模型中的可见性、原子性、有序性。可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的,原子性是指该操作具有不可分割性。有序性:一个变量在同一时刻只允许一条线程对其进行lock操作,此规则决定了持有同一个对象的两个同步块智能串行执行。  volatile提供了一种稍弱的同步机制,即volatile用来确保变量更新操作通知其他线程,吧变量声明为volatile后,编译和运行时都会注意到这个变量时共享的,因此不会将该变量上的操作和其他内存操作一起重排序。对于底层而言声明变量时volatile的,JVM保证了每次读变量都从内存中读,跳过Cpu cache这一步。。正常的变量都是每个线程都从内存拷贝变量到CPU缓存中,如果多个cpu那个每个线程可能在不同cpu上被处理,这以为这个每个线程可以拷贝到不同cpu cache中。volatile读性能与普通变量相同,但写操作稍慢,因为他需要在本地代码中插入内存屏障保证处理不乱序执行(就是在写操作之后加入一个存储指令强迫线程将最新的值刷新到主内存中)。这样读必须从内存读,写必须在完后存到内存中从而保证了volatile的可见性。

10、乐观锁与悲观锁

乐观锁:总是认为不会产生并发问题,每次去取数据的时候认为不会其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有数据对其进行修改,一般会使用版本号机制或者CAS操作实现。Version方式:一般在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1,当线程A要更新数据时,在读数据的同时也会读取version值,在提交更新时,若刚才读取到的version值是当前数据库中的version值相等时才更新,否则重试更新操作,知道更新成功。CAS操作方式:即Compare and swap涉及三个操作数,数据所在内存值、预期值和新值。当需要更新时,判断当前内存值和之前取到的值是否相等,若相等则用新值更新,若失败则重试。(也会有个自旋的操作)(ABA问题CAS有着自己问题,因为如果一个线程修改V的假设成原来的A,先修改B,再修改成A,当前线程无法分辨V值是否发生了变化)比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

悲观锁:总是假设最坏的情况发生,每次取数据时候都会认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等)当前线程想要访问数据时,都需要阻塞挂起,可以依靠数据库本身实现一些行锁、读锁、写锁、等,都在这些操作之前,在java中Synchronize也是悲观锁。

11、Synchroize的实现原理与应用

Java中任何一个对象都可以作为锁,而在Synchronized实现同步的方式中,1、普通同步方法:锁是当前实例对象。2、静态同步方法锁是当前类的Class对象。3、同步方法块:锁是Synchronized括号里配置的对象。任何一个对象都与一个Monitor相关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronize在JVM中的实现都是基于进入和退出Monitor对象来实现方法同步和代码同步,虽然具体实现细节不一样,但是都可以通过对MonitorEnter和MonitorExit指令来实现。MonitorEnter指令插入在同步块开始的位置,当代码执行到指令时,将会尝试去获取该对象的Monitor的所有权,即尝试获得该对象的锁,而MonitorExit则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应MonitorExit。

JAVA对象头:Synchronized使用的锁是存放在java对象头中,具体位置是头中的MarkWord中,不同的状态也会有不同的记录存储方式。

Monitor Record是线程私有的数据结构,每一个线程都有一个可用的monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor record关联。。每一个java对象都有一个monitor与之对应,同时monitor有一个Owner字段存放拥有该锁的线程的唯一表示,表示该对象的锁被这个线程占用。

锁的类型:无锁-》偏向锁-》轻量级锁-》重级锁

偏向锁大多数情况锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入偏向锁,减少不必要的CAS,加锁:当一个线程访问同步块并获取锁时,会在对象头和帧栈中的锁记录里存储偏向锁的的线程ID和,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需要简单测试一下对象头的Mark Word里是否从存储着只想当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁是否为1,如果没有设置则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(实际上就是如果在设了偏向锁标志的情况下发生了竞争此时就会将偏向锁升级为轻量级锁)膨胀过程:当前线程执行CAS获取偏向锁失败,则表示该锁对象存在竞争并且这个时候另一个线程获得偏向锁的所有权,当到达全局安全点的时候(就是获得偏向锁的的线程被挂起的时候了),从Monitor Record列表中获得一个空闲记录,并将Object设置为lightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。(清晰的来说就是:当锁对象第一次被线程获取的时候,虚拟机吧对象头的标志位设置为01,即偏向模式,同时用CAS操作获得这个锁的线程ID的记录在MarkWord,并将是否偏向标志设置为1。CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关同步块时,直接检查ID是否与自身线程一致。突然有另一个线程尝试去获取锁,那么偏向锁模式就宣告结束了,将其变为轻量级的锁定“00”状态

轻量级锁:引入背景:这种锁的实现的背后基于这样一种假设,即在真实的情况下我们程序中大部分同步代码一般都处于无锁竞争状态。(即单线程执行环境)在无锁竞争的情况下完全可以避免调用系统层面的重量级锁,取而代之只需在monitorenter与monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取和释放。

 

加锁: 
(1)当对象处于无锁状态时(RecordWord值为HashCode,状态位为001),线程首先从自己的可用moniter record列表中取得一个空闲的moniter record,初始Owner值被设置该线程自己的标识,一旦monitor record准备好然后我们通过CAS原子指令安装该monitor record的起始地址到对象头的LockWord字段,如果存在其他线程竞争锁的情况而调用CAS失败,则只需要简单的回到monitorenter重新开始获取锁的过程即可。

(2)对象已经被膨胀同时Owner中保存的线程标识为获取锁的线程自己,这就是重入(reentrant)锁的情况,只需要简单的将Nest加1即可。不需要任何原子操作,效率非常高。

(3)对象已膨胀但Owner的值为NULL,当一个锁上存在阻塞或等待的线程同时锁的前一个拥有者刚释放锁时会出现这种状态,此时多个线程通过CAS原子指令在多线程竞争状态下试图将Owner设置为自己的标识来获得锁,竞争失败的线程在则会进入到第四种情况(4)的执行路径。

(4)对象处于膨胀状态同时Owner不为NULL(被锁住),在调用操作系统的重量级的互斥锁之前先自旋一定的次数,当达到一定的次数时如果仍然没有成功获得锁,则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Object和monitor record之间的关联,所以在原子性加1后需要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要重新monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

偏向锁:优点加锁和解锁不许额外的消耗,执行非同步方法比仅存在纳秒级的差距,缺点如果线程之间存在竞争没回带来额外的锁撤销的的消耗,适用于一个线程访问同步块的场景。所谓偏向锁,更类似于一种确认机制判断线程ID是否相同。

轻量级锁:优点竞争的线程不会阻塞(通过线程自旋一定的次数,减少了线程之间的切换)提高了程序的响应速度,缺点如果始终得不到竞争的线程使自身自旋会消耗CPU。适用于追求响应时间,同步块执行速度非常快的情况。

重量级锁:线程竞争不适用自旋,不会消耗CPU 缺点线程阻塞,响应时间会缓慢,追求吞吐量,同步执行时间较长。

吞吐量是对单位时间内完成的工作量的度量,而响应时间是提交请求和返回请求之间使用的时间。

上面百度的实在过于晦涩,简述一下锁的膨胀过程:JVM在当前线程的帧栈中创建自己用于存储锁记录的空间(LockRecord),然后将MarkWord翻进去,同时生成一个叫Owner的指针指向加锁的对象,同时用CAS操作尝试吧对象头的MarkWord换成一个指向锁记录的指针。如果成功了那么就拿到了锁,如果失败了有两种可能1、指向当前线程的指针,2、别的值。如果是1则说明锁发生了“重入”现象,直接当做成功获得锁处理。为什么叫获得锁成功了而CAS失败了,牵扯到CAS的具体过程,先比较某个值是不是预测的值,是的话就动用原子操作交换,否则不操作直接返回失败。在CAS的时候,期待的值就是原来版本的MarkWord,发生重入时会发现期待的值并不是期待的原本的MarkWord,而是一个指针,所以当然就返回失败,但是如果这个指针指向这个线程,那么说明已经获得这个锁不过是再进一次。如果是2,那么发生了竞争就短时间内自旋获得锁如果还是无法获得就膨胀成重量级锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值