Java多线程
2021-03-08 13:47:28 54 举报
AI智能生成
Java多线程知识总结
作者其他创作
大纲/内容
概念
进程
线程
NEW
新建状态,线程被构建,还未调用start()方法
RUNNABLE
运行状态,jvm把就绪和运行中统称为RUNNABLE
BLOCKED
阻塞状态,表示线程阻塞于锁
WAITING
等待状态,调用了wait()方法,需要其他线程调用niotfy()或者notifyAll()方法来激活
TIMED_WAITING
超时等待状态,不同于WAITING,可以在超过指定时间后,自行激活
TERMINATED
终止状态,表示该线程已经执行完毕
并发与并行
并发
同一段时间,多个任务都在执行
并行
同一时刻,多个任务同时执行
线程同步
synchronized
作用范围
方法
静态方法
使用的同步锁是类对象,和其他静态同步方法存在锁竞争
非静态方法
使用的锁是当前对象,同一对象之间存在锁竞争,不同对象之间不存在竞争关系
代码块
使用的锁是synchronized中传入的对象,依据是否同一对象而决定否存在竞争
底层原理
方法
代码块
每个java对象的对象头中都有monitor对象,通过能否获取到monitor对象来判断能否获取锁
锁优化
自旋锁
当线程竞争锁失败时,不直接阻塞自己,而是自旋(空等待,比如一个有限的for循环)一会儿,在自旋的同时重新竞争锁。如果在自旋结束前获得了锁,那么获取锁成功;否则自旋结束后阻塞自己
自旋锁可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)
单核处理器情况下,不存在实际的并行。当前线程不阻塞自己,拥有锁的线程就无法执行,锁永远不会释放。进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择如果锁竞争时间比较长,那么自旋通常不能获得锁,白白浪费自旋占用的CPU时间。这通常发生在所持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁
使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
偏向锁
偏向锁的目标是减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗
如果有其他线程申请锁,那么偏向锁很快就膨胀为轻量级锁。浪费了维持偏向锁的性能消耗
使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。
轻量级锁
使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功。否则可以继续使用自旋锁获取或者升级为重量级锁
轻量级锁的目标是减少无实际竞争的情况下,使用重量级锁产生的性能消耗
如果锁竞争激烈,轻量级锁很快就将升级为重量级锁,那么维持轻量级锁的过程就成了浪费
重量级锁
内置锁在Java中被抽象成监视器锁(monitor)。监视器锁直接对应底层操作系统的互斥量(mutex)
这种同步方式成本非常高,包括系统调用引起的内核态和用户态切换、线程阻塞造成的线程切换等
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 条评论
下一页