0
点赞
收藏
分享

微信扫一扫

蹊源的Java笔记—线程并发与线程安全


蹊源的Java笔记—线程并发与线程安全

前言

上期博客我们对线程与线程池相关知识进行了总结,实际上我们在使用线程的过程中有不得不提的问题就是线程并发与线程安全,我们在为了提升系统性能的时候,采用多线程的方式,就有可能会造成数据不安全,本篇文章蹊源将带领大家了解一下线程并发与线程安全相关的知识。

线程与线程池可参考我的博客:​​蹊源的Java笔记—线程与线程池​​

Spring知识可参考我的博客:​​蹊源的Java笔记—Spring​​

正文

线程并发

线程并发,指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个进程快速交替的执行。

线程并发可能会造成以下两种情况:

  • 线程安全问题: 多个线程同时操作共享变量,一定情况下会造成数据不准确问题。
  • 共享内存不可见性问题:由于 ​​Java​​​ 内存模型会造成一定程度内存不可见,一般使用​​volatile​​关键字来解决。

常见的并发包
​​​java.util.concurrent​​​:多线程并发包
​​​java.util.concurrent.atomic​​​:多线程的原子性操作提供的工具类的包
​​​java.util.concurrent.lock​​​: 多线程的锁机制的包
提供了3个接口:

  • Lock接口:支持锁规则
  • ReadWriteLock接口:定义了一些读取者可以共享而写入者独占的锁。
  • Condition接口:描述了可能与锁有关联的条件变量

CountDownLatch(线程计数器)和CyclicBarrier(回环栅栏)区别:

  • ​CountDownLatch​​​和​​CyclicBarrier​​​都是在​​java.util.concurrent​​下
  • ​CountDownLatch​​​用于主线程等待其他子线程任务都执行完毕后再执行,​​CyclicBarrier​​用于一组线程相互等待大家都达到某个状态后,再同时执行;
  • ​CountDownLatch​​​是不可重用的,​​CyclicBarrier​​可重用

Semaphore信号量

Semaphore信号量是 ​​java.util.concurrent​​​包下用来:限制线程并发的数量的工具类,基于​​AQS​​​实现的,在构造的时候会设置一个值,代表着资源数量。在请求资源调用​​task​​​时,会用自旋的方式减1,如果成功,则获取成功了,如果失败,导致资源数变为了0,就会加入队列里面去等待(这里也可以像​​reentrantLock​​​一样选择公平或者非公平的方式)。调用​​release​​的时候会加1,补充资源,并唤醒等待队列。

线程同步

线程同步是指多线程通过线程安全的机制来控制线程之间的执行顺序也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有实现线程同步的话,就会造成多线程下数据安全问题。

实现线程同步的6种方式:

  1. 使用​​Synchronized​​标记临界区
  2. 对变量使用​​volatile​​进行标记,可以实现原子操作的线程安全
  3. 使用锁机制
  4. 使用局部变量​​ThreadLocal​​实现线程同步
  5. 使用阻塞队列​​SyschronousQueue​​实现线程同步
  6. 使用原子变量实现线程同步

Java锁机制

乐观锁与悲观锁

  • 悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放. ​​sychronized​​提供的是悲观锁,适合写入频繁场景
  • 乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作.适合读取频繁场景

如何实现乐观锁:

  • 在数据库 添加​​version​​字段
  • 通过​​CAS​​​(自旋锁)操作:内存位置、期望原值、新值 (​​CAS​​​称为无锁栈、无锁队列 是一个原子性操作(一段内存同时只能有一个​​CPU​​​方位))​​update status=3 where id=111 and status=1;​​(增加筛选条件)

CAS操作是一种无锁的方式来实现线程安全的方式,​​java.util.concurrent.atomic​​​ 原子变量就是基于​​CAS​​操作实现的:

  • AtomicInteger
  • AtomicReference​:用于引用类
  • AtomicStampedReference​​:引入时间戳用于对时间敏感的引用类 ,它可以避免​​ABA​​问题
  • AtomicIntegerArray​:用于数组
  • AtomicIntegerFieldUpdater​​:借助反射机制,让普通变量也能使用原则操作,如​​User​​​类中的​​age​​;

CAS操作有哪些问题:

  • ​ABA​​问题,指的是修改后又被修改回来。
  • 每个线程都会尝试去执行,对​​CPU​​的消耗比较大
  • 只能保证一个共享变量的原子操作

自旋锁

自旋锁,是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁的状态是通过自旋锁的保持者来改变(命令模式).
举个例子:​​​while(true) for(;;)​​ 进入的就是一种自旋锁。

公平锁和非公平锁

公平锁和非公平锁的区别在于公平锁会保证先来的线程会先获取资源(其内部实现时​​AQS​​​原理),而非公平不能保证。公平锁的实现是通过​​FIFO​​​先进先出的队列来实现的,非公平锁会由​​JVM​​​就近安排线程获取资源的顺序,所以非公平锁的性能是由于公平锁的。
​​​sychronized​​​是非公平锁,​​ReenterLock​​(默认也是非公平锁)可以实现公平锁。

