敖丙思维导图-多线程之synchronized\ThreadLocal\Lock\Volatitle\线程池

本文详细探讨了Java多线程领域的关键概念,包括synchronized的底层实现、线程的启动与终止方式、线程状态以及锁的升级与降级机制。深入分析了ThreadLocal内存泄漏问题,讨论了使用synchronized与Lock的优劣,并介绍了AQS、ReentrantLock、读写锁和StampedLock。此外,还涵盖了线程池的工作原理、线程间通信方法以及并发工具类如BlockingQueue、Atomic类的应用。文章最后探讨了进程切换与线程切换的差异以及并发编程中重要的概念,如互斥锁、自旋锁、CountDownLatch和CyclicBarrier的区别,并介绍了生产者消费者模型的实现方式。
摘要由CSDN通过智能技术生成

敖丙思维导图系列目录

这些知识整理都是自己查阅帅丙资料(当然还有其他渠道)加以总结滴~ 每周都会更新知识进去。
如有不全或错误还请大家在评论中指出~


  1. 敖丙思维导图-集合
  2. 敖丙思维导图-多线程之synchronized\ThreadLocal\Lock\Volatitle\线程池
  3. 敖丙思维导图-JVM知识整理
  4. 敖丙思维导图-Spring
  5. 敖丙思维导图-Redis
  6. 敖丙思维导图-RocketMQ+Zookeeper
  7. 敖丙思维导图-Mysql数据库

本文章目录


以下大部分来自傲丙的blog

线程的start方法和run方法的区别

创建线程的方式有多种,不管使用继承Thread的方式还是实现Runnable接口的方式,都需要重写run方法。

  • 如果调用run方法(同步的,还是当前线程)。
  • 调用start就是我们刚刚启动的那个线程(异步的)。启动了线程, 由 Jvm 调用 run 方法

启动一个线程是调用 start() 方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行。这并不意味着线程就会立即运行。

如何实现处理多线程的返回值

  1. 主线程等待法
  2. 使用Thread中的join()方法(可以阻塞当前线程以等待子线程处理完毕)
  3. 通过Callable接口实现:通过FutureTask或者线程池配合Future获取

创建线程的方式

  1. 继承Thread (Thread是实现了Runnable接口的类,使得run支持多线程。)
  2. 覆写Runnable接口 (单一继承原则,推荐使用Runnable接口)

a.实现Runnable接口避免多继承局限
b.实现Runnable()可以更好的体现共享的概念

  1. 覆写Callable接口 (call()方法,有返回值)
  2. 通过线程池启动

终止线程的方式

  1. 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
  2. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。

调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

  1. 使用 interrupt 方法中断线程。

线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。

线程的状态

Java线程主要分为以下六个状态:新建态(new),运行态(Runnable),无限期等待(Waiting),限期等待(TimeWaiting),阻塞态(Blocked),结束(Terminated)

  1. 新建(new)
    新建态是线程处于已被创建但没有被启动的状态,在该状态下的线程只是被创建出来了,但并没有开始执行其内部逻辑。
  2. 运行(Runnable)
    运行态分为Ready和Running,当线程调用start方法后,并不会立即执行,而是去争夺CPU,当线程没有开始执行时,其状态就是Ready,而当线程获取CPU时间片后,从Ready态转为Running态
  3. 等待(Waiting)
    处于等待状态的线程不会自动苏醒,而只有等待被其它线程唤醒,在等待状态中该线程不会被CPU分配时间,将一直被阻塞。以下操作会造成线程的等待:

