概述
并行:同一个时间点,多个线程同时执行
并发:同一个时间段,多个线程交替执行,微观上是一个一个的执行,宏观上感觉是同时执行
核心问题:
多线程访问共享数据存在资源竞用问题
不可见性
java内存模型(jmm)
变量数据都存在于主内存里,每个线程还有自己的工作内存(本地内存),规定线程不能直接对主内存中的数据进行操作,只能把主内存的数据加载到自己的工作内存中操作,操作完成后,在写会主内存
这样的设计就引发了不可见性,应该线程在自己的工作内存中操作了数据后,另一个线程也在操作,但是不知道数据已经被另一个线程修改了
乱序性
为了优化指令执行,在执行一些等待时间比较长的执行时,可以把其他的一些执行指令提前执行,提高速度
但是在多线程场景下,就会出现问题
非原子性
线程切换执行带来非原子性
cpu保证原子性执行cpu指令级别的,但是对于高级语言的一条代码,有时是要拆分为多条指令的,线程在执行到某条执行时操作系统会切换到其他线程去执行,这样这条高级语言指令执行就是非原子性的
总结:工作内存的缓存导致了不可见性,指令的优化导致了无序性,线程的切换执行导致了非原子性
volatile关键字
所修饰的变量在一个工作内存中操作后,底层会将工作内存中的数据同步到其他工作内存中,使其立即可见
- 确保多线程操作变量时的可见性,当一个线程修改了变量值,新值会立即对其他线程可见
- 防止指令重排序的发生
- volatile 无法保证变量操作的原子性
如何保证原子性?
解决非原子性问题,都可以通过加锁的方式实现,synchronized和ReentrantLock都可以实现
Java还提供了一种方案,在不加锁的情况下,实现++操作的原子性就是原子类
在java.util.concurrent包下,定义了许多与并发编程相关的处理类,此包一般大家简称为JUC
实现方式:采用CAS(比较并交换)思想,当多个线程对同一个内存数据操作时,假设A线程把主内存数据加载到自己的工作内存中,这个工作内存中的值就是预期值,然后在自己的工作内存中操作后,当写回主内存时,先判断预期值和主内存的值是否一致,如果一致说明还没有其他线程修改,直接写回主内存,一旦预期值和主内存的值不一样,说明有其他线程已经修改过了,线程A需要重新获取主内存的值,重新操作,判断,直到预期值和主内存中的值相同才结束,否则一直判断
由于采用自旋的方式,使得线程都不会阻塞,一直自旋,适合并发量低的场景,如果并发量过大,线程会一直自旋,会导致CPU开销大
还会有一个ABA问题,线程A拿到主内存值后,期间有其他线程已经多次修改内存数据,最终又修改到和线程A拿到的值相同,可以通过带版本号解决
锁分类
乐观锁/悲观锁
乐观锁是一种不加锁的实现,例如原子类,认为不加锁,采用自旋的方式尝试修改共享数据是不会有问题的,悲观锁是一种加锁的实现,例如synchronized和ReentrantLock认为不加锁修改共享数据会出问题
可重入锁
又名递归锁,当同一个线程,获取锁进入到外层方法中,可以在内存中可以进入另一个方法中(内层与外层使用同一把锁)
synchronized和ReentrantLock也是可重入锁
读写锁
ReentrantReadWriteLock读写锁实现
ReentrantReadWriteLockWriteLock()
ReentrantReadWriteLock.ReadLock()
读读不互斥
读写互斥
写写互斥
分段锁
将锁的粒度进一步细化,提高并发效率
hashtable是线程安全的,方法上都加了锁,假如有两个线程同时读,也只能一个一个的读,并发效率低
concurrentHashMap没有给方法上加锁,使用hashtable表中的每个位置上的第一个对象作为锁对象,这样可以多个线程对不同位置进行操作,相互不影响,只有对同一个位置操作时,才互斥
有多吧锁,提高并发操作的效率
自旋锁
线程尝试不断的获取锁,当第一次获取不到时,线程不阻塞,尝试继续获取锁,有可能后面几次尝试后,有其他线程释放了锁,此时就可以获取锁,当尝试获取到一定次数后(默认10次),仍然获取不到任何锁,那么可以进入阻塞状态
synchronized就是自旋锁
并发量低适合自旋锁
共享锁/独占锁
共享锁:可以被多个线程共享,读写锁中的读锁就是共享锁
独占锁:一把锁只能有一个线程使用,读写锁的写锁,synronized和reentrantLock都是独占锁
公平锁/非公平锁
公平锁:可以按照请求获得锁的顺序来得到锁
非公平锁:不按照请求获取锁的顺序来得到锁
synchronized是非公平的
ReentrantLock默认是非公平的,可以通过构造方法去改变成公平锁
锁状态
无锁
偏向锁
一段同步代码块一直由一个线程执行,那么会在锁对象中记录下了线程信息,可以直接会的锁
轻量级锁
当锁状态为偏向锁时,此时又有其他线程访问,锁状态升级为轻量级锁,线程不阻塞,采用自旋方式获取锁
重量级锁
当锁状态为轻量级锁时,如果有大量的线程到来,锁状态升级为重量级锁,自旋的线程会进入阻塞状态,由操作系统去调度管理
Synchronized
关键字,由jvm提供,实现同步,隐式的加锁和释放锁修饰代码块和方法,修饰代码块时需要我们提供一个同步锁对象,任意类的对象都可以,但只能是唯一一个,记录线程有没有进入到同步代码块,修饰方法时,对象是自己提供的,非静态方法锁对象默认为this,静态方法锁对象为当前的class对象,控制同步是依靠进入和退出监视器对象实现的,如果是同步方法,在指令中会为方法添ACC_SYNCHRONIZED标志,如果是同步代码块,在进入到同步代码块时,会执行monitorenter,离开同步代码块时或出异常时,执行monitorexit,为了提高锁的获取与释放效率在对象头中记录锁状态,
AQS
抽象同步队列,是一个实现线程同步的框架,并发包中有很多底层都用到了AQS,通过 FIFO 队列 和 原子状态变量(state) 管理线程的阻塞与唤醒,提供了统一的底层实现机制
核心思想:线程竞争资源时,通过 CAS 操作尝试修改 state 获取资源,成功则直接执行;失败则封装为节点进入队列等待,通过 LockSupport 实现阻塞 / 唤醒。AQS 支持独占(如 ReentrantLock)和共享(如 Semaphore)两种模式,子类只需重写 tryAcquire/tryRelease 等方法即可快速实现自定义同步器,是构建锁、信号量等并发工具的高效底层机制
JUC常用类
ConcurrentHashMap
HashMap适合单线程,不允许多个线程同时访问操作,如果有多线程访问会报异常
Hashtable是线程安全的 直接给方法加锁,效率低
ConcurrentHashMap是线程安全的 没有直接给方法加锁,用哈希表中每一个位置上的第一个元素作为锁对象,哈希表的长度是16,那么就有16把锁对象,锁住自己的位置即可,这样如果多个线程操作不同的位置,那么相互不影响,只有多个线程操作同一个位置,才会等待
如果位置上没有何元素,那么会采用cas机制插入数据到对应位置
hashtable concurrentHashMap 键值都不能为空
CopyOnWriteArrlist
ArrayList是单线程场景下使用的,在多线程场景下会报异常
Vector 是线程安全的,在方法上加了锁,效率低,读写用的同一把锁
CopyOnWriteArrayList 写方法上加了锁(ReentrantLock实现的),写入数据时,先把圆数组做了一个备份,把要添加的数据写入到备份数组中,当写入完成后,在把修改后的数组赋值到原数组中去
给写加锁,读没有加锁,读的效率就变高了,适合写操作少,读操作多的场景
CopyOnWriteArraySet
CountDownLatch
线程池
为减少频繁的创建和销毁线程
JDk5引入了线程池,建议使用ThreadPoolExecutor类来创建线程池,提高了效率
ThreadPoolExecutor 构造器的各个参数:
corePoolSize 核心线程池的线程数量(初始化5)
maximumPoolSize 线程池中最大线程数量(初始化10)
keepAliveTime 空闲线程存活时间当核心线程池中的线程足以应付任务时,非核心线程池中的线程在指定空闲时间到期后,会销毁
unit 时间单位
workQueue 等待队列,当核心线程池中的线程都在使用时,会先将等待的线程放到队列中,如果队列满了,才会创建新的线程(非核心线程池中的线程)
threadFactory 线程工厂,用来创建线程池中的数量
handler 拒绝处理任务的策略
四大策略:AbortPolicy 抛异常
CallRunsPolicy 由提交任务的线程执行,例如在main线程提交,则由main线程执行拒绝任务
DiscardOldestPolicy 丢弃等待时间最长的任务
DiscardPolicy 丢弃最后不能执行的任务
工作流程:
有大量工作任务到来时,先判断核心线程池中的线程是否都忙着,有空闲的就让核心线程池中的线程执行任务,没有空闲的,就判断等待的队列是否已满,如果没满,就把任务添加到队列等待,如果队列已经满了,在判断非核心线程池中的线程是否都忙着,如果有空闲的,,没满,就就由非核心的线程池中的线程执行,如果非核心线程池也满了,就使用对应的拒绝策略处理
提交任务
void ececute 没有返回值
submit 有返回值
关闭线程池
shutdown 执行后不再接收任务,会把线程池中和等待队列中已有的任务执行完后再停止
shutdownNow 立即执行,等待的任务不再执行
ThreadLocal
为每一个线程提供一个变量副本,只在当前线程中使用,相互隔离
实现:为每个Thread创建一个ThreadLocal.ThreadLocalMap threadLocals把变量副本放在ThreadLocal.ThreadLocalMap 中,用ThreadLocal对象统一作为键,每一个获取变量时,先拿到当前线程,拿到线程中的ThreadLocal.ThreadLocalMap
内存泄漏:对象已经不用了,但是垃圾回收不能回收该对象(例如数据库连接对象,流对象,socket)
不再使用时调用remove方法,删除键值对,可以避免内存泄漏问题
对象引用:
强引用
Object obj=new Object();强引用
对象如果有强引用关联,那么肯定是不能被回收的
软引用
被SoftReference类包裹的对象,当内存充足时,不会被回收,当内存不足时,即使有引用指向,也会被回收
弱引用
被WeakReference类所包裹的对象,只要发生垃圾回收,该类对象都会被回收掉
ThreadLocal被弱引用管理
当发生垃圾回收时,被回收掉,但是value还与外界保持着引用关系,
虚引用
被PhantomReference类包裹的对象,随时都可以被回收,通过虚引用对象跟踪对象回收状态