互斥锁

互斥锁: 同一时刻只能有一个线程获得互斥锁,其余线程处于睡眠状态.

自旋锁vs互斥锁

  • 互斥锁和自旋锁都能够保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
  • 自旋锁没有上下文的切换性能比较高,但是存在死锁的风险,只适用保持锁时间比较短的情况下。
  • 互斥锁会让获取不到资源的线程​​sleep​​​(不放弃锁,放弃对​​CPU​​​的争夺),进入线程阻塞状态。
    ​​​synchronized​​​和​​ReenterLock​​都是互斥锁。

可重入锁

可重入锁:不放弃锁,已经获得该锁的线程可以再次进入被该锁锁定的代码块。
可重入的原理是通过计数器来实现的:同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
​​​synchronized​​​和​​ReenterLock​​都是可重入锁。

sychronized同步锁

sychronized的作用是:用来确保多线程下的线程安全。

sychronized使用方法

  • 对象锁:用​​sychronized​​​修饰代码块 (手动去指定锁对象 入口​​monitorenter​​​ 出口​​monitorexit​​ )
  • 类锁:用​​sychronized​​​修饰普通方法(类锁只能在同一时刻被一个对象拥有 通过方法的 ​​ACC_SYNCHRONIZED​​​ 标志符是否被设置,这里会隐形调用​​monitorenter​​​、​​monitorexit​​这两个指令 )

sychronized实现原理

monitorenter指令(入口):每个对象都会关联一个监视器锁(​​monitor​​​) ,当​​monitor​​​被占用时就会处于锁定状态,线程执行​​monitorenter​​​指令时尝试获取​​monitor​​的所有权,过程如下:

  1. 如果​​monitor​​​的进入数为0,则该线程进入​​monitor​​​,然后将进入数设置为1,该线程即为​​monitor​​的所有者;
  2. 如果线程已经占有该​​monitor​​​,只是重新进入,则进入​​monitor​​的进入数加1;
  3. 如果其他线程已经占用了​​monitor​​​,则该线程进入阻塞状态,直到​​monitor​​​的进入数为0,再重新尝试获取​​monitor​​的所有权;

monitorexit指令(出口):执行​​monitorexit​​​的线程必须是​​objecter​​​所对应的​​monitor​​​的所有者。指令执行时,​​monitor​​​的进入数减1,如果减1后进入数为0,那线程退出​​monitor​​​,不再是这个​​monitor​​​的所有者。其他被这个​​monitor​​​阻塞的线程可以尝试去获取这个 ​​monitor​​ 的所有权。

sychronized锁的状态

  1. 无锁状态
  2. 偏向锁状态:第一个线程获取该锁,存储该线程​​id​​​,并改变对象的对象头​​MarkWord​​​中的状态。 偏向锁可以撤销成无锁状态, 线程是不会主动释放偏向锁,需要等待其他线程来竞争,这种机制可以减小只有一个线程下,​​CAS​​操作的性能消耗
  3. 轻量级锁状态:第二个线程尝试获取该锁,当检测到对象头​​MarkWord​​已存在偏向锁,进入自旋。
  4. 重量级锁状态:线程自旋失败,进入阻塞状态。

知识点:

  • 锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
  • 偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(​​CAS​​)的开销,看起 来让这个线程得到了偏护。
  • 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进 一步提高性能。

sychronized在jdk1.6之后的优化

  • 自旋锁
  • 自适应自旋锁:根据自旋状况,自动调整自旋的次数。(​​jdk1.6​​​默认是10次,​​jdk1.7​​​之后由​​JVM​​决定)
  • 锁消除: ​​JVM​​​检测到不可能存在共享数据竞争,这是​​JVM​​会对这些同步锁进行锁消除(无锁状态)
  • 锁粗化: ​​JVM​​​检测到同一个对象有连续的加锁、解锁操作,会合并成为一个更大范围的加锁、解锁操作,例如将加锁解锁操作移到​​for​​循环外面。
  • 轻量级锁
  • 偏向锁
  • 重量级锁

知识点:

  1. ​sychronized​​锁升级到重量级锁时就是进入不可逆状态。
  2. 对象头中的​​MarkWord​​记录了对象和锁有关的信息

ReentrantLock同步锁

​ReentantLock​​​ 继承接口 ​​Lock​​​ 并实现了接口中定义的方法,它是一种可重入锁,除了能完成​​synchronized​​ 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等 避免多线程死锁的方法。

​ReentrantLock​​则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现,防止出现死锁的方法有:

  • 响应中断​​lockInterruptibly()​
  • 可轮询锁​​tryLock()​
  • 定时锁​​tryLock( long time)​

