IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Java concurrent program - 深入理解Java管程 -> 正文阅读

[Java知识库]Java concurrent program - 深入理解Java管程

1.资源共享带来的问题

临界区Critical Section
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。在这里插入图片描述
在多个线程对共享资源读写操作时发生指令交错,就会出现问题。

2.synchronized

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
Synchronized(对象)中的对象可以理解为一个房间,有唯一的入口,房间一次只能进入一个人,线程可以理解为人。当线程执行到Synchronized(对象)时,可以理解为一个人进入到房间,并锁住了门拿走了钥匙,并在房间内执行临界区的代码。
当发生了上下文切换时,第二个人也就是另外一个线程执行到Synchronized(对象),发现了门被锁住了,只能在门外面等着,线程阻塞住了。
即使第一个线程的CPU时间用完了,被踢出了门外(锁住了对象就并不能一直执行下去),这时门还是锁住的,第一个线程仍拿着钥匙,第二个线程还在阻塞状态进不来,只有下次轮到第一个线程自己再次获得时间片时才能开门进入
当第一个线程执行完Synchronized(对象)临界区的代码,这时候才会从房间出来并解开门上的锁,唤醒第二个线程把钥匙给他。第二个线程这时才可以进入房间,锁住了门拿上钥匙,执行它Synchronized(对象)临界区的代码。

3.方法上的synchronized

在这里插入图片描述

4.变量的线程安全分析

成员变量和静态变量的线程安全

如果它们没有共享,则线程安全。
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:如果只有读操作,则线程安全。如果有读写操作,则这段代码是临界区,需要考虑线程安全。
局部变量的线程安全

局部变量是线程安全的。因为局部变量存在于局部变量表中,而局部变量表存在于栈帧中,JVM栈是线程私有的。
局部变量引用的对象则未必是线程安全的。如果该对象没有逃离方法的作用访问,它是线程安全的。如果该对象逃离方法的作用范围,需要考虑线程安全。
方法的访问修饰符可以保护线程安全。比如private和final。当一个子类new了一个thread,在父类的方法中加一个private,可以防止子类对父类产生干扰。
常见的线程安全类

String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:它们的每个方法是原子的,但注意它们多个方法的组合不是原子的。
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。

5.Monitor概念

在这里插入图片描述
其中,Klass Word是一个指针指向了对象从属的类。其中Mark Word结构为:
在这里插入图片描述
Monitor 被翻译为监视器或管程,是由操作系统提供的。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向Monitor对象的指针。
在这里插入图片描述
刚开始Monitor中Owner为null;
当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner,obj会与monitor进行关联,obj对象头的Mark Word 中就被设置指向Monitor对象的指针;
在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED;
Thread-2 执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的;
图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析。

6.synchronized原理进阶

1. 轻量级锁(没有竞争)
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized。
假设有两个方法同步块,利用同一个对象进行加锁。
在这里插入图片描述
其过程如下:
(1)创建锁记录(Lock Record)对象,每个线程的栈帧都包含一个锁记录的对象。
在这里插入图片描述
(2)让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。其中“lock record 地址00”为00故为轻量级锁
在这里插入图片描述
(3)如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁。并且为轻量级锁。
在这里插入图片描述
(4)如果CAS失败,有两种情况:如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。如果是自己执行了 synchronized 锁重入,那么再添加一条Lock Record作为重入的计数。
在这里插入图片描述
(5)当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
(6)当退出synchronized代码块(解锁时)锁记录的值不为 null,这时使用CAS将 Mark Word 的值恢复给对象头。成功,则解锁成功。失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
2. 膨胀锁(有竞争)
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。其过程如下所示:
(1)当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁。
在这里插入图片描述
(2)这时Thread-1加轻量级锁失败,进入锁膨胀流程即为Object对象申请Monitor锁,让Object指向重量级锁地址。然后自己进入Monitor的EntryList BLOCKED。轻量级锁没有阻塞状态。注意object中的“Monitor 地址 10”。
在这里插入图片描述
当 Thread-0 退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程。
3. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。
4. 偏向锁(没有竞争)
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的 Mark Word 头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
偏向状态
在这里插入图片描述
一个对象创建时:如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0。偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数来禁用延迟。
用锁的顺序:优先使用偏向锁,然后轻量锁,最后重量锁。
撤销偏向锁
(1)Hashcode()方法会禁用偏向锁,因为偏向锁的对象MarkWord中存储的是线程id,没有足够的位置存储hashcode。
(2)当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
(3)调用wait/notify。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的 Thread ID。
当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
批量撤销
当撤销偏向锁阈值超过40次后,JVM会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
锁消除
JIT即时编译器会对字节码文件优化,当锁对象是局部变量时

7.wait notify

在这里插入图片描述
Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
BLOCKED线程会在Owner线程释放锁时唤醒
WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争。
Wait(long timeout):有时限的等待,时间到后会自动唤醒,可以被其他线程提前唤醒。

8.Park & Unpark

它们是LockSupport类中的方法。
在这里插入图片描述
Park & Unpark与wait & notify相比:

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必。
  • park & unpark 是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么精确
  • park & unpark可以先unpark,而wait & notify不能先notify

9.活跃性

  • 死锁
    一个线程需要同时获取多把锁,这时就容易发生死锁。t1线程获得对象锁,接下来想获取B对象的锁t2线程获得B对象锁,接下来想获取A对象的锁。这样就会发生死锁的现象。
    检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁。
  • 活锁
    活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
  • 饥饿
    一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。顺序加锁能够解决死锁现象,但是也会发生饥饿问题,可以用ReentrantLock解决。

10.ReentrantLock

ReentrantLock相对于synchronized它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支持可重入
    基本语法:
    在这里插入图片描述
    可重入
    可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
    可打断
    线程t1在等待获取锁的过程中,其他线程可以采用interrupt()方法打断t1线程的等待,lock.lock()方法是不可打断的,lock. lockInterruptibly ()方法是可以被打断。可打断是为了避免一直在等待锁的获取,避免死锁的发生。
    锁超时
    等待一段时间后无法获取锁,就放弃等待。tryLock()方法,返回是否获取锁。tryLock(time)表示等一段时间后返回是否获取锁。
    公平锁
    ReentrantLock和synchronized中的monitor默认是不公平的(不按照阻塞队列的顺序来分配释放的锁)。本意是为了解决饥饿问题。
    构造时传入true会变成公平锁,公平锁会降低并发度。
    条件变量
    synchronized中也有条件变量,就是monitor里面的waitSet,当条件不满足时进入waitSet 等待。ReentrantLock的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。
  • Synchronized中,那些不满足条件的线程都在一间休息室等消息
  • ReentrantLock支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
    创建条件变量: Condition cond1 = lock.newCondition()。await前需要提前获取锁。Cond1.await()进入等待。Cond1.signal()唤醒cond1中的某一个线程。Cond1.signalAll唤醒cond1中所有线程。
    使用要点:
  • await执行后,会释放锁,进入 conditionObject 等待
  • await的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争lock锁成功后,从await后继续执行
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-18 12:34:18  更:2021-08-18 12:35:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 10:04:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码