美文网首页
java-守护线程/JUC/Volatile/AQS/CAS/锁

java-守护线程/JUC/Volatile/AQS/CAS/锁

作者: Nenezsnp | 来源:发表于2023-11-02 17:46 被阅读0次

    学习摘抄整理,已付参考链接

    守护线程

    守护线程和用户线程的区别

    守护线程创建

    ThreadLocal

    ThreadLocal理解

    ThreadLocal与Synchronized的区别

    ThreadLocal的简单使用

    ThreadLocal 常见使用场景

    场景

    ThreadLocal使用原理

    ThreadLocal 内存泄露的原因

    为什么不将key设置为强引用

    key 如果是强引用

    如何正确的使用ThreadLocal

    参考链接:[理解ThreadLocal]

    共享变量的内存可见性问题详解

    并发和并行

    共享资源

    Java共享变量的内存可见性问题

    synchronized关键字

    synchronized关键字介绍

    synchronized的内存语义

    volatile关键字

    volatile关键字介绍

    volatile内存语义

    volatile顺序一致性

    参考链接:[共享变量的内存可见性问题详解][单核CPU与多核CPU,进程与线程,程序并发执行?][Synchronized关键字][Volatile关键字]

    原子性操作

    jmm的8大原子操作

    参考链接[8大原子性操作]

    CAS操作

    ABA问题

    参考链接[CAS ABA问题]

    Unsafe类

    参考链接

    伪共享

    MESI 缓存一致性协议

    什么是伪共享?

    参考链接[小林coding-伪共享][伪共享]

    互斥锁(独占锁)和共享锁

    读写锁

    读写锁ReentrantReadWriteLock的原理

    ReentrantReadWriteLock类图

    源码解读

    ReentrantReadWriteLock类的继承关系
    Sync类的继承关系
    Sync类的内部类
    Sync类的属性
    Sync类的构造函数

    读写锁的状态设计

    写锁的获取与释放

    锁的获取,看下tryAcquire

    锁的释放,看下tryRelease

    参考链接[读写锁]

    乐观锁与悲观锁

    乐观锁

    什么是乐观锁

    Java中CAS算法实现乐观锁

    乐观锁的缺点

    悲观锁

    什么是悲观锁

    数据库悲观锁

    参考链接[乐观锁和悲观锁]

    公平锁和非公平锁

    什么是公平锁

    参考链接[公平锁和非公平锁]

    可重入锁

    什么是可重入锁

    解析可重入锁

    ReenTrantLock可重入锁和synchronized的区别

    可重入性

    锁的实现

    性能区别

    功能区别

    ReenTrantLock独有的能力

    Synchronized可重入例子

    ReentrantLock可重入例子

    参考链接、[可重入锁]

    自旋锁

    参考链接

    AQS

    AQS简单介绍

    AQS原理

    AQS模版方法

    守护线程

    守护线程和用户线程的区别

    Java 中的线程可以分为两类:用户线程和守护线程(Daemon Thread)。它们之间的区别在于虚拟机在何时结束进程。
    用户线程是虚拟机启动的线程中的普通线程,当所有用户线程结束运行后,虚拟机才会停止运行,即使还有一些守护线程在运行,虚拟机也不会理会直接停止运行。
    守护线程是在程序中创建的线程,它的作用是为其他线程提供服务。当所有的用户线程结束运行后,守护线程也会随之结束,而不管它是否执行完毕。守护线程通常用于执行一些辅助性任务,如垃圾回收、缓存清理等,它们不需要等待所有的任务完成后再退出。
    在 Java 中,可以使用 Thread 类的 setDaemon() 方法将一个线程设置为守护线程,也可以使用 isDaemon() 方法判断一个线程是否为守护线程。默认情况下,线程都是用户线程

    守护线程创建

    创建:将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon (true) 方法来实现。
    创建细节:

    • thread.setDaemon (true) 必须在 thread.start () 之前设置,否则会跑出一个 llegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程;
    • 在 Daemon 线程中产生的新线程也是 Daemon 的;
    • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
      代码示例:
    public class DemoTest {
        public static void main(String[] args) throws InterruptedException {
            Thread threadOne = new Thread(new Runnable() {
                @Override
                public void run() {
                    //代码执行逻辑
                }
            });
            //设置threadOne为守护线程
            threadOne.setDaemon(true); 
            threadOne. start();
        }
    }
    

    ThreadLocal

    ThreadLocal理解

    ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量

    ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

    • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
    • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

    ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。
    ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

    下图可以增强理解:


    ThreadLocal线程本地副本

    ThreadLocal与Synchronized的区别

    ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。

    但是ThreadLocal与synchronized有本质的区别:

    • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
    • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

    一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

    ThreadLocal的简单使用

    直接上代码:

    public class ThreadLocaDemo {
     
        private static ThreadLocal<String> localVar = new ThreadLocal<String>();
     
        static void print(String str) {
            //打印当前线程中本地内存中本地变量的值
            System.out.println(str + " :" + localVar.get());
            //清除本地内存中的本地变量
            localVar.remove();
        }
        public static void main(String[] args) throws InterruptedException {
     
            new Thread(new Runnable() {
                public void run() {
                    ThreadLocaDemo.localVar.set("local_A");
                    print("A");
                    //打印本地变量
                    System.out.println("after remove : " + localVar.get());
                   
                }
            },"A").start();
     
            Thread.sleep(1000);
     
            new Thread(new Runnable() {
                public void run() {
                    ThreadLocaDemo.localVar.set("local_B");
                    print("B");
                    System.out.println("after remove : " + localVar.get());
                  
                }
            },"B").start();
        }
    }
     
    A :local_A
    after remove : null
    B :local_B
    after remove : null
    

    从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

    ThreadLocal 常见使用场景

    ThreadLocal 适用场景

    • 每个线程需要有自己单独的实例
    • 实例需要在多个方法中共享,但不希望被多线程共享

    场景

    1. 存储用户Session

    一个简单的用ThreadLocal来存储Session的例子:

    private static final ThreadLocal threadSession = new ThreadLocal();
     
        public static Session getSession() throws InfrastructureException {
            Session s = (Session) threadSession.get();
            try {
                if (s == null) {
                    s = getSessionFactory().openSession();
                    threadSession.set(s);
                }
            } catch (HibernateException ex) {
                throw new InfrastructureException(ex);
            }
            return s;
        }
    

    2.数据库连接,处理数据库事务
    3.数据跨层传递(controller,service, dao)
    每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
    例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
    在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
    比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

    package com.kong.threadlocal;
     
     
    public class ThreadLocalDemo05 {
        public static void main(String[] args) {
            User user = new User("jack");
            new Service1().service1(user);
        }
     
    }
     
    class Service1 {
        public void service1(User user){
            //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
            UserContextHolder.holder.set(user);
            new Service2().service2();
        }
    }
     
    class Service2 {
        public void service2(){
            User user = UserContextHolder.holder.get();
            System.out.println("service2拿到的用户:"+user.name);
            new Service3().service3();
        }
    }
     
    class Service3 {
        public void service3(){
            User user = UserContextHolder.holder.get();
            System.out.println("service3拿到的用户:"+user.name);
            //在整个流程执行完毕后,一定要执行remove
            UserContextHolder.holder.remove();
        }
    }
     
    class UserContextHolder {
        //创建ThreadLocal保存User对象
        public static ThreadLocal<User> holder = new ThreadLocal<>();
    }
     
    class User {
        String name;
        public User(String name){
            this.name = name;
        }
    }
     
    执行的结果:
     
    service2拿到的用户:jack
    service3拿到的用户:jack
    

    4.Spring使用ThreadLocal解决线程安全问题
    我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

    一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示。


    ThreadLocal变量

    这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

    下面的实例能够体现Spring对有状态Bean的改造思路:
    代码清单9-5 TopicDao:非线程安全

    public class TopicDao {
       //①一个非线程安全的变量
       private Connection conn; 
       public void addTopic(){
            //②引用非线程安全变量
           Statement stat = conn.createStatement();
           …
       }
    

    由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

    代码清单9-6 TopicDao:线程安全

    import java.sql.Connection;
    import java.sql.Statement;
    public class TopicDao {
     
      //①使用ThreadLocal保存Connection变量
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
    public static Connection getConnection(){
             
            //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
            //并将其保存到线程本地变量中。
    if (connThreadLocal.get() == null) {
                Connection conn = ConnectionManager.getConnection();
                connThreadLocal.set(conn);
                  return conn;
            }else{
                  //③直接返回线程本地变量
                return connThreadLocal.get();
            }
        }
        public void addTopic() {
     
            //④从ThreadLocal中获取线程对应的
             Statement stat = getConnection().createStatement();
        }
    

    不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

    当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

    ThreadLocal使用原理

    ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal, 但是实际上使用的值value却是自己独有的一份。用“ThreadLocal引用关系图”直接表示threadlocal 的使用方式。


    ThreadLocal引用关系

    从图中我们可以当线程使用threadlocal 时,是将threadlocal当做当前线程thread的属性ThreadLocalMap 中的一个Entry的key值,实际上存放的变量是Entry的value值,我们实际要使用的值是value值。value值为什么不存在并发问题呢,因为它只有一个线程能访问。threadlocal我们可以当做一个索引看待,可以有多个threadlocal 变量,不同的threadlocal对应于不同的value值,他们之间互不影响。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

    ThreadLocal 内存泄露的原因

    Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。可以看图“ThreadLocal引用关系”

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    

    主要两个原因

    • 没有手动删除这个 Entry
    • CurrentThread 当前线程依然运行

    第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。

    第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。

    综上, ThreadLocal 内存泄漏的根源是:
    由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。

    为什么不将key设置为强引用

    key 如果是强引用

    那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
    假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。请结合图“ThreadLocal引用关系”看

    那么为什么 key 要用弱引用

    事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

    如何正确的使用ThreadLocal

    • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

    参考链接:理解ThreadLocal

    共享变量的内存可见性问题详解

    并发和并行

    并发是指同一时间段内多个任务同时都在执行,并且都没有执行结束,而并行是在说单位时间内多个任务同时在执行。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累计而成,所以说并发的多个任务在单位时间内不一定同时在执行。

    单核CPU:单核就是CPU集成了一个运算核心,在工作期间只能执行某一个程序,处理多个程序时,只能分时(时间片的概念)处理。现在推出的CPU基本没有单核CPU了。
    多核CPU:在一颗芯片里集成了多个CPU运算核心,相当于多个单核CPU同时工作。因此,多核处理器可以同时处理多个程序,而不用等上一个程序完成。

    例如使用单核CPU,多个工作任务是以并发方式运行的,因为只有一个CPU,各个任务分别占用一段时间,再切换到其他任务,等到下一次CPU使用权是再次执行未完成的任务。

    使用多核CPU时,可以将任务分配到不同的核同时运行,实现并行。

    共享资源

    所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。


    线程与共享变量资源

    线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

    Java共享变量的内存可见性问题

    Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量赋值到自己的工作空间或者叫作工作内存,线程读变量时操作的是自己工作内存中的变量。


    主内存和工作内存

    Java内存模型是一个抽象的概念,在实际实现中线程的工作内存如下:


    工作内存

    上图所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java内存模型里面的工作内存,就对应上图中的L1或者L2缓存或者CPU的寄存器。

    当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完成之后将变量值更新到主内存。

    假入线程A和线程B同时处理一个共享变量,且使用不同CPU执行,并且当前两级Cache都为空,那么这时候由于Cache的存在,将会导致内存不可见问题:

    • 线程A首先获取共享变量X的值,由于两级Cache都没命中,所以加载主内存中X的值,假如为0.然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都为1
    • 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里都是正常的,因为这时候主内存中也是X=1.然后线程B修改X的值为2,并将其存放到线程B所在的一级Cache和共享Cache中,最后更新主内存中X的值为2
    • 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=1,到这里就出现问题了,命名线程B已经把X的值修改为2了,但是线程A获取的还是1
      这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见

    解决办法:synchronized关键字和volatile关键字

    synchronized关键字

    synchronized关键字介绍

    synchronized块是java提供的一种原子性内置锁,Java种的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。

    内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

    另外,由于java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户状态切换到内核状态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文的切换

    知识扫盲:start
    ①内核态
    CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU也可以将自己从一个程序切换到另一个程序。
    ②用户态
    只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其他程序获取。

    之所以会有这样的区分,是为了防止用户进程获取别的程序的内存数据,或者获取外围设备的数据。

    Synchronized原本是依赖操作系统实现的,因此在使用synchronized同步锁的时候需要进行用户态到内核态的切换。简单来说,在JVM中monitor enter和monitor exit字节码是依赖于底层操作系统的Mutex Lock来实现,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。
    知识扫盲:end

    synchronized的内存语义

    synchronized的一个内存语义可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到内存。

    除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。另外请注意,synchronized关键字会引起线程上下文切换并带来线程调度开销。

    volatile关键字

    volatile关键字介绍

    对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用volatile关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个线程被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

    volatile内存语义

    volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)

    // synchronized同步方式
    public class ThreadSafeInteger {
        private int value;
        
        public synchronized int get() {
            return value;
        }
     
        public synchronized void set(int value) {
            this.value = value;
        }
    }
     
    //volatile同步方式
    public class ThreeadSafeInteger2 {
        private volatile int value;
        
        public int get() {
            return value;
        }
     
        public void set(int value) {
            this.value = value;
        }
    }
    

    这里使用synchronized和使用volatile是等价的,都解决了共享变量value的内存可见性问题,但是前者是独占锁,同时只能有一个线程调用get()方法,其他调用线程会被阻塞,同时会存在线程上下文切换和线程重新调度的开销,这也是使用锁方式不好的地方。而后者是非阻塞算法,不会造成线程上下文切换的开销。

    volatile虽然提供了可见性保证,但并不保证操作的原子性。

    volatile顺序一致性

    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为如下三种:

    指令重排

    1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
    当变量声明为volatile时,Java编译器在生成指令序列时,会插入内存屏障指令。通过内存屏障指令来禁止重排序。
    JMM内存屏障插入策略如下:
    在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障。
    在每个volatile读操作后面插入一个LoadLoad,LoadStore屏障。

    Volatile写插入内存屏障后生成指令序列示意图:

    指令执行顺序-写内存屏障

    Volatile读插入内存屏障后生成指令序列示意图:

    指令执行顺序-读内存屏障

    通过上面这些我们可以得出如下结论:编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。

    防止重排序使用案例:

    public class SafeDoubleCheckedLocking {
        private volatile static Instance instane;
        public  static Instance getInstane(){
            if(instane==null){
                synchronized (SafeDoubleCheckedLocking.class){
                    if(instane==null){
                        instane=new Instance();
                    }
                }
            }
            return instane;
        }
    }
    

    创建一个对象主要分为如下三步:

    分配对象的内存空间。
    初始化对象。
    设置instance指向内存空间。
    如果instane 不加volatile,上面的2,3可能会发生重排序。假设A,B两个线程同时获取,A线程获取到了锁,发生了指令重排序,先设置了instance指向内存空间。这个时候B线程也来获取,instance不为空,这样B拿到了没有初始化完成的单例对象(如下图)


    实例内存空间

    参考链接:

    共享变量的内存可见性问题详解
    单核CPU与多核CPU,进程与线程,程序并发执行?
    Synchronized关键字
    Volatile关键字

    原子性操作

    jmm的8大原子操作

    原子操作图

    内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

    • lock(锁定)

      作用于主内存,

      它把一个变量标记为一条线程独占状态;

    • read(读取)

      作用于主内存,

      它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;

    • load(载入)

      作用于工作内存,

      它把read操作的值放入工作内存中的变量副本中;

    • use(使用)

      作用于工作内存,

      它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;

    • assign(赋值)

      作用于工作内存,

      它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;

    • store(存储)

      作用于工作内存,

      它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;

    • write(写入)

      作用于主内存,

      它把store传送值放到主内存中的变量中。

    • unlock(解锁)

      作用于主内存,

      它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

    JMM对这八种指令的使用,制定了如下规则:

    • 不允许readloadstorewrite操作之一单独出现。即使用了read必须load,使用了store必须write

    • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。

    • 不允许一个线程将没有assign的数据从工作内存同步回主内存。

    • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作。

    • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解

      锁。

    • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。

    • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。

    • 对一个变量进行unlock操作之前,必须把此变量同步回主内存。

    提问:程序不知道主内存的值被修改过了,该怎么办?


    内存值修改,程序同步本地变量

    参考链接

    8大原子性操作

    CAS操作

    1.cas的定义
    cas为compare and swap的缩写:比较与交换

    CAS(V,A,B)
    1:V 表示内存中的地址
    2:A 表示预期值
    3:B 表示要修改的新值
    

    2.cas的底层原理
    cas为什么能够保证原子性:

    • 底层用的Unsafe类进行操作
    • Unsafe类是cas的核心类,java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。 Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的方法
    • 注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底昃资源执行相应任务,Unsafe类中的native方法是调用底层原语,原语是有原子性的

    3.cas的问题

    • 循环时间长,开销比较大

    CAS是Java乐观锁的一种实现机制,在Java并发包中,大部分类就是通过CAS机制实现的线程安全,它不会阻塞线程,如果更改失败则可以自旋重试,允许多线程并发修改,但是互相比较,互相比较以后直到全部的线程执行成功,并发性加强了,但是循环时间长,开销大。

    解决办法:JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用:
    (1)第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源
    (2)第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)
         而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
    
    • 只能保证一个共享变量的原子操作

    对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

    解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,
            你可以把多个变量放在一个对象里来进行CAS操作。
    

    ABA问题

    1.ABA现象
    ABA问题就是 讲桌上面放了1瓶水,张三有10秒的操作时间,他先把水换成了 水果,用了2秒,接着他又把水果换成了水,尽管最后的结果没有发生改变,但是这之间有很多次的操作机会,所以就造成了漏洞,也就是常说的狸猫换太子

    2.如何解决ABA问题

    • 加版本管理

    • 使用Atomic*原子性操作

    使用 java提供的类:java.util.concurrent.atomic.AtomicInteger


    原子性操作类

    参考链接

    CAS ABA问题

    Unsafe类

    参考链接:https://zhuanlan.zhihu.com/p/368970745

    伪共享

    MESI 缓存一致性协议

    由于 CPU 和内存的速度差距太大,为了拉平两者的速度差,现代计算机会在两者之间插入一块速度比内存更快的高速缓存,CPU 缓存是分级的,有 L1 / L2 / L3 三级缓存。

    由于单核 CPU 的性能遇到瓶颈(主频与功耗的矛盾),芯片厂商开始在 CPU 芯片里集成多个 CPU 核心,每个核心有各自的 L1 / L2 缓存。其中 L1 / L2 缓存是核心独占的,而 L3 缓存是多核心共享的。为了保证同一份数据在内存和多个缓存副本中的一致性,现代 CPU 会使用 MESI 等缓存一致性协议保证系统的数据一致性。

    缓存一致性问题 MESI协议

    什么是伪共享?

    基于局部性原理的应用,CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,每一小块数据也叫 CPU 缓存行(CPU Cache Line)。

    在并行场景中,当多个处理器核心修改同一个缓存行变量时,有 2 种情况:

    • 情况 1 - 修改同一个变量: 两个处理器并行修改同一个变量的情况,CPU 会通过 MESI 机制维持两个核心的缓存中的数据一致性(Conherence)。简单来说,一个核心在修改数据时,需要先向所有核心广播 RFO 请求,将其它核心的 Cache Line 置为 “已失效”。其它核心在读取或写入 “已失效” 数据时,需要先将其它核心 “已修改” 的数据写回内存,再从内存读取;
      事实上,多个核心修改同一个变量时,使用 MESI 机制维护数据一致性是必要且合理的。但是多个核心分别访问不同变量时,MESI 机制却会出现不符合预期的性能问题。

    • 情况 2 - 修改不同变量: 两个处理器并行修改不同变量的情况,从程序员的逻辑上看,两个核心没有数据依赖关系,因此每次写入操作并不需要把其他核心的 Cache Line 置为 “已失效”。但从 CPU 的缓存一致性机制上看,由于 CPU 缓存的颗粒度是一个个缓存行,而不是其中的一个个变量。当修改其中的一个变量后,缓存控制机制也必须把其它核心的整个 Cache Line 置为 “已失效”。
      在高并发的场景下,核心的写入操作就会交替地把其它核心的 Cache Line 置为失效,强制对方刷新缓存数据,导致缓存行失去作用,甚至性能比串行计算还要低。

    这个问题我们就称为伪共享问题。


    伪共享

    参考链接

    小林coding-伪共享
    伪共享

    互斥锁(独占锁)和共享锁

    独占锁:指该锁只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
    共享锁:指该锁可被多个线程所持有。进行变量的读取和使用

    读写锁

    读写锁ReentrantReadWriteLock的原理

    解决线程安全问题使用ReentrantLock就可以了,但是ReentrantLock是独占锁,某一时刻只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。

    ReentrantReadWriteLock类图

    ReentrantReadWriteLock类图

    由类图可知,读写锁内部维护了一个ReadLock和一个WriteLock,他们依赖Sync实现具体功能,而Sync继承自AQS,并且提供了公平和非公平的实现。

    源码解读

    先看下ReentrantReadWriteLock类的整体结构

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
       
        private final ReentrantReadWriteLock.ReadLock readerLock;//读锁?
     
        private final ReentrantReadWriteLock.WriteLock writerLock;//写锁?
        
        final Sync sync;
    
        public ReentrantReadWriteLock() {//使用默认(非公平)的排序属性创建一个新的ReentrantReadWriteLock
            this(false);
        }
        public ReentrantReadWriteLock(boolean fair) {//使用给定的公平策略创建一个新的ReentrantReadWriteLock
            sync = fair ? new FairSync() : new NonfairSync();
            readerLock = new ReadLock(this);
            writerLock = new WriteLock(this);
        }
    
        public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }//返回用于写入操作的锁?
    
        public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }//返回用于读取操作的锁?
    
        abstract static class Sync extends AbstractQueuedSynchronizer { ....
    
        static final class NonfairSync extends Sync {....}//非公平策略
    
        static final class FairSync extends Sync {.... }//公平策略
    
        public static class ReadLock implements Lock, java.io.Serializable {}//读锁?
           ....
        public static class WriteLock implements Lock, java.io.Serializable {}//写锁?
           ....
    
    }
    
    ReentrantReadWriteLock类的继承关系

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}

    public interface ReadWriteLock {
      
        Lock readLock();
    
        Lock writeLock();
    }
    

    ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口规范了读写锁方法,具体操作由子类去实现,同时还实现了Serializable接口,表示可以进行序列化操作。

    Sync类的继承关系
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    

    Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。

    Sync类的内部类

    Sync类的内部类存在两个,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用,HoldCounter源码如下

    static final class HoldCounter {//计数器
        int count = 0;//计数
        // Use id, not reference, to avoid garbage retention
        final long tid = getThreadId(Thread.currentThread());//线程id
    }
    

    HoldCounter有两个属性,count和tid,其中count表示某个读线程重入次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。

    ThreadLocalHoldCounter源码如下:

    static final class ThreadLocalHoldCounter
        extends ThreadLocal<HoldCounter> {//本地线程计数器
        public HoldCounter initialValue() {//重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter的值
            return new HoldCounter();
        }
    }
    

    ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没用进行set的情况下,get到的均是initialValue方法里面生成的那个HoldCounter对象。

    Sync类的属性
    abstract static class Sync extends AbstractQueuedSynchronizer {
        //版本序号
        private static final long serialVersionUID = 6317671515068378041L;
        //高16位为读锁,低16位为写锁
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        //本地线程计数器
        private transient ThreadLocalHoldCounter readHolds;
        //缓存计数器
        private transient HoldCounter cachedHoldCounter;
        //第一个读线程
        private transient Thread firstReader = null;
        //第一个读线程的计数
        private transient int firstReaderHoldCount;
    

    该属性中包括了读锁,写锁线程的最大量。本地线程计数器等。

    Sync类的构造函数
    //构造函数
    Sync() {
        //本地线程计数器
        readHolds = new ThreadLocalHoldCounter();
        //设置AQS的状态
        setState(getState()); // ensures visibility of readHolds
    }
    

    在Sync的构造函数中设置了本地线程计数器和AQS的状态state。

    读写锁的状态设计

    读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。

    读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读状态,也就是获取到读锁的次数,低16位表示获取到写线程的可重入次数。

    读写锁32位状态

    假设当前同步状态值为S,get和set的操作如下:
    (1)获取写状态:
    S&0x0000FFFF:将高16位全部抹去
    (2)获取读状态:
    S>>>16:无符号补0,右移16位
    (3)写状态加1:
    S+1
    (4)读状态加1:
      S+(1<<16)即S + 0x00010000
    在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。

    写锁的获取与释放

    看下WriteLock类中的lock和unlock方法:

    WriteLock+ReadLock

    public void lock() {
    sync.acquire(1);
    }
    public void unlock() {
    sync.release(1);
    }

    可以看到就是调用独占式同步状态的获取与释放,因此真实的实现就是Sync的tryAcquire和tryRelease。

    锁的获取,看下tryAcquire

    tryAcquire
    protected final boolean tryAcquire(int acquires) {
        //当前线程
        Thread current = Thread.currentThread();
        //获取状态
        int c = getState();
        //写线程数量(即获取独占锁的重入数)
        int w = exclusiveCount(c);
        //c!=0说明读锁或者写锁已经被某线程获取
        if (c != 0) {
            //w=0说明已经有线程获取了读锁返回false,w!=0并且当前线程不是写锁的拥有者,则返回false
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            //说明当前线程获取了写锁,判断可重入次数(最大次数65535)
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            //次数当前线程已经持有写锁,设置可重入次数
            setState(c + acquires);
            return true;
        }
        //到这里说明此时c=0,读锁和写锁都没有被获取,writerShouldBlock表示是否阻塞
        if (writerShouldBlock() ||
            !compareAndSetState(c, c + acquires))
            return false;
        //设置锁位当前线程所持有
        setExclusiveOwnerThread(current);
        return true;
    }
    

    获取的状态被volatile关键字修饰使得各个线程都可见

     int c = getState();
    

    判断是公平的还是非公平的

    writerShouldBlock()
    

    核心方法,使用比较赋值的方式来操作,采用自旋来处理,底层使用unsafe保证原子性(面试说的cas+status就是指这块)

    protected final boolean compareAndSetState(int expect, int update) {
            // See below for intrinsics setup to support this
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }
    

    锁的释放,看下tryRelease

    tryRelease

    源码如下,将状态变更为nextc并赋值现在的状态

    protected final boolean tryRelease(int releases) {
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                int nextc = getState() - releases;
                boolean free = exclusiveCount(nextc) == 0;
                if (free)
                    setExclusiveOwnerThread(null);
                setState(nextc);
                return free;
            }
    

    参考链接

    读写锁

    乐观锁与悲观锁

    乐观锁

    什么是乐观锁

    乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);

    特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁。

    版本号机制实现乐观锁

    版本号机制实现乐观锁一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则将会重试更新操作,直到更新成功。
    这里举一个简单的例子进行说明:

    假设数据库中帐户信息表中有一个 version 字段,当前值为 1.0 ;而当前帐户余额字段(balance)为 $10000。

    • ① 操作员 A 此时将其读出(version=1.0),并从其帐户余额中扣除 8000(10000-$8000)。
    • ② 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1.0),并从其帐户余额中扣除 5000(10000-$5000)。
    • ③ 操作员 A 完成了修改工作,将数据版本号加一(version=1.1),连同帐户扣除后余额(balance=$8000),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为1.1 。
    • ④ 操作员 B 试图向数据库提交数据(balance=$5000),但此时比对数据库记录版本时发现,操作员 B 读到的数据版本号为 1.0 ,数据库记录当前版本为 1.1 ,不满足 “ 读取到的 version 值与当前数据库中的 version 值相等 “ 的乐观锁策略,
    • 因此,操作员 B 的提交被驳回。这样就避免了操作员 B 用基于 version=1.0 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
    version版本执行流程

    Java中CAS算法实现乐观锁

    CAS:Compare and Swap。比较并交换的意思。CAS操作有3个基本参数:内存地址A,旧值B,新值C。它的作用是将指定内存地址A的内容与所给的旧值B相比,如果相等,则将其内容替换为指令中提供的新值C;如果不等,则更新失败。类似于修改登陆密码的过程。当用户输入的原密码和数据库中存储的原密码相同,才可以将原密码更新为新密码,否则就不能更新。

    乐观锁的缺点

    • 开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
    • ABA问题:当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。
    • 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。

    悲观锁

    什么是悲观锁

    悲观锁,顾名思义就是总是假设最坏的情况,每次获取数据的时候都认为别人会修改,所以每次在获取数据的时候都会上锁,这样别人想获取这个数据就会阻塞直到它拿到锁后才可以获取(共享资源每次只给一个线程使用,其它线程阻塞,当前线程用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 Lock 等锁就是悲观锁思想的实现。

    数据库悲观锁

    以MySQL InnoDB为例:

    商品t_phone表中有一个字段status,status为1代表商品未售空,status为2代表商品已经售空,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

    如果不采用锁,那么操作方法如下:

    //1.查询出商品信息
    select status from t_phone where id=1;
    //2.根据商品信息生成订单
    insert into t_orders (id,phone_id) values (null,1);
    //3.修改商品status为2
    update t_phone set status=2;

    上面这种场景在高并发访问的情况下很可能会出现问题。前面已经提到,只有当phone status为1时才能对该商品下单,上面第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把phone status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。

    使用悲观锁来实现:

    在MySQL数据库中要使用悲观锁就必须关闭MySQL自动提交的属性我们可以使用命令设置MySQL为非autocommit模式:

    set autocommit=0;
    设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:
    //0.开始事务
    begin;
    //1.查询出商品信息
    select status from t_phone where id=1 for update;
    //2.根据商品信息生成订单
    insert into t_orders (id,phone_id) values (null,1);
    //3.修改商品status为2
    update t_phone set status=2;
    //4.提交事务
    commit;

    与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_phone表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

    参考链接

    乐观锁和悲观锁

    公平锁和非公平锁

    什么是公平锁

    多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

    • 优点:所有的线程都能得到资源,不会饿死在队列中。
    • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

    什么是非公平锁

    多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

    • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
    • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

    大家经常使用的ReentrantLock中就有相关公平锁,非公平锁的实现了。

    大家还记得我在乐观锁、悲观锁章节提到的Sync类么,是ReentrantLock他本身的一个内部类,他继承了AbstractQueuedSynchronizer,我们在操作锁的大部分操作,都是Sync本身去实现的。
    Sync呢又分别有两个子类:FairSync和NofairSync。具体实现还需要点进去跟一下代码

    公平和非公平实现

    A线程准备进去获取锁,首先判断了一下state状态,发现是0,所以可以CAS成功,并且修改了当前持有锁的线程为自己。


    非公平锁

    这个时候B线程也过来了,也是一上来先去判断了一下state状态,发现是1,那就CAS失败了,真晦气,只能乖乖去等待队列,等着唤醒了,先去睡一觉吧。

    非公平锁

    A持有久了,也有点腻了,准备释放掉锁,给别的仔一个机会,所以改了state状态,抹掉了持有锁线程的痕迹,准备去叫醒B。

    非公平锁

    这个时候有个带绿帽子的仔C过来了,发现state怎么是0啊,果断CAS修改为1,还修改了当前持有锁的线程为自己。

    B线程被A叫醒准备去获取锁,发现state居然是1,CAS就失败了,只能失落的继续回去等待队列,路线还不忘骂A渣男,怎么骗自己,欺骗我的感情。

    非公平锁

    以上就是一个非公平锁的线程,这样的情况就有可能像B这样的线程长时间无法得到资源,优点就是可能有的线程减少了等待时间,提高了利用率。

    现在都是默认非公平了,想要公平就得给构造器传值true。

    ReentrantLock lock = new ReentrantLock(true);

    ReentrantLock公平锁设置

    线A现在想要获得锁,先去判断下state,发现也是0,去看了看队列,自己居然是第一位,果断修改了持有线程为自己。

    公平锁

    线程b过来了,去判断一下state,嗯哼?居然是state=1,那cas就失败了呀,所以只能乖乖去排队了。

    公平锁

    线程A暖男来了,持有没多久就释放了,改掉了所有的状态就去唤醒线程B了,这个时候线程C进来了,但是他先判断了下state发现是0,以为有戏,然后去看了看队列,发现前面有人了,作为新时代的良好市民,果断排队去了。

    公平锁

    线程B得到A的召唤,去判断state了,发现值为0,自己也是队列的第一位,那很香呀,可以得到了。

    公平锁

    参考链接

    公平锁和非公平锁

    可重入锁

    什么是可重入锁

    可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁锁同步着的代码块

    解析可重入锁

    拿ReentrantLock来说,Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或再入锁。

    重入到哪里:进入同步域(及同步代码块/方法或显示锁锁定的代码)

    在Java中,除了ReentrantLock(显式的可重入锁)以外,synchronized也是重入锁(隐式的可重入锁)

    ReenTrantLock可重入锁和synchronized的区别

    可重入性

    从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

    拓展:synchronized锁的实现方式并不是通过计数器来实现的。它是通过线程持有的对象监视器(也可以称为锁对象)来实现的。当线程请求进入一段同步代码时,如果该锁没有被其他线程占用,那么该线程会获得该锁,同时锁对象的计数器会+1。

    锁的实现

    Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

    性能区别

    在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

    功能区别

    便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

    锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

    ReenTrantLock独有的能力

    • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
    • ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
    • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

    Synchronized可重入例子

    public class SyncLockDemo2 {
        public synchronized void add(){
            add();//递归调用add()
        }
        public static void main(String[] args) {
            new SyncLockDemo2().add();//对象调用递归方法
        }
    }
    

    此代码运行会产生可重入栈溢出的报错。

    ReentrantLock可重入例子

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReenterLockDemo {
        public static void main(String[] args) {
            //Lock演示可重入锁
            Lock lock = new ReentrantLock();
            //创建线程
            new Thread(()->{
                //要子线程单独执行的任务,此任务要上锁/加锁
                //加锁/上锁
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()
                    +"外层");
                    //内层
                    try {
                        lock.lock();//内层上锁
                        System.out.println(Thread.currentThread().getName()
                                +"内层");
                    }finally {
                        //此处缺少lock.unlock();
                    }
                }finally {
                    lock.unlock();//解锁/释放锁
                }
    
            },"t1").start();
            //另外创建新线程
            new Thread(()->{
                lock.lock();
                System.out.println("sss");
                lock.unlock();
            },"t2").start();
        }
    }
    

    输出:
    t1外层
    t1内层
    证明可以重入进入。

    不释放内层锁的代码:
    造成其他线程等待,程序不能结束,不能输出sss字符串:

    参考链接

    可重入锁

    自旋锁

    参考链接

    深入理解Linux内核之自旋锁

    AQS

    AQS简单介绍

    AQS 的全称为(AbstractQueuedSynchronizer),这个类java.util.concurrent.locks 包下面。~简单介绍嘛,就这些

    AQS原理

    AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

    CLH队列

    此处引用一下源码中Node的注释,Node有prev和next,同时被volatile修饰,可见CLH是一个虚拟的双向队列(虚拟的双向队列不存在实例,仅存在节点之前的关系)。AQS是将每条请求共享资源的线程封装成一个CLH队列中的一个Node节点来实现锁的分配。

    AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

    AQS status

    看到这个图应该比较熟悉了,volatile修饰的status使得线程之间是可见的,而compareAndSetState调用了unsafe的原子性操作。

    AQS模版方法

    AQS模版方法

    从源码可以看出AQS提供了tryAcquire和tryRelease方法,但是里面直接抛了异常。诶,熟悉不模版方法,父类不实现,由子类进行实现。

    Sync继承了AQS

    abstract static class Sync extends AbstractQueuedSynchronizer {...)
    
    FairSync实现了tryAcquire
    NonFairSync实现了tryAcquire
    NonFairTryAcquire实现

    然后就到了cas+status。

    相关文章

      网友评论

          本文标题:java-守护线程/JUC/Volatile/AQS/CAS/锁

          本文链接:https://www.haomeiwen.com/subject/bzniidtx.html