Java从线程安全到synchronized和Lock探索

大家好,我是一安,今天继续聊一下线程安全

什么是线程安全?

用《java concurrency in practice》中的一句话来表述:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。从这句话中我们可以知道几层意思:

  • 线程安全是和对象密切绑定的;
  • 线程的安全性是由于线程调度和交替执行造成的;
  • 线程安全的目的是实现正确的结果

避免线程安全问题

由于CPU的执行速度和内存的存取速度严重不匹配,为了优化性能及充分利用运算能力,基于时间局部性、空间局部性等局部性原理,CPU在和内存间增加了多层高速缓存,当需要取数据时,CPU会先到高速缓存中查找对应的缓存是否存在,存在则直接返回,如果不存在则到内存中取出并保存在高速缓存中。

现在多核处理器越基本已经成为标配,这时每个处理器都有自己的缓存,这就带来了缓存一致性的问题:cpu计算时数据读取顺序优先级:寄存器->高速缓存->内存,计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。

Java从线程安全到synchronized和Lock探索

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须是工作内存中进行,而不能直接读写主内存中的变量。线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。当多个线程同时读写某个内存数据时,各个线程都从主内存中获取数据,线程之间数据是不可见的,就可能会产生寄存器/高速缓存/内存之间的同步问题

如何破?

线程安全的前提是该变量是否被多个线程访问,只要有多于一个的线程操作给定的状态变量,此时就可能产生多线程问题。jvm层面避免线程安全问题主要围绕着并发过程中的原子性、可见性、有序性这三个特征

原子性

原子性就是操作不能被线程调度机制中断,要么全部执行完毕,要么不执行。

可见性

可见性就是一个线程对共享变量做了修改之后,其他的线程立即能够看到修改后的值。Java内存模型将工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

Java提供了volatile关键字来保证可见性。当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中,非volatile变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。而声明变量是volatile的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会跳过CPU cache这一步去内存中读取新值。volatile只确保了可见性,并不能确保原子性

有序性

为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,编译器和处理器常常会对指令做重排序。

有序性:即程序执行的顺序按照代码的先后顺序执行。CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。通过volatile关键字来保证一定的“有序性”,volatile关键字本身就包含了禁止指令重排序的语义。另外可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性

synchronized

synchronized能够把任何一个非null对象当成锁,实现由两种方式:

  • 类锁,当synchronized作用于静态方法时是给class加锁
  • 对象锁,当synchronized作用于一个对象实例时或非静态方法时

synchronized锁又称为对象监视器(object)

当多个线程一起访问某个对象监视器的时候,对象监视器会将这些请求存储在不同的容器

  • Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
  • Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中
  • Wait Set:哪些调用wait方法被阻塞的线程被放置在这里
  • OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
  • Owner:当前已经获取到所资源的线程被称为Owner
  • !Owner:当前释放锁的线程Java从线程安全到synchronized和Lock探索synchronized在jdk1.6之后提供了多种优化方案《你对Java中的锁了解多少,你又能说出几种锁?

lock

与synchronized不同的是lock是纯java手写的,与底层的JVM无关。在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReenTrantLock、ReadWriteLock(实现类有ReenTrantReadWriteLock),其实现都依赖AbstractQueuedSynchronizer类(简称AQS),实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。Java从线程安全到synchronized和Lock探索主要从以下几个特点介绍:

  • 可重入锁,如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
  • 可中断锁,顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
  • 公平锁和非公平锁,公平锁以请求锁的顺序来获取锁,非公平锁则是无法保证按照请求的顺序执行。synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。参数为true时表示公平锁,不传或者false都是为非公平锁。
  • 读写锁,读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

AQS:

从使用层面来讲,AQS 的功能分为两种:

  • 独占锁:每次只能由一个线程持有锁。ReentrantLock 就是以独占方式实现的互斥锁;
  • 共享锁:允许多个线程同时获取锁,并发访问共享资源。比如 ReentrantReadWriteLock;

AQS 内部变量:

变量名 说明 关注层级
state 同步状态,标识当前状态是锁定、还是非锁定 重点
head 指向 Node 节点,同步队列、等待队列的头节点指针 重点
tail 指向 Node 节点,同步队列、等待队列的尾节点指针 重点
unsafe Unsafe 类实例,实现线程 park、unpark 的关键 知道
stateOffset state 字段在实例中的便宜量,用于 CAS 操作 了解
headOffset head 字段在实例中的便宜量,用于 CAS 操作 了解
tailOffset tail 字段在实例中的便宜量,用于 CAS 操作 了解
nextOffset next 字段在实例中的便宜量,用于 CAS 操作 了解

工作原理介绍:

AQS 的实现依赖内部的 FIFO 的双向队列,如果当前线程竞争锁失败,那么AQS 会把当前线程以及等待状态信息构造成一个 Node 加入到这个队列中,同时调用 Unsafe 方法,使当前线程进入阻塞状态。当获取锁的线程释放了锁后,会从队列中唤醒下一个阻塞的节点(线程)Java从线程安全到synchronized和Lock探索

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点,所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。线程抢占的方式是通过 CAS 操作修改state的值,修改成功意味着当前线程抢占到了资源,修改失败的线程则加入 FIFO 队列,等待被唤醒。

Node 类组成如下:

static final class Node {
 static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    /**该节点由于超时或中断而被取消,当前节点的线程作废*/
    static final int CANCELLED =  1;
    /**表明当前节点的下一节点的线程将要进入阻塞状态,需要被 unparking 唤醒*/
    static final int SIGNAL    = -1;
    /**表明当前节点的线程 处于等待状态,也就是再等待队列,等待被通知*/
    static final int CONDITION = -2;
    
    static final int PROPAGATE = -3;
 /**CANCELLED、SIGNAL、CONDITION、PROPAGATE*/
    volatile int waitStatus;
    
    volatile Node prev; //前驱节点
    volatile Node next; //后继节点
    volatile Thread thread;//当前线程
    Node nextWaiter; //存储在condition队列中的后继节点
    
    // 是否为共享锁
    final boolean isShared() { 
     return nextWaiter == SHARED;
    }

 final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

 Node() {// Used to establish initial head or SHARED marker
    }
    
    // 将线程构造成一个Node,添加到等待队列
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 这个方法会在Condition队列使用
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

锁竞争、锁释放对队列的变化:

锁竞争:

新的线程没有抢到锁,加入 FIFO 队列Java从线程安全到synchronized和Lock探索线程 Thread0 抢占到了锁,线程 Thread1、Thread2、Thread3 抢占锁失败,进入 FIFO 同步队列,等待被唤醒


锁释放:

当锁释放的时候,会判断 head 节点的 waitStatus 的状态是不是 != 0,如果是,则说明 FIFO 队列有等待线程,唤醒Node1节点保存的线程(thread1)

threa0 释放了锁,判断 head 的 waitStatus = -1,则调用 unpack 唤醒第一个 node,也就是 thread1,这里 thread1 抢占到了锁Java从线程安全到synchronized和Lock探索

thread1未抢到锁,节点变化如下:Java从线程安全到synchronized和Lock探索threa0 释放了锁,判断 head 的 waitStatus = -1,则调用 unpack 唤醒第一个 node,也就是 thread1,这里 thread1 抢占到了锁失败,被新线程 thread3 抢占锁成功,那么 thread1 会重新进入 wait 状态,在 FIFO 同步队列中的位置是不变的

lock与synchronized区别

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 一个类
锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态 无法判断 可以判断
锁类型 可重入 不可中断 非公平 可重入 可判断 可公平(两者皆可)
性能 少量同步 大量同步

号外!号外!

如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!

Java从线程安全到synchronized和Lock探索

多线程+EasyExcel实现报表优雅导出


一文教你快速上手EasyExcel


如何快速搭建一套生产级RabbitMQ

Java从线程安全到synchronized和Lock探索


原文始发于微信公众号(一安未来):Java从线程安全到synchronized和Lock探索

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44590.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!