没有设置timeout参数的Object.wait()方法。
没有设置timeout参数的Thread.join()方法。
LockSupport.park()方法(实际上park方法并不是LockSupport提供的,而是在Unsafe中
锁:https://juejin.im/post/5d8da403f265da5b5d203bf4

  1. 限期等待(TimeWaiting)
    处于限期等待的线程,CPU同样不会分配时间片,但存在于限期等待的线程无需被其它线程显式唤醒,而是在等待时间结束后,系统自动唤醒。以下操作会造成线程限时等待:

Thread.sleep()方法。
设置了timeout参数的Object.wait()方法。
设置了timeout参数的Thread.join()方法。
LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。

  1. 阻塞(Blocked)
    当多个线程进入同一块共享区域时,例如Synchronized块、ReentrantLock控制的区域等,会去整夺锁,成功获取锁的线程继续往下执行,而没有获取锁的线程将进入阻塞状态,等待获取锁。
  2. 结束(Terminated)
    已终止线程的线程状态,线程已结束执行。
    在这里插入图片描述

Synchronized底层实现

  • 有序性 (as-if-serial 单线程情况下程序的结果是正确)
  • 可见性 (JMM)
  • 原子性 (同一时间只有一个线程能拿到锁)
  • 可重入性 (锁对象的时候有个计数器,清0释放锁)
  • 不可中断性 (一个线程获取锁之后,另外一个线程处于阻塞或者等待不会被中断)

JVM 中,对象在内存中分为三块区域

  • 对象头Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息;**Klass Point(类型指针)**虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例变量:类的数据信息,父类的信息
  • 填充数据:对象起始地址必须是8字节的整数倍。空对象8个字节。

它如何实现可重入?

同步代码块 - synchronized (new test()) {

  1. 当我们进入一个方法时,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
  2. 如果已经是这个monitor的owner了,再次进入会把进入数+1.
  3. 同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。

同步实例/静态方法 - public synchronized void method() { / synchronized(Synchronized.class){

  • 一旦执行到这个方法,就会先判断是否有标志位ACC_SYNCHRONIZED,ACC_SYNCHRONIZED会去隐式调用刚才的monitorenter,monitorexit。

同步方法默认用this或者当前类class对象作为锁;
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;

synchronized 对象锁和类锁的区别

  • 如果多线程同时访问同一类的 类锁(synchronized 修饰的静态方法) public synchronized static void xxx以及对象锁(synchronized 修饰的非静态方法) public synchronized void xxx这两个方法执行是异步的,原因:类锁和对象锁是两种不同的锁。
  • 类锁对该类的所有对象都能起作用,而对象锁不能。

锁升级过程

在这里插入图片描述

  1. 偏向锁。锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。这个过程是采用了CAS乐观锁操作的,每次同一线程进入,对标志位+1。

CAS 循环时间长开销大/只能保证一个共享变量的原子操作。
AtomicInteger举例,他的自增函数incrementAndGet()利用了CAS

  1. 轻量级锁。偏向锁关闭,或多个线程竞争偏向锁时-》如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象。
    JVM会利用CAS尝试把对象原本的Mark Word 更新到Lock Record的指针,成功就说明无其它锁竞争,加锁成功,改变锁标志位,执行相关同步操作。
    如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,开启自旋锁。
  2. 自旋锁。Linux系统的用户态内核态的切换很耗资源(线程的等待唤起过程)。不断自旋,防止线程被挂起,一旦可以获取资源,就直接尝试成功,直到超出阈值(默认10次)。自旋都失败了,那就升级为重量级的锁,像1.5的一样,等待唤起。
synchronized 锁升级之后,怎么降级

我们可以进行一段时间进行统计,统计并发度已经很低,如果还是重量级锁,则进行锁对象的切换。换一个锁对象,这样又开始偏向锁状态,提升了处理速度;锁对象切换时候,需要注意并发操作;

synchronized java规定锁升级之后,则无法锁降级;

JDK的优化

锁消除

在这里插入图片描述

锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

锁粗化

在这里插入图片描述
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展 (粗化)到整个操作序列的外部。

用synchronized还是Lock

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
  • synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

ThreadLocal

一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。

线程私有变量,多线程不互相影响。(采用了空间换时间的设计思想,主要用来实现在多线程环境下的线程安全和保存线程上下文中的变量)。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。
内部实现是其内部类名叫ThreadLocalMap的成员变量threadLocals,key为本身,value为实际存值的变量副本。它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

在这里插入图片描述
ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置。
ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

ThreadLocal的设计本身就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。

ThreadLocal 内存泄漏问题

在ThreadLocal中,进行get,set操作的时候会清除Map里所有key为null的value。

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行(可能是作为线程池中的一员),那么这个Entry对象中的value就得不到回收,发生内存泄露。ThreadLocal被回收,key的值变成null,导致整个value再也无法被访问到;
解决办法:在使用结束时,调用ThreadLocal.remove将对应的值全部置空;

为什么要将Entry中的key设为弱引用?

如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

设置为弱引用的key能预防大多数内存泄漏的情况。
如果key为弱引用,引用的ThreadLocal的对象被回收时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

ThreadLocal 应用

ThreadLocal使用场景为 用来解决数据库连接、Session管理(需要传递一个全局变量的情况)
Spring事务隔离级别采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。
ThreadLocal包装SimpleDataFormat解决线程安全问题:SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

线程同步的方法

  • wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常。
  • notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。
  • notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争

AQS ( 底层使用了模板方法模式,继承AbstractQueuedSynchronizer并重写指定的方法)

当有自定义同步器接入时,只需重写API层所需要的部分方法即可,不需要关注底层具体的实现流程。

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH(虚拟的双向队列)锁实现的(不存在队列实例,仅存在结点之间的关联关系),即将暂时获取不到锁的线程加入到队列中。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;

AQS使用一个int成员变量state来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)。

AQS 对资源的共享方式:

  • Exclusive(独占tryAcquire-tryRelease):只有一个线程能执行,如ReentrantLock。
  • Share(共享tryAcquireShared-tryReleaseShared):多个线程可同时执行,如Semaphore(允许多个线程同时访问)、CountDownLatch(用来协调多个线程之间的同步)、 CyclicBarrier(让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门 // 它可以多次使用)、ReadWriteLock 。
    ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

Lock

ReentrantReadWriteLock (实现了ReadWriteLock接口)

也是基于AQS的,读写锁把state高16为记为读状态,低16位记为写状态,就分开了。
readLock()和writeLock()用来获取读锁和写锁。
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

1.将state域按位分成两部分,高位部分表示读锁,低位表示写锁,由于写锁只有一个,所以写锁的重入计数也解决了,这也会导致写锁可重入的次数减小(前面提到的2^16-1)。
2.读锁是共享锁,可以同时有多个,那么只靠一个state来计算锁的重入次数是不行的。ReentrantReadWriteLock是通过一个HoldCounter的类来实现的,这个类中有一个count计数器,同时该类通过ThreadLocal关键字被修饰为线程私有变量,那么每个线程都保留一份对读锁的重入次数

ReentrantLock (唯一实现了Lock接口)

ReentrantLock在内部使用了内部类Sync来管理锁,内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
- FairSync: 会判断当前是否有等待队列,如果有则将自己加到等待队列尾;
- NonfairSync: 直接使用CAS去进行锁的占用
- AQS: 队列同步器。AQS 有一个 state 标记位,(CAS修改state,volatile的int类型)值为1 时表示有线程占用,这时会做一个判断(看当前持有锁的线程是不是自己,如果是自己,那么将state的值加1就可以了,表示重入返回即可。)其他线程需要进入到同步队列等待,同步队列是一个双向链表(线程ID和当前请求的线程ID一样就可重入)。当获得锁的线程需要等待某个条件时,会进入 condition等待队列,等待队列可以有多个。当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。*(ArrayBlockingQueue和LinkedBlockingQueue都是基于这个实现的)
(1):先通过CAS尝试获取锁, 如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起;
(2):当锁被释放之后, 排在队首的线程会被唤醒CAS再次尝试获取锁,
(3):如果是非公平锁, 同时还有另一个线程进来尝试获取可能会让这个线程抢到锁;
(4):如果是公平锁, 会排到队尾,由队首的线程获取到锁。

StampedLock (不可重入锁)

ReadWriteLock读的过程中不允许写,这是一种悲观的读锁。Java 8引入了新的读写锁StampedLock,读的过程中也允许获取写锁后写入。StampedLock提供了乐观读锁,通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。

独占锁与共享锁
(1):ReentrantLock为独占锁ÿ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值