Java多线程
2021-03-21 19:32:45 1 举报
AI智能生成
多线程知识点
作者其他创作
大纲/内容
概念
进程
线程
NEW
新建状态,线程被构建,还未调用start()方法
RUNNABLE
运行状态,jvm把就绪和运行中统称为RUNNABLE
BLOCKED
阻塞状态,表示线程阻塞于锁
WAITING
等待状态,调用了wait()方法,需要其他线程调用niotfy()或者notifyAll()方法来激活
TIMED_WAITING
超时等待状态,不同于WAITING,可以在超过指定时间后,自行激活
TERMINATED
终止状态,表示该线程已经执行完毕
并发与并行
并发
同一段时间,多个任务都在执行
并行
同一时刻,多个任务同时执行
线程同步
synchronized
Java对象(堆中组成)
实例变量
存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据
由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
对象头
Mark Word(标记字段)
指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
Class Point(类型指针)
用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。它还用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
1.6版本变更
1.6之前:synchronized属于重量级锁,依赖于底层操作系统的 Mutex Lock 来实现。操作系统实现线程切换,需要将用户态转为核心态,这也是早期syncchronized效率低下的原因。
1.6之后:引入了轻量级锁和偏向锁,减少获得锁和释放锁所带来的性能消耗
底层原理
方法
代码块
1、执行 monitorenter 指令,当前线程将试图获取对象锁所对应的 monitor 的持有权,当对象锁的 monitor 的计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。 2、倘若其他线程已经拥有对象锁的 monitor 的所有权,那当前线程将被阻塞,直到正在执行的线程执行完毕,即 monitorexit 指令被执行,执行线程将释放 monitor 并设置计数器值为 0,其他线程将有机会持有 monitor。 3、每条monitorexit命令都会对应monitorenter命令,一一配对。
作用范围
方法
静态方法
使用的同步锁是类对象,和其他静态同步方法存在锁竞争
非静态方法
使用的锁是当前对象,同一对象之间存在锁竞争,不同对象之间不存在竞争关系
代码块
使用的锁是synchronized中传入的对象,依据是否同一对象而决定否存在竞争
锁优化
自旋锁
当线程竞争锁失败时,不直接阻塞自己,而是自旋(空等待,比如一个有限的for循环)一会儿,在自旋的同时重新竞争锁。如果在自旋结束前获得了锁,那么获取锁成功;否则自旋结束后阻塞自己
自旋锁可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)
单核处理器情况下,不存在实际的并行。当前线程不阻塞自己,拥有锁的线程就无法执行,锁永远不会释放。进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择如果锁竞争时间比较长,那么自旋通常不能获得锁,白白浪费自旋占用的CPU时间。这通常发生在所持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁
使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
自适应自旋:对于同一个锁对象来说,如果自旋等待刚刚成功获取锁,并且持有锁的线程正在运行,那么虚拟机就会认为此次自旋也很有可能成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。如果对于某个锁,自旋很少成功获取过,那么在以后获取这个锁时将可能自动省略掉自旋过程,以避免浪费处理器资源。
偏向锁
偏向锁的目标是减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗
如果有其他线程申请锁,那么偏向锁很快就膨胀为轻量级锁。浪费了维持偏向锁的性能消耗
使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。
轻量级锁
使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功。否则可以继续使用自旋锁获取或者升级为重量级锁
轻量级锁的目标是减少无实际竞争的情况下,使用重量级锁产生的性能消耗
如果锁竞争激烈,轻量级锁很快就将升级为重量级锁,那么维持轻量级锁的过程就成了浪费
重量级锁
内置锁在Java中被抽象成监视器锁(monitor)。监视器锁直接对应底层操作系统的互斥量(mutex)
这种同步方式成本非常高,包括系统调用引起的内核态和用户态切换、线程阻塞造成的线程切换等
锁消除
StringBuffer 是一个线程安全的类,在它的 append 方法中有一个同步块,锁对象就是 sb,但是虚拟机观察变量 sb,发现它是一个局部变量,本身线程安全,并不需要额外的同步机制。因此,这里虽然有锁,但可以被安全的清除,在 JIT 编译之后,这段代码就会忽略掉所有的同步而直接执行。
锁粗化
StringBuffer的append 方法。如果虚拟机探测到了这样的操作,就会把加锁的同步范围扩展(粗化)到整个操作序列的外部。就是扩展到第一个 append 操作之前直至最后一个 append 操作之后,这样只需要加锁一次。
各个锁的优缺点
synchronized与ReentrantLock
两者都是可重入锁
synchronized依赖于JVM,ReentrantLock依赖于API
synchronized是非公平锁ReentrantLock可指定是公平锁或非公平锁
ReentrantLock比synchronized多一些高级功能
等待可中断
可实现公平锁
可实现选择性通知
synchronized与volatile
并发编程的三个特性
原子性
一个操作或者多次操作,要么都执行、要不都不执行。synchronized可以保证原子性
可见性
当一个线程对共享变量进行了修改,其他线程可以马上看到最新的值。volatile可以保证可见性
有序性
代码在执行过程中,编译器会对字节码进行冲重排序优化,无法保证代码的执行顺序就是编写代码的顺序。volatile可以禁止指令重排
volatile是synchronized的轻量级实现
多线程访问volatile关键字不会阻塞,synchronized可能会阻塞
volatile能保证数据的可见性,不能保证原子性;synchronized两者都可以保证
volatile主要解决多线程之间数据的可见性问题;synchronized解决多线程之间数据的同步性问题
背景
为什么用多线程
随着处理器的核心数不断增多,单线程程序已无法发挥多核处理器的优势线程比进程切换效率更高
使用多线程引入问题
内存泄漏
静态集合类
数据库连接、网络连接、IO连接等
变量不合理的作用域
内部类引用外部类
死锁
死锁发生条件
1.互斥条件:该资源任一时刻只能被一个线程拥有
2.请求与保持条件:一个线程在获取资源阻塞时,对已获得的资源不释放
3.不剥夺条件:一个线程获得的资源在其使用完成之前不能被其他线程主动剥夺,只能由自己主动释放
4.循环等待条件:多个线程之间形成首尾相接的循环等待资源关系
避免死锁
只需要破坏四个条件中的任意一个即可
互斥条件:本来就是让线程之间互斥,所以无法破坏请求与保持条件:一次性申请所有的资源不剥夺条件:可以让线程在无法获得资源时,先把已获得资源进行释放循环等待条件:按照某一顺序来获取资源
解决死锁
上下文切换
一般情况下线程数都比cpu核心数要多,cpu只能不断在线程之间切换来执行每一个线程。cpu每次切换都得保存当前状态,和加载下一个线程的状态。这就是上下文切换
ThreadLocal
每个线程的专属变量,不会被其他线程访问到
ThreadLocal中有一个静态内部类ThreadLocalMap,可以理解为ThreadLocal的定制化HashMap实现。KEY为线程对象,Value为set进去的值
内存泄漏问题
ThreadLocalMap中的Key为ThreadLocal的弱引用,而Value是强引用。所以在ThreadLocal没有被外部强引用的时候,在垃圾回收时会把Key给回收掉,而Value却不会被回收。这样会出来Key为null的Entry。如果我们不做什么措施,Value就永远无法被回收,发生内存泄漏。ThreadLocal已经考虑了这种情况,在调用set()、get()和remove()方法时,会清理掉Key为null的Entry
Java锁
乐观锁/悲观锁
乐观锁
乐观锁总是认为不存在并发问题,每次去取数据的时候,总认为不会有其他线程对数据进行修改,因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用“数据版本机制”或“CAS操作”来实现。
CAS(Compare and Swap 比较并交换),当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
分支主题
数据版本机制,实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳
悲观锁
每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有
如ReentrantLock是独享锁,Synchronized是独享锁
共享锁是指该锁可被多个线程所持有
如ReadWriteLock,其读锁是共享锁,其写锁是独享锁,独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
互斥锁/读写锁
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。如ReetrantLock、Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁。
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。Synchronized也是一种非公平锁
ReetrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,如ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
偏向锁/轻量级锁/重量级锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
Java线程池
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool
创建一个周期线程池,支持定时及周期性任务执行
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
0 条评论
下一页