Java线程安全

本文为原创,如需转载请注明来处即可。

目录:Java线程安全

互斥同步

  • 定义
  • 具体实现
  • 字节码指令的具体执行过程
  • 字节码指令的2个注意点
  • 现代jdk juc包源码解读

非阻塞同步

  • 定义
  • 硬件指令集的发展
  • cas操作的底层实现
  • cas的语义漏洞
  • 使用场景分析

无需同步

  • 定义
  • 无需同步的2种方式
  • ThreadLocal类的源码分析和原理分析
  • 消费序列的架构分析
  • 使用场景解读

互斥同步


定义

互斥同步(mutual exclusion & synchronization)是常见的一种并发正确性保障手段。同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只能被一个线程(或者是一些,使用信号量的时候)使用。
互斥是实现同步的一种手段,如临界区,互斥量,信号量都是主要的互斥实现方式。
互斥是因,同步是果。互斥是方法,同步时目的。

具体实现

最基本的互斥实现手段就是synchronized关键字。synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit字节码指令,这两个字节码都需要一个reference类型的参数来指明锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class对象来作为锁对象。

字节码指令的具体执行过程

在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit时,将计数器减1,当为0时,锁就被释放。如果获取锁失败,那么当前线程就要阻塞等待,直到对象锁被另一个线程释放。

字节码指令的2个注意点

  1. synchronized同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的情况。
  2. 同步块在已进入线程执行完之前,会阻塞其他的线程进入。
  • tip : Java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,需要操作系统来完成,就需要从用户态转换为核心态,代价是很大的。所以synchronized是Java语言中的重量级操作。

现代jdk juc包源码解读

TODO:【探究】探究reentrantlock锁(可以和其他类型的锁一起总结研究)【源码级别】
TODO:【探索】深入探索juc包【源码级别】
  • ReentrantLock
    • 功能分析:
      • ReentrantLock 和 synchronized 有一样的可重入特性。代码写法上有区别,一个表现为api层面上的互斥锁(lock(),unlock()方法配合try/finally语句块来完成),一个是原生语法层面上的互斥锁。
      • 相比synchronized,ReentrantLock增加了一些高级特性,如:等待可中断,可实现公平锁,锁可以绑定多个条件。
        • 等待可中断:等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待锁的线程可以放弃等待,改为处理其他事情。
        • 可实现公平锁:公平锁是指多个线程在等待同一个锁时,必须按照锁的申请顺序来一次获得锁。ReentrantLock默认情况下时非公平的,可以通过带布尔型的构造函数要求使用公平锁。
        • 可以绑定多个条件:绑定多个条件是指一个ReentrantLock对象可以绑定多个condition对象。【使用synchronized如果需要绑定多个对象,就要添加锁】
    • 源码解读【TODO:可以单开一个po专门分析juc源码】
    • TIP:虚拟机在未来的性能改进中肯定会更加偏向于原生的synchronized,所以还是提倡synchronized能实现需求的情况下,优先使用synchronized【待验证】。

非阻塞同步


定义(与阻塞同步对比,做了哪些优化)

  • 阻塞同步:阻塞同步最主要的问题就是进行线程阻塞和环境带来的性能问题,因此这种同步也称为阻塞同步(blocking synchronization)。从处理问题的方式上看,互斥同步属于一种悲观的并发策略。
  • 非阻塞同步:随着硬件指令集的发展,我们有了另一个选择:基于冲突检测的乐观并发策略。通俗的讲,就是先进行操作,如果没有其他线程争用贡献数据,那么操作就成功了;如果共享数据有争用,产生了冲突,再采取其他的补偿措施(常见的补偿措施,不断重试,直到成功),这种乐观的并发的许多实现都不需要把线程挂起,因此这种同步成为非阻塞同步(Non-blocking synchronization)。

硬件指令集的发展

乐观并发策略得益于硬件指令集的发展,硬件保证一个从语义上看起来需要多次操作的行为只需要一条处理器指令集就可以完成,如下:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(swap)
  • 比较并交换(Compare-and-Swap,简称cas)
  • 加载链接/条件存储(LL/SC)

TIP:再IA64,x86指令集中有cmpxchg指令完成CAS操作。

cas操作的底层实现

  • cas指令需要3个操作数,分别是内存位置(再Java中可以简单的理解为变量的内存地址,用V表示),旧的预期值(A),和新值(B)。CAS指令执行时,当且仅当V的值符合旧的预期值A时,处理器用B更新A的值,否则不执行更新,但无论是否更新,返回的都是V的旧值。上述操作是一个原子操作。

TODO:【探索】从硬件指令集的角度探索cas的原子性。
  • CAS在Java中的实现:
    • 在jdk1.5后,Java才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的compareandswapint()等方法包装提供。虚拟机内部对这些方法多了特殊处理,即时编译之后就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认定是无条件内联进去的。

    TIP:Unsafe类不是提供给用户调用的类(unsafe.getunsafe()的代码中限制了只有启动类加载器(Bootstrap classloader)加载的class才能访问它【TODO:探索源码】),因此如果不适用反射手段,我们只能使用java api间接的访问它。如:juc包中的整个原子类,其中的compareandset()等方法都使用了unsafe的cas操作。
    TODO:探索源码】。

cas操作的语义漏洞

cas操作不是完美的,存在这样一个逻辑漏洞,简称 “ABA” 问题:

一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功

非阻塞算法使用场景分析

TODO:【探索】cas操作使用场景分析。

无需同步/无同步方案


定义

如果一个方法本身就不涉及共享数据,那就自然无需同步措施来保证正确性。

无需同步的2种方式

  • 可重入代码:

    这种代码也叫做纯代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会发生任何错误。

    所有的可重入代码都是线程安全的,但是并非所有线程安全的代码都是可重入的。可重入代码都有一些共同的特征,如不依赖堆上共享数据和公共的系统资源,用到的状态量是从参数传入,不调用非可重入的方法等。

    可以通过简单的原则来判断是否可重入性:如果一个方法,他的返回结果是可以预测的,只要输入了相同的数据,就可以返回相同的结果,那么他就可以满足可重入的要求,当然也就是线程安全的。

  • 线程本地存储:

    java.lang.ThreadLocal类可以实现线程本地存储的功能。每一个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了以 ThreadLocal.threadLocalHashCode为键,以本地线程变量为值得K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap访问入口,每一个ThreadLocal对象都包含了独一无二的threadLocalHashCode值,就可以找到对应的本地线程变量。

    大部分使用消费序列的架构模式都会使用线程本地存储。

ThreadLocal类的源码分析和原理分析

TODO:【探究】详细探究ThreadLocal原理及其源码实现,必要的话单开一个po。

消费序列的架构分析

TODO:【延申】尝试探究消息服务队列的线程模型以及消息架构。【参考rocketMQ,KAFKA】

使用场景解读

TODO:【延申】探索本地线程存储的使用场景,并解读。

参考:
《深入理解Java虚拟机》第二版
《Java并发编程之美》
《rocket mq实战》

你可能感兴趣的:(Java线程)