多线程大总结
2022-10-19 08:07:27 0 举报
AI智能生成
并发
作者其他创作
大纲/内容
阻塞队列
解决了什么?
阻塞队列(BlockingQueue)支持插入和移除等方法,一般用来实现生产者和消费者功能
(生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。
阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。)
(生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。
阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。)
实现方式
ReentranLock + Condition 实现队列的阻塞,ReentranLock 是锁,
Condition是条件状态,通过等待/通知机制,来实现线程之间的通信
Condition是条件状态,通过等待/通知机制,来实现线程之间的通信
图
java有哪些阻塞队列
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列
线程常见工具
CyclicBarrier 线程栅栏
字面意思是“可循环使用的屏障”。它的作用是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,
屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。
屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。
demo
CountDownLatch
作用
CountDownLatch让一个或多个线程在运行过程中的某个时间点能停下来等待其他的一些线程完成某些任务后再继续运行。
类似的任务可以使用线程的 join() 方法实现:在等待时间点调用其他线程的 join() 方法,
当前线程就会等待join线程执行完之后才继续执行,但 CountDownLatch 实现更加简单,并且比 join 的功能更多。
类似的任务可以使用线程的 join() 方法实现:在等待时间点调用其他线程的 join() 方法,
当前线程就会等待join线程执行完之后才继续执行,但 CountDownLatch 实现更加简单,并且比 join 的功能更多。
demo
原理图
Semaphore 信号量
Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。
demo
总结
1、Semaphore 内部维护一组信号量,即一个 volatile 的整型 state 变量
2、Semaphore 分为公平或非公平两种方式,获取信号量或释放信号量的本质是对 state 进行原子的减少或增加操作
3、获取不到信号的线程放在等待队列里面,释放信号的时候会唤醒后继节点
4、Semaphore 主要用于对线程数量、公共资源(比如数据库连接池)等进行数量控制
HashMap&CHM
HashMap
常见的变量
负载因子 0.75
初始化值为16
链表树化为红黑树大于等于8,数组长度大于等于64
红黑树退化为链表因子为6
put()方法流程
1,首先进入到putVal方法,然后判断table是否为空,为空则进行扩容
2,不为空则通过扰动函数计算出key的hash值与(n-1)作与运算得到数组下标值
3, 然后数组下标对应的位置是否有值,如果为空则直接插入,否则判断key值与hash是否相当,是的话直接覆盖
4, 如果不相等的话,则判断是否是红黑树,如果是的话,则直接插入
5,如果不是的话,判断链表长度是否大于8,如果小于8,则插入
6,如果链表长度大于8,数组长度小于64,则就行扩容,而后转为红黑树,插入
2,不为空则通过扰动函数计算出key的hash值与(n-1)作与运算得到数组下标值
3, 然后数组下标对应的位置是否有值,如果为空则直接插入,否则判断key值与hash是否相当,是的话直接覆盖
4, 如果不相等的话,则判断是否是红黑树,如果是的话,则直接插入
5,如果不是的话,判断链表长度是否大于8,如果小于8,则插入
6,如果链表长度大于8,数组长度小于64,则就行扩容,而后转为红黑树,插入
怎么计算数组下标
通过扰动函数(hashcode的高8位与低8位就行异或运算)得到一个hash值&(n-1)
1, 位运算提升性能
2,使hash值更散列,减少hash碰撞
2,使hash值更散列,减少hash碰撞
为什么扩容为2的幂次方
因为在计算数组下标的时候,是数组长度减1与hash值作位运算
假如为16,16-1为15,二进制位1111,就能直接使用&运算实现取模运算。
假如为16,16-1为15,二进制位1111,就能直接使用&运算实现取模运算。
1.减少碰撞次数,
2.增加查询效率,
3.减少空间浪费
2.增加查询效率,
3.减少空间浪费
什么时候树化,什么时候转会链表
当链表长度大于8的时候,而且数组长度大于64的时候,会转为红黑树
当树的节点小于等于6个的时候,会退化成链表
当树的节点小于等于6个的时候,会退化成链表
不能自定义容量大小,比如传入6,也会扩容为8,为2的次幂方
JDK1.8之后做了什么优化?
1,数据结构加入了红黑叔
时间复杂度为0(lgn)
2,链表插入节点的方式
1.8中变成了尾插法
1.7中,插入链表节点使用头插法
头插法主要存在的问题是:并发下调用transfer()方法,可能会导致链表死循环,以及数据的丢失。
代码
1.8中变成了尾插法
代码
3,hash函数
Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,
使得计算结果的不确定性增强,降低发生哈希碰撞的概率。
使得计算结果的不确定性增强,降低发生哈希碰撞的概率。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
CHM
什么是CHM?
ConcurrentHashMap 是线程安全且高效的HashMap,hashmap的key,value都允许为空,但是CHM不允许
1 线程不安全的HashMap(在多线程环境下,使用HashMap 进行put操作会引起死循环,导致CPU利用率接近100%)
2 效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)
3 ConcurrentHashMap 的锁 分段技术可有效提升并发访问率。
2 效率低下的HashTable(HashTable 容器使用synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下)
3 ConcurrentHashMap 的锁 分段技术可有效提升并发访问率。
CHM如何解决线程安全问题
jdk1.7是对Segment就行加锁,加锁范围过大。
采用的是lock+分段锁
采用的是lock+分段锁
jdk1.8是对node加锁,加锁粒度小,效率更高
使用的是sync+cas保证
使用的是sync+cas保证
CHMJDK1.7&1.8的区别
JDK1.7
ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap。
而Segment本身继承了ReentrantLock,它本身就是一个锁。
在Segment中通过HashEntry数组来维护其内部的hash表。
每个HashEntry就代表了map中的一个K-V,用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素。
而Segment本身继承了ReentrantLock,它本身就是一个锁。
在Segment中通过HashEntry数组来维护其内部的hash表。
每个HashEntry就代表了map中的一个K-V,用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素。
图
JDK1.8
去掉Segment的概念
图
1,数据存储结构
1JDK1.7,采用数组+链表+segment
JDK1.8,用数组+链表+红黑树
2.重hash方式
JDK1.7,因为每个segment里有一个数组,需要重hash
3.线程安全方式
1.JDK1.7对整个数组进行了分割分段(Segment),每一个segment都是一个hashtable,每一把锁只锁容器其中一部分数据,
多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高井发访问率。segment+ ReentrantLock
2.JDK1.8使用的是优化的synchronized关键字同步代码块和cas操作了维护井发Node +CAS+Synchronized
4 效率问题
1.JDK1.7,当并发太大,链表太长,效率低下
2.JDK1.8,效率高
1JDK1.7,采用数组+链表+segment
JDK1.8,用数组+链表+红黑树
2.重hash方式
JDK1.7,因为每个segment里有一个数组,需要重hash
3.线程安全方式
1.JDK1.7对整个数组进行了分割分段(Segment),每一个segment都是一个hashtable,每一把锁只锁容器其中一部分数据,
多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高井发访问率。segment+ ReentrantLock
2.JDK1.8使用的是优化的synchronized关键字同步代码块和cas操作了维护井发Node +CAS+Synchronized
4 效率问题
1.JDK1.7,当并发太大,链表太长,效率低下
2.JDK1.8,效率高
线程池
为什么要有线程池?
手动创建线程的缺点
频繁创建,开销大
不好管理(不知道哪里创建了线程、线程名字可能没有)
不好管理(不知道哪里创建了线程、线程名字可能没有)
1,可以提高性能,减少线程创建与销毁时带来的性能开销
2,对线程更好地进行管理,如线程数,存活时间等等
3,方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,
并且会造成cpu过度切换(需要保持当前执行线程的现场,并恢复要执行线程的现场
并且会造成cpu过度切换(需要保持当前执行线程的现场,并恢复要执行线程的现场
设计思路
主要为了实现线程的复用
核心线程是怎么做到不回收的?
有任务就处理,没有任务就阻塞(生产者-消费者)
主要流程
1、当提交任务的时候,如果工作线程小于核心线程,则创建线程,并执行任务
2、如果工作线程数大于核心线程数,且工作线程数小于最大线程数,则将任务丢入到阻塞队列里面
3、如果工作线程数小于最大线程数,且阻塞队列满了,则尝试创建临时线程去执行任务
4、如果工作线程数等于最大线程数,且阻塞队列满了,调用拒绝策略
类
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)}
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)}
ThreadPoolExecutor
jdk提供了哪些线程池
newFixedThreadPool
固定线程数的线程池
newSingleThreadExecutor
单一线程数的线程池
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明: Executors 返回的线程池对象的弊端如下:
线程数量如何设置
CPU密集型
主要目的就是不让cpu执行上下文切换
cpu核数+1
IO密集型
主要是看IO等待时间
2*cpu核数+1
基本参数
核心线程数
最大线程数
存活时间
存活单位
阻塞队列
拒绝策略
线程工厂
拒绝策略
CallerRunsPolicy
谁提交任务,谁就负责执行任务(线程池不管)
AbortPolicy
丢弃任务并抛出RejectedExecutionException异常。 【默认】
DiscardPolicy
也是丢弃任务,但是不抛出异常。
DiscardOldestPolicy
将最早进入队列的任务删除,之后再尝试加入队列
联想对女孩子写表白信
不接不管,自己去处理这封信
丢掉这封信,并说我不爱你
直接丢掉这封信,并不管你
将第一封信丢掉,然后再尝试接收你的信
注意事项:
线程池中的线程没有回收的过程,线程如果执行完毕,那么线程就会结束,当有任务时,核心线程会阻塞在任务队列的take方法,不会结束线程
核心线程也是可以回收的,只需设置allowCoreThreadTimeOut=true
线程池中,底层启动线程是调用Worker中thread的start方法,执行任务是通过调用阻塞队列中Runnable的run方法
线程池中的maximumPoolSize参数,代表当我任务队列满了的时候,可以添加除主线程外的线程来帮忙执行任务,但是核心线程加额外的线程不能超过maximumPoolSize的值
线程基础
为什么要用线程?
1,更好的压榨CPU资源,提示程序运行效率
2, 提高并发量
进程&线程
可以从java编译到.class,再到类加载,到jvm运行来讲,被jvm加载的程序,活着的程序就叫进程
线程:cpu调度的最小单元,一个进程中包含多个线程,线程使用进程分配的资源
并发&并行
并发:一段时间内来运行的多个线程
并行:同一时间来运行的线程,有多少核cpu就可以同时并行运行多少个线程
wait()&sleep()
联系
都可以实现线程的阻塞
wait()和sleep需要捕获InterruptedException异常,park()不用
区别
wait()是Object类中的,会释放锁,
sleep()是Thread类的静态方法,还是会持有锁。
sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。
sleep()是Thread类的静态方法,还是会持有锁。
sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。
wait()/park()休眠线程时,线程状态是waiting,而sleep()是time_waiting状态,sleep一定会带时间
notiy()¬iyAll()
notify随机唤醒一个线程,notiyAll 唤醒全部线程
unpark()可以唤醒指定线程
在sync里面使用
创建方式
通俗说法四种
继承Thread,重写run方法
实现runnable()重写run方法
实现Callable()方法,从写call方法,有返回值,放到线程池,或者Future里
线程池创建线程
牛逼说法1种
本质上都是实现Runnable接口
生命周期
new
runnable
running
ready
teminate
blocked
只有sync同步代码块里面才会blocked,reentrantlock只会进入waiting状态
waiting
time_waiting
正常运行只有三个生命周期,但是当线程在runnable状态调用wait().sleep(),locksupper.park()等方法时,会进入waiting状态,如果调用上述方法加时间单位,进入time_waiting状态,然后调用这些notify,notifyAll,等方法就行唤醒,回到runnable状态,当在runnable状态进入sync代码块,则进入blocked阻塞状态。
图片
线程的停止
stop() 过时,放弃,因为没有情面可言,直接停止线程,不管你是否在运行状态, 相当于kill -9
线程正常run()结束
通过interrupt(),中断方式
修改中断标记状态,将false改为true;
唤醒阻塞状态下的线程,并且能被try捕获带中断异常,当异常被捕获到之后,在catch里面会自动的将中断标记复位
唤醒阻塞状态下的线程,并且能被try捕获带中断异常,当异常被捕获到之后,在catch里面会自动的将中断标记复位
thread.isInterrupt() 获取中断标记
interrupted()获取中断标记,然后复位中断标记
线程通信
通过共享内存的方式
每个线程都有自己的工作线程,通过主内存进行数据传递
通过等待通知的方式
wait()/notiy/caodition.await()/condition.signal()
通过流的方式
带来的问题
原子性,有序性,可见性
活跃性问题
死锁
四大条件
互斥
不可剥夺
请求与保持
循环等待
解决死锁
对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了
对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了
活锁
跟死锁相反,抢占到共享资源,然后立马释放,不去执行后面的代码
饥饿
有些线程的优先级比较高,有些优先级比较低,优先级低的就有可能一直抢占不到,这就是处于饥饿状态
性能问题
当线程数量大于cpu核心数量的时候,
就会带来上下文切换,因为要保存线程状态等操作,所以会造成性能开销
就会带来上下文切换,因为要保存线程状态等操作,所以会造成性能开销
CPU100%排查思路?
1, jps
2, top -c 查看cpu利用率最大的进程
3,top -H -p 查看进程里面占用资源最多的那个线程
4,printf "0x%x\n" 线程id
5, jstack pid | grep -A 20 线程的16进制
2, top -c 查看cpu利用率最大的进程
3,top -H -p 查看进程里面占用资源最多的那个线程
4,printf "0x%x\n" 线程id
5, jstack pid | grep -A 20 线程的16进制
线程安全
问题
原子性
一组操作,要么全部成功,要么全部失败,也是i++
可见性
一个线程对一个共享变量的修改,对其他线程可见
有序性
有代表性的例子就是i++,是由三个指令组成,
synchronize
问题引出?
int 变量count++引出思考--》线程安全(原子性)--》解决(sync)-->如何(锁,互斥)-->如何实现互斥--》对象内存布局--》对象头--》锁标识---》锁升级
使用?锁范围?
修饰静态方法
表示类锁,锁的是类
修饰实例方法
表示对象锁,锁的是对象
修饰代码块
取决于sync修饰的是什么
如果是类名.class 表示类锁
如果是对象引用,则表示锁的是对象
对象内存布局
对象头(对象标记(Mark Word),和类元信息 (Class Metadata Pointer),实例数据,对齐填充组成
对象头
mark word
类元信息 (Class Metadata Pointer)
对象指向方法区,看这个对象属于哪个方法区
数组长度(只有数组对象才有)
实例数据
是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充
齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。
由于32位操作系统要求对象大小是8字节的整数倍,64位的是16字节的整数倍
由于32位操作系统要求对象大小是8字节的整数倍,64位的是16字节的整数倍
内存布局图
对象标记(markword)
hashcode()
分代年龄
4bit ,转为10进制是15,表示在年轻代最多能呆15次,就要进入老年代
偏向锁标记
其他锁标记
无锁
轻量级锁
重量级锁
图
锁升级
偏向锁在jdk1.8是默认关闭的,但是面试的时候还是提一嘴吧
锁升级
偏向锁
当不存在竞争的时候(也就是只有一个线程去抢占共享资源的时候),此时才会有偏向锁
轻量级锁
在升级为轻量级锁的时候,第一个线程会修改锁标志,还会将MarkWord里面数据复制到线程的栈中,还会让对象头指向当前的线程,设置成功后表示抢占到锁,在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁!当然次数也可以更改
当存在竞争的时候,就不会出现偏向锁,而是直接升级为轻量级锁
重量级锁
重量级锁就是线程进入到阻塞状态(当要唤醒线程的时候,是需要CPU进行上下文切换的)
当锁竞争激烈等的时间太长那只能使用Monitor监视器 ,基于操作系统的锁达到效果了
javac -p xxx.class 编译,发现编译成指令后每个synchronized修饰的代码块前后都会有加上一个monitorenter 和monitorexit指令, 这其实就对应了我们上面那种加锁逻辑图里的lock 和unlock操作,monitorexit 指令又两次是因为在出现异常的时候我们也需要解锁操作
当只有一个线程的时候,默认是偏向锁,并记录偏向锁标记为1,而且记录线程id,当其他线程来竞争的时候,升级为轻量级锁,然后还没抢到的时候,会再次升级为重量级锁。
非公平锁
锁消除
1. 锁消除
JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。
编译就不用加入monitorenter和monitorexit指令。
JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。
编译就不用加入monitorenter和monitorexit指令。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可
JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可
实现原理:
ObjectMonitor中有两个队列(EntryList、WaitSet)以及锁持有者Owner标记,其中WaitSet是哪些调用wait方法之后被阻塞等待的线程队列,EntryList是ContentionList中能有资格获取锁的线程队列。当多个线程并发访问同一个同步代码时候,首先会进入EntryList,当线程获得锁之后monitor中的Owner标记会记录此线程,并在该monitor中的计数器执行递增计算代表当前锁被持有锁定,而没有获取到的线程继续在EntryList中阻塞等待。如果线程调用了wait方法,则monitor中的计数器执行赋0运算,并且将Owner标记赋值为null,代表当前没有线程持有锁,同时调用wait方法的线程进入WaitSet队列中阻塞等待,直到持有锁的执行线程调用notify/notifyAll方法唤醒WaitSet中的线程,唤醒的线程进入EntryList中等待锁的获取。除了使用wait方法可以将修改monitor的状态之外,显然持有锁的线程的同步代码块执行结束也会释放锁标记,monitor中的Owner会被赋值为null,计数器赋值为0。如下图所示
图
volatile
问题引出?
i++存在指令重排序的问题?---》引出有序性问题----》volatile如何解决有序性问题?--》cpu是如何优化的?
CPU优化之路--》因为cpu与内存存在性能上的严重差距---》引出cpu高速缓存---》出现数据一致性问题--》总线锁,缓存锁(MESI协议)--》性能问题---》storebuffer--->内存屏障(读屏障,写屏障,全屏障)
引出cpu高速缓存(L1,L2,L3)-->缓存一致性问题(从共享内存中获取值,但是修改之后没有刷回到主内存,或者其他线程用的还是旧值)----》MESI(修改,排他,共享,失效)--》(可能存在性能问题.上面的机制相当于串行执行写数据了.如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack,然后获取独占锁后才能写数据,那可能就会导致性能很差了,因为这个对共享变量的写操作,实际上在硬件级别变成串行的了)--》通过写缓存和无效队列去解决性能问题(异步),这里又造成了指令重
排的问题,为了解决这个问题---->引入了内存屏障(其实就是禁止写缓存和无效队列的使用)通过读写屏
障和全屏障来解决指令重排,通过volatile关键字修饰的变量,jvm会通知底层开启内存屏障
排的问题,为了解决这个问题---->引入了内存屏障(其实就是禁止写缓存和无效队列的使用)通过读写屏
障和全屏障来解决指令重排,通过volatile关键字修饰的变量,jvm会通知底层开启内存屏障
cpu执行代码的速度>>IO速度------->为了解决前面的问题,提出了高速缓存的概念,但是它又引出了一
个新的问题:缓存一致性问题--------->通过上锁去解决(总线锁和缓存锁【MESI】被修改,独享的,共享
的,失效的),造成性能问题----->通过写缓存和无效队列去解决性能问题(异步),这里又造成了指令重
排的问题,为了解决这个问题---->引入了内存屏障(其实就是禁止写缓存和无效队列的使用)通过读写屏
障和全屏障来解决指令重排,通过volatile关键字修饰的变量,jvm会通知底层开启内存屏障
个新的问题:缓存一致性问题--------->通过上锁去解决(总线锁和缓存锁【MESI】被修改,独享的,共享
的,失效的),造成性能问题----->通过写缓存和无效队列去解决性能问题(异步),这里又造成了指令重
排的问题,为了解决这个问题---->引入了内存屏障(其实就是禁止写缓存和无效队列的使用)通过读写屏
障和全屏障来解决指令重排,通过volatile关键字修饰的变量,jvm会通知底层开启内存屏障
JMM
就算引出了内存屏障,但是对不同的硬件系统还是有些不一致的,然后java提出jmm,实现统一规范
Java内存模型
(Java Memory Model )就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,
保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
Java内存模型
(Java Memory Model )就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,
保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重
排序、处理器会对代码乱序执行等带来的问题
排序、处理器会对代码乱序执行等带来的问题
happen-before模型
程序执行顺序性规则 as-if-serial
传递性
volatile 变量规则
thread.join规则
final
sync
start规则
volatile不能解决线原子性问题,但是能解决可见性和有序性可见性
作用:
volatile修饰的变量可以被所有线程可见(强缓存一致性)
禁止指令重排,修饰的变量,jvm会通知底层开启内存屏障,禁止指令重排
底层:
volatile底层的实现其实是通过lock关键字进行实现的
volatile的变量在进行写操作时,会在前面加上lock前缀。
作用:
volatile修饰的变量可以被所有线程可见(强缓存一致性)
禁止指令重排,修饰的变量,jvm会通知底层开启内存屏障,禁止指令重排
底层:
volatile底层的实现其实是通过lock关键字进行实现的
volatile的变量在进行写操作时,会在前面加上lock前缀。
ThreadLocal
是什么?解决了什么?
并发工具类
通过线程隔离,解决了线程安全问题
场景
全局用户信息的使用---》登录的时候将token解析成用户,然后将用户信息存在threadlock里面,
下次用的时候也可以更好实现线程隔离。
下次用的时候也可以更好实现线程隔离。
SimpleDateFormat案列
底层存储结构?
底层通过ThreadLockMap存储的不同的entry
key存的是threadlocal,而value存的是变量的值
对key进行了弱引用,而entry和value是强引用
结构图
为什么key是弱引用?
反证法
前提事实:threadlocalmap的key存的是本身,也就是threadlocal,假如每个key都强引用指向threadlocal,那么就算threadlocal的置为null, 但是 这个threadlocal还是会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收
线程池中使用 ThreadLocal 为什么可能导致内存泄露呢
根据上一个问题回答,因为key是弱引用,可能会被回收,但是因为value是强引用,所以会存在key为null,而值没法回收的情况
如何解决内存泄露问题?
首先threadlocal本身在调用set(),get()和扩容方法的时候会进行回收,第二,在threadlocal使用完毕的时候,调用其remove方法
如何解决hash问题
hash冲突解决的方法有四种
开放寻址法
线性探索
index++
二次探测再散列
左右两边探索
threadlocal
2,链地址法:
将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
hashmap
3, 再哈希
4, 建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
Threadlocalmap使用的方式是开放寻找法的线性探索,当index的位置有值的时候,往下index+1下面找,直到找到为空的为止
四种引用方式
强
只要对象还活着,垃圾收集器宁可抛出OOM异常,也不会回收这个对象
我们最常见的引用
软(SoftReference)
当内存充足时GC不回收该对象,
当内存不充足时回收该对象
当内存不充足时回收该对象
引用队列(ReferenceQueue)
弱(WeakReference)
不管内存充不充足,只要就行了GC就会回收改对象
ThreadLocal
虚(PhantomReference)
虚引用 就是 形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。
因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。
因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。
ReentrantLock
如何设计一个锁?
1,要实现互斥特性
设计一个共享变量,拿到可以为1,没抢到为0
2,抢到锁不需要处理,
没有抢到的话,需要就行阻塞和唤醒
没有抢到的话,需要就行阻塞和唤醒
阻塞队列,FIFO
对特定线程的阻塞和唤醒/LockSupport.park()/unpark();
2,要实现重入特性
存储线程ID,看是否是同一个
3,实现公平与非公平
逻辑性实现
非公平一上来就就就行cas抢占
lock接口主要方法
void lock();
boolean tryLock()
不遵循设定的公平的规则,一旦有线程释放了锁,
那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程现在在等待队列里了
那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程现在在等待队列里了
boolean tryLock(long time, TimeUnit unit)
void lockInterruptibly()
void unlock();
锁的类别
线程要不要锁住同步资源
锁住
悲观锁
sync/lock
阻塞和唤醒带来的性能劣势
不锁住
乐观锁
一般用CAS实现,在一个原子操作内,比较并替换
原子类和并发容器
多个线程能否共享一把锁
可以
共享锁/读锁
不可以
独享锁/写锁
多线程竞争时,是否排队
排队
公平锁
缺点:更慢,吞吐量更小
先尝试插队,插队失败再排队
非公平锁
缺点:有可能线程饥饿,也就是某些线程在长时间内,始终得不到执行
同一个线程是否可以重复获取一把锁
可以
可重入
不可以
不可重入
等锁的过程
自旋
自旋锁
阻塞和唤醒一个java线程需要操作系统切换CPU状态来完成,这种状态转换需要消耗处理器时间
如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
所以避免这些场景,所以使用cas操作,典型应用:AtomicInteger
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
缺点:没有抢占到锁会一直抢占,会造成开销
阻塞
非自旋锁
可中断锁
synchronize就是不可中断锁
locks是中断锁,tryLock(tine)和lockIntrruptibly
锁优化:
JVM层面
锁自旋/自适应自旋
不会盲目自旋,会转为阻塞锁
锁消除
有些情况下不需要加锁
代码在方法内部的,并且所有同步都是在方法内部的,在这种情况下根本不可能有人来访问我们的方法,jvm会认为它是私有的,不需要加锁
锁粗化
一系列的操作都是对同一个对象反复地加锁解锁加锁解锁,在这种情况下,会将一些列的加锁解锁合为一个加锁解锁。就不用反复申请释放锁了,只需一次就可以全部执行完我们的事项了
我们自己写代码时的优化
1,缩小同步代码块:原子类的就行了
2,尽量不要锁住方法,因为后来方法可能会增加
3,减少请求锁的次数
4,避免认为制造热点
某些数据是共享的,使用它就需要加锁,
例如求hashmap的size,不要遍历,可以用一个变量的来维护。然后
例如求hashmap的size,不要遍历,可以用一个变量的来维护。然后
5,锁中尽量不要再包含锁
6,选择适当的锁和工具类
juc下的锁和Semaphore
相同点:
闸门,锁只允许一个线程通过,semaphore允许设置的线程数量数通过
2,查看是否阻塞,trylock()/tryacquire()
3,底层就是AQS
和sync的区别
sync是关键字,在异常时自动释放锁
reentrantlock是juc包下的一个工具类
reentrantlock是juc包下的一个工具类
lock更加灵活,但是需要手动释放锁
比较图
加锁过程?
1, 第一个线程过来了(使用的是非公平锁),那么第一个线程thread1会去获取锁.通过CAS的操作,
将当前AQS的state由0变成1,证明当前thread1已经获取到锁,
并且将AQS的exclusiveOwnerThread设置成thread1,证明当前持有锁的线程是thread1。
将当前AQS的state由0变成1,证明当前thread1已经获取到锁,
并且将AQS的exclusiveOwnerThread设置成thread1,证明当前持有锁的线程是thread1。
2,此时thread2,来抢占线程,一上来就就行cas抢占锁,无疑失败,因为此时thread1还占着,而后执行下面代码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
简单流程:
1,当线程A跑来调用lock()方法的尝试就行加锁,这个加锁的过程,直接就是用cas操作将state值从0变成1,设置当前加锁线程为线程A
2,线程B过来一看,发现state的值不是0啊?所以cas操作将state从0变为1会失败,因为state的值当前为1,说明已经有人加锁了!再检查是否是自己之前加的锁?如果不是,此时加锁失败。会将自己放入一个AQS等待队列里面。如果是自己加锁,state值再加1
3,线程A在执行完自己的业务逻辑代码之后,就会释放锁。将AQS内的state变量的值减为0.将加锁线程变量也设置为null,从等待队列的头节点唤醒线程B,尝试加锁。
2,线程B过来一看,发现state的值不是0啊?所以cas操作将state从0变为1会失败,因为state的值当前为1,说明已经有人加锁了!再检查是否是自己之前加的锁?如果不是,此时加锁失败。会将自己放入一个AQS等待队列里面。如果是自己加锁,state值再加1
3,线程A在执行完自己的业务逻辑代码之后,就会释放锁。将AQS内的state变量的值减为0.将加锁线程变量也设置为null,从等待队列的头节点唤醒线程B,尝试加锁。
图
AQS
应用:
ReentrantLock/Semaphore/CountDownLatch(private static final class Sync extends AbstractQueuedSynchronizer {})
是AbstractQueuedSynchronizer的缩写,是一个抽象类,是Java并发包 java.util.concurrent 中是一个
可以用来构建锁,同步器,协作工具类的工具类。Lock类会有一个AQS类型的属性,来实现锁。实现逻辑是这样子的?
可以用来构建锁,同步器,协作工具类的工具类。Lock类会有一个AQS类型的属性,来实现锁。实现逻辑是这样子的?
共享变量
state
在Semaphore表示“剩余的许可证的数量
CountDownLatch表示:还需要倒数的数量
在ReentrantLock:表示锁的占有情况,包括可重入次数
当state的值0的时候,标识改Lock不被任何线程所任何线程所占有
保存线程id的setExclusiveOwnerThread
由双向链表组成的AQS同步队列
控制线程抢锁和配合的FIFO队列
用来存放"等待的线程“,AQS就是排队管理器,当多个线程争用一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的线程
如果加锁失败,将会自旋,如果自旋还没获得锁的话就会加入到由双向链表组成的阻塞队列中,由尾部加入
期望协作工具类去实现的获取/释放锁等重要方法
获取操作会依赖state变量,经常会阻塞(比如获取不到锁的时候)
在Semaphore中,获取就是acquire方法,作用是获取一个许可证
在CountDownLatch里面,获取就是await方法,作用是等待,直到倒数结束
Condition队列
AQS一个内部类ConditionObject,它实现了Condition接口,主要用于实现条件锁。
ConditionObject中也维护了一个队列,这个队列主要用于等待条件的成立,当条件成立时,其它线程将signal这个队列中的元素,将其移动到AQS的队列中,等待占有锁的线程释放锁后被唤醒。
Condition典型的运用场景是在BlockingQueue中的实现,当队列为空时,获取元素的线程阻塞在notEmpty条件上,一旦队列中添加了一个元素,将通知notEmpty条件,将其队列中的元素移动到AQS队列中等待被唤醒。
类比
CAS
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做!
ABA问题
某一时刻取出内存值然后在与当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。
ABA 问题的过程是当有两个线程 T1 和 T2 从内存中获取到值A,线程 T2 通过某些操作把内存 值修改为B,
然后又经过某些操作将值修改为回值A,T2退出。
ABA 问题的过程是当有两个线程 T1 和 T2 从内存中获取到值A,线程 T2 通过某些操作把内存 值修改为B,
然后又经过某些操作将值修改为回值A,T2退出。
在修改变量的时候添加版本号
原子引用类 AtomicStampedRe
ference 来解决这个问题
ference 来解决这个问题
0 条评论
下一页