导图社区 Java并发
并发编程总结,包含线程,线程池,锁等知识
编辑于2020-02-21 05:17:26并发编程
多线程
概念
进程:是操作系统进行资源分配和调度的基本单位
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程
创建方式
继承 Thread
实现 Runnable 接口
实现 Callable 接口
线程池创建线程
Runnable与Callable区别
1 实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果
2 Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理;而Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出
线程的五种状态(生命周期)
新建
就绪
运行
阻塞
死亡
线程的调度策略
抢占式调度,优先级高的任务一直执行
线程间通信与协作
Sleep() yield() join()
sleep() 让当前线程睡眠一段时间,期间不会释放任何持有的锁
yield() 让出该线程的时间片给其他线程。 线程调用该方法,表示放弃当前获得的 CPU 时间片,回到就绪状态。
join() 暂停当前线程,等待调用线程指向结束之后再继续执行 调用时,当前线程不会释放掉锁
wait() notify() notifyAll()
wait() 让线程处于等待状态 会释放掉 CPU 执行权和占有的锁
notify() 唤醒处于 wait 状态的线程
notifyAll() 唤醒所有处于 wait 状态的线程
线程池
可以使创建好的线程在指定的时间内由系统统一管理,而不是在执行时创建,执行后销毁,从而避免了频繁创建和销毁线程带来的系统开销
线程池处理流程
1 先判断线程池的核心线程是否有空闲,如果有,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的的核心线程数小于 corePoolSize ,那就再创建一个核心线程
2 如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新任务放到等待队列中。如果等待队列又满了,那就查看当前线程数是否达到 maximumPoolSize ,如果未达到,就继续创建线程。
3 如果已经达到了,就交给 RejectedExecutionHandler( 拒绝策略 ) 来决定怎么处理这个任务
使用(构造器参数含义)
corePoolSize( 线程池的核心线程数大小 )
runnableTaskQueue( 任务队列 )
ArrayBlockingQueue: 是一个基于数组的有界队列
LinkedBlockingQueue: 一个基于链表结构的阻塞队列,吞吐量通常高于 ArrayBlockingQueue
SynchronosQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue
PriorityBlockingQueue: 一个具有优先级的无限阻塞队列
maximumPoolSize( 线程池最大线程数 )
ThreadFactory: 用于设置创建线程的工厂
RejectedExecutionHandler( 拒绝策略 )
CallerRunsPolicy: 只用调用者所在线程来运行任务
DiscardOldestPolicy: 丢弃队列里最近的一个任务,并执行当前任务
DiscardPolicy: 不处理,丢弃掉
实现 RejectedExecutionHandler 接口自定义策略。 例如: 1 记录日志 2 持久化不能处理的任务
keepAliveTime( 线程活动保持时间 )
TimeUnit( 线程活动保持时间的单位 )
注意事项
建议使用 new ThreadPoolExecutor(...) 的方式创建线程
合理设置线程数
CPU 密集型任务,就要尽量压榨 CPU ,参考值可以设置为 NCPU+1
IO 密集型任务,参考值可以设置为 2*CPU
Synchronized
使用方式
修饰实例方法:作用于当前实例对象加锁 修饰静态方法:作用于当前类对象加锁 修饰代码块: 指定加锁对象,对给定对象加锁
底层实现
1对象头 在JVM虚拟机中,对象在内存中的存储布局,可分为三个区域: 对象头(Header) 实例数据(Instance Data) 对齐填充(Padding) Java对象头主要包括两部分数据: 类型指针(Klass Pointer): 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例 标记字段:(Mark Word):用于存储对象自身运行时数据
2 Java锁对象存储位置
synchronized使用的锁对象是存储在Java对象头里的标记字段里
3 Monitor
锁
乐观锁与悲观锁
乐观锁: 认为每次去拿数据时别人都不会修改,所以不加锁,但是在更新时会判断在此期间别人有没有去更新这个数据,
Java 中的实现: CAS 算法
参数:需要读者的内存值 v ,进行比较的值 a ,要写入的值 b 当且仅当预期值 a 和内存值 v 相同时,将内存值 v 修改为 b ,否则返回 v
悲观锁: 总是假设每次去拿数据时认为别人会修改,所以每次在拿数据时都会上锁。其他人想拿到这个数据就会阻塞直到它拿到锁
Java 中的实现: synchronized 关键字
MySQL 中: 行锁,表锁,读锁,写锁
总结
悲观锁适合写操作多的场景 乐观锁适合读操作多的场景
公平锁与非公平锁
公平锁: 每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则加入到等待队列中,按照 FIFO 的原则取到自己
优点: 等待锁的线程不会饿死 缺点: 等待队列中的每个线程除第一个以外都会阻塞, CPU 唤醒阻塞线程的开销比非公平锁大
非公平锁: 线程直接尝试占有锁,如果尝试失败,在采用类似公平锁的方式
优点: 减少线程唤起的开销,整体吞吐效率高 缺点: 处于等待队列中的线程可能等很久才会获得锁
Java JDK 中并发包的 ReentrantLock 可以指定构造函数的 boolean 类型来创建公平锁和非公平锁 例如:公平锁用 new ReentrantLock(true) 来实现
独享锁与共享锁
独享锁: 该锁一次只能被一个线程持有
共享锁: 该锁可被多个线程所持有
比较:
对于 Java ReentrantLock 而言是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock ,其读锁是共享锁,写锁是独享锁 读锁的共享锁可保证并发读是非常高效的 独享锁与共享锁也是通过 AQS 来实现的
分段锁
一种锁设计,不是具体的一种锁
Java 常见锁机制
synchronized
ReentrantLock
Samaphore
AtomicInteger
concurrent 包(并发包)
实现结构
volatile
被 volatile 修饰的变量,如果值发生了改变,其他线程立马可见
CAS
参数:需要读者的内存值 v ,进行比较的值 a ,要写入的值 b 当且仅当预期值 a 和内存值 v 相同时,将内存值 v 修改为 b ,否则返回 v
AQS
AQS数据模型
三个核心成员变量
共享资源: volatile int state 队头节点: head头节点 队尾节点: tail尾节点 三个变量都是volatile修饰的,通过它来保证共享变量的可见性
AQS中state状态变更是基于CAS实现的
主要有三个方法: state状态通过volatile保证共享变量可见性,再由CAS对该同步状态进行原子操作
CLH队列(FIFO)
CLH队列通过内置的FIFO队列来完成线程等待队列排队
资源共享方式
独占模式,ReentrantLock
共享模式,Semaphore
AQS锁获取与释放原理
获取锁
1 线程a获取锁,state将0设置为1,线程a占用 2 在a没有释放锁期间,线程b也来获取锁,线程b获取到state为1,表示线程被占用,线程b创建一个Node节点放入队尾,并且阻塞线程b 3 同理线程c获取state为1,表示线程被占用,线程c创建Node节点,放入队尾且阻塞线程
释放锁
1 线程a执行完,将state从1设置为0 2 唤醒下一个Node b线程节点,然后再删除线程a节点 3 线程b占用,获取state状态,执行完后唤醒下一个Node节点
ReentrantLock
主要由CAS和AQS实现,支持公平锁和非公平锁
ConcurrentHashMap
子主题