​ReentrantLock​​获取锁的几个方式:

  • ​tryLock​​​ 能获得锁就返回 ​​true​​​,不能就立即返回 ​​false​​;
  • ​tryLock(long timeout,TimeUnit unit)​​​,可以增加时间限制,如果超过该时间段还没获得锁,返回 ​​false​​;
  • ​lock​​​ 能获得锁就返回 ​​true​​,不能的话一直等待获得锁;
  • ​lockInterruptibly​​​能获得锁就返回 ​​true​​​,不能话一直等待获得锁,但等待的过程如果发生​​interrupt()​​​中断操作,​​lock​​​不会抛异常,​​lockInterruptibly​​会抛异常。

ReentrantLock原理

ReentrantLock主要利用​​CAS+AQS​​队列来实现的:

  • 先通过​​CAS​​​尝试获取锁, 如果此时已经有线程占据了锁,那就加入​​AQS​​队列并且被挂起;
  • 当锁被释放之后, 公平锁的情况下:排在队首的线程会被唤醒​​CAS​​​再次尝试获取锁,非公平锁下由​​JVM​​就近决定哪个线程获取资源。

ReentrantLock和synchronized

相同点

  1. 都是同步锁,用来确保多线程下数据安全
  2. 都是互斥锁、可重入锁 ,获取该锁的线程可以再次进入被该锁锁定的代码块。

不同点

  1. ​synchronized​​​是非公平锁,​​ReentrantLock​​可以是非公平锁,也可以是公平锁。
  2. ​ReentrantLock​​​ 是 ​​API​​​ 级别的,​​synchronized​​​ 是 ​​JVM​​​ 级别的,​​synchronized​​​的加锁解锁都是由​​JVM​​​来控制,​​ReentrantLock​​需要自己来控制。
  3. ​ReentrantLock​​​ 通过 ​​Condition​​​ 可以绑定多个条件,从而实现 分组需要唤醒的线程们,可以精确唤醒,而不像​​synchronized​​那样随便唤醒一个线程或者全部线程。
  4. ​synchronized​​​不可中断,除非抛出异常或者正常运行完成; ​​ReentrantLock​​​可中断, 调用​​interrupt()​​方法可中断
  5. ​ReentrantLock​​​则需要用户手动去释放锁,若没有主动释放锁,就有可能导致出现死锁现;​​synchronized​​​不需要用户手动去释放锁,当​​synchronized​​代码执行完成后,系统会自动让线程释放对锁的占用;

ReadWriteLock读写锁

​ReadWriteLock​​读写锁:

  • 读写锁的规则是“ 读读不互斥,读写互斥,写写互斥”,所以​​ReadWriteLock​​ 读锁相当于一个共享锁,写锁相当于互斥锁。
  • ​ReentrantLock​​​为独占锁, 每次只能有一个线程能持有锁,​​ReentrantLock​​就是通过独占锁的方式来实现互斥的。

读写锁是基于​​AQS​​原理

  • ​AQS​​​是将每一条请求共享资源的线程封装成一个​​CLH​​​锁队列的一个结点(​​Node​​),来实现锁的分配。
  • ​AQS​​​就是基于​​CLH​​​队列,用​​volatile​​​修饰共享变量​​state​​​,线程通过​​CAS​​去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
  • ​CLH​​​队列是一个虚拟的双向队列(​​FIFO​​先进先出),虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。

蹊源的Java笔记—线程并发与线程安全_线程并发

​AQS(AbstractQueuedSynchronizer)​​​这个抽象类,借助模板模式,提供了以下方法自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,​​AQS​​已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到​​condition​​才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回​​true​​​,失败则返回​​false​​。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回​​true​​​,失败则返回​​false​​。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回​​true​​​,否则返回​​false​​。

知识点:

1.为什么Java中的long与double可能存在线程安全的问题
除了​​​long​​​和​​double​​​类型,​​Java​​​基本数据类型都是的简单读写都是原子的,而简单读写就是赋值和​​return​​语句。

目前的​​JVM​​​(​​java​​​虚拟机)都是将32位作为原子操作,并非64位。当线程把主存中的 ​​long/double​​​类型的值读到线程内存中时,可能是两次32位值的写操作,显而易见,如果几个线程同时操作,那么就可能会出现高低2个32位值出错的情况发生。要在线程间共享​​long​​​与​​double​​​字段是,必须在​​synchronized​​​中操作,或是声明为​​volatile​​。

2.常见的可能会存在线程安全的情况:

  • ​ArrayList​​​不是线程安全的,​​Vector​​​是线程安全的,线程安全是基于​​synchronized​​来实现的.
  • ​HashMap​​​不是线程安全的,​​HashTable​​​是线程安全的,线程安全实现是基于​​synchronized​​​来实现的,​​ConcurrentHashMap​​​也是线程安全的,采用锁分离技术,线程安全是基于​​CAS+synchronized​​实现的。
  • ​StringBuilder​​​ 不是线程安全, ​​StringBuffer​​​是线程安全的,线程安全是基于​​synchronized​​来实现的。
  • ​SimpleDateFormat​​​如果在多线程情况下使用存在线程安全问题,可以通过使用局部变量(常用)、 ​​ThreadLocal​​线程副本变量、同步锁等方式来解决。

蹊源的Java笔记—线程并发与线程安全_大厂面试_02


举报

相关推荐

0 条评论