JAVA并发各种锁,乐观锁、悲观锁、自旋锁、轻量级锁、偏向锁、重量级锁,适合小白,一看就懂

前言

  本文章主要针对像我一样代码只写了小几千行的小白,快速地了解锁的概念。文章90%摘自别处,是一种整理!!不是自己写的!!,不涉及底层数据结构层面的操作,属于比较容易理解的。相信入门的你,看那些大佬写的深层次东西也看不懂。也是为了自己面试复习准备。

锁的分类

锁从宏观上分类,分为悲观锁与乐观锁

乐观锁
  乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
  java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

  悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized

公平锁/非公平锁
公平锁和非公平锁的区别就在于,公平锁是FIFO机制,谁先来谁就在队列的前面,就能优先获得锁。非公平锁支持抢占模式,先来的不一定能得到锁。

package thread;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

public class Test6 {
    public static class Service {
        public ReentrantLock fairLock = new ReentrantLock(true);

        public void testMethod() {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + "拿到了锁");
            } finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Service service = new Service();
        ArrayList<Thread> list = new ArrayList<>();
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始运行");
                service.testMethod();

            }
        };
        for (int i = 0; i < 10; i++) {
            list.add(new Thread(runnable));
        }
        System.out.println(list);
        for (Thread thread : list) {
            thread.start();
        }
    }
}

----output----
[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]
Thread-0开始运行
Thread-1开始运行
Thread-0拿到了锁
Thread-2开始运行
Thread-2拿到了锁
Thread-3开始运行
Thread-3拿到了锁
Thread-4开始运行
Thread-5开始运行
Thread-6开始运行
Thread-4拿到了锁
Thread-1拿到了锁
Thread-6拿到了锁
Thread-7开始运行
Thread-8开始运行
Thread-5拿到了锁
Thread-9开始运行
Thread-7拿到了锁
Thread-8拿到了锁
Thread-9拿到了锁
运行的顺序:0,1,2,3,4,5,6,7,8,9.
获得锁的顺序:0,2,3,4,1,6,5,7,8,9.

把true去掉改成非公平锁
----output----
[Thread[Thread-0,5,main], Thread[Thread-1,5,main], Thread[Thread-2,5,main], Thread[Thread-3,5,main], Thread[Thread-4,5,main], Thread[Thread-5,5,main], Thread[Thread-6,5,main], Thread[Thread-7,5,main], Thread[Thread-8,5,main], Thread[Thread-9,5,main]]

Thread-0开始运行
Thread-1开始运行
Thread-2开始运行
Thread-3开始运行
Thread-4开始运行
Thread-1拿到了锁
Thread-6开始运行
Thread-6拿到了锁
Thread-5开始运行
Thread-2拿到了锁
Thread-7开始运行
Thread-8开始运行
Thread-7拿到了锁
Thread-9开始运行
Thread-3拿到了锁
Thread-0拿到了锁
Thread-4拿到了锁
Thread-5拿到了锁
Thread-8拿到了锁
Thread-9拿到了锁


运行的顺序:0,1,2,3,4,5,6,7,8,9
拿到锁的顺序:1,6,2,7,3,6,4,5,8,9

上面的例子可以看出,公平锁的是有顺序的,非公平锁是无序的。同样除了重入锁,重入读写锁一样可以设置非公平和公平。

由重量级细化分为四种:自旋锁,轻量级锁,偏向锁,重量级锁

操作系统

用户态核心态

两个操作系统的概念,用户态,核心态
(1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。
(2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。

线程阻塞的代价
  java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

  如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
  如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

数据结构

  太底层了,不适合新手,本文整理的内容适合新手从宏观上去了解,不做太深入地研究。

自旋锁

what?
 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

why?
避免核心态和用户态之间切换的消耗。
  如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

缺点
  但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间

偏向锁

what?
偏向于第一个访问锁的线程。注意只有一个线程访问

why?
  为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行。
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用。

偏向锁转向轻量级的条件:线程由1->2
一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁
这里写图片描述

轻量级锁

what?
上文提到当有两个线程,竞争的时候就会升级为轻量级锁。

why?
  引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
这里写图片描述

重量级锁synchronized

这里写图片描述

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

  • 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中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

锁优化

介绍3种简单的锁优化
这里写图片描述
这里写图片描述

资料出处

操作系统部分:
https://www.jianshu.com/p/255c6c7603d0
锁:
https://blog.csdn.net/zqz_zqz/article/details/70233767
http://cmsblogs.com/?p=2071
http://www.cnblogs.com/butterfly100/p/8786856.html

猜你喜欢

转载自blog.csdn.net/qq_41376740/article/details/80607243