线程间的通信
- 共享内存
访问内存中的公共状态----隐式通信 - 消息传递
线程之间没有公共状态-通过发送消息,来显示通信
线程之间的同步
- 共享内存方式,需要手动指定不同线程发生顺序 – 显式
- 消息传递,消息的发送必须再消息接收之前,所以是,同步是—隐式的。
内存抽象
- 堆: 实例,静态域,数组
- 栈:
- 内存屏障
从源代码到指令序列的重排序
- 1 编译器优化重排序
- 2 指令级并行重排序
- 3 内存系统重排序
happens-before JDK5 后,使用happens-before
规则
- 同一个线程的中的任意操作 happends-before 该线程后续操作
- 一个锁的解锁 先于 随后对这个锁的加锁
- volatile 的写,先于后续对volatile的渎
- 传递性
- 注 happens-before 指前一个操作按照顺序排在后面一个操作之前,且结果对后一个可见。。。并不一定前一个先于后一个被执行。
数据依赖性
两个操作访问同一个变量,一个为写操作就存在数据依赖性(写后读,写后写,读后写)
重排序会导致结果的改变。所以编译器和处理器在重排序会遵守数据依赖性,即不会对此重排序
注:(只针对单核CPU且单线程)
as-if-serial (不管怎么重排序,执行结果不能被改变)
int a =1;
int b = 2;
int c=a+b;
waitNotify 经典范式
flag 是一个存储在内存中的VOLITALE 标志,,或者可以是对象中的一个属性,主要是flag要对两个线程都可见
- 通知方
synchronized(对象){
改变flag
对象.notifyAll()
}
- 接收方
synchronized(){
if (!flag){
对象.wait()
}
逻辑代码
}
即两个线程之间通过共享内存中的变量flag进行通信
管道输入/输出流(piped)
类似文件读写的字节流,字符流,,,但是有一点不同,,它的传输媒介是内存,主要应用于不同线程之间的通信
面向字节
- PipedOutputStream
- PipedInputStream
面向字符
- PipedWriter
- PipedReader
pip类型的流要先绑定即,使用connect方法(否则访问该流的时候会抛出异常)。
join 方法
thread2 需要等待thread1 结束以后再停止,那么先thread1.join() 然后thread2 停止
java中的各种锁
Lock 接口,有以下方法
- void lock() // 阻塞 获取锁,调用该方法当前线程将会获取锁,获得以后从该方法返回
- void lockInterruptibly() //阻塞 可中断的获取锁,在获取锁的过程中,可以响应中断
- boolean tryLock() 非阻塞的获取锁,调用以后立马返回,获取到锁返回true,失败返回false
- tryLock可以填入超时参数,(超时,中断,获取到锁)会返回
- void unlock() 释放锁
- Condition newCondition() 获取等待通知组件,组件和当前锁绑定,只有获得了锁才能调用组件的wait方法释放锁
同步器 构建锁的基础框架
内置一个int型的变量表示同步状态
内置FIFO 队列完成要获取锁的线程的排队
同步器的使用
需要继承同步器,并重写抽象方法
1 使用同步器的
- getState 获取状态
- setState 设置状态
- compareAndSetState(int expect,int update) CAS方法设置当前状态可以保证设置的原子性
2 重写方法
独占式
- boolean tryAcquire(int arg)//独占式获取状态
- booblean tryRelease(int arg) //释放同步状态
共享式
- int tryAcquireShared(int arg) //返回大于等于0 获取成功
- boolean tryRelaseShared(int arg) //释放同步状态
- boolean isHeldExclusively() // 同步状态是否被当前状态独占
- 独占锁:同一时刻只能由一个线程访问锁的状态也就
- 共享锁,多个线程可以同时获取锁状态,同时访问资源
例子: 文件读写,,读操作是共享式,写操作是独占式
同步方法基本分三类
- 独占式的获取释放锁状态
- 共享式获取和释放锁状态
- 查看队列中的线程情况
队列同步器的实现
-
同步队列: 里面存放的是获取同步状态失败的线程(Node)
-
什么是Node: Node=等待状态+prev+next+nextWaiter+thread
-
同步队列的首节点释放锁以后,从首届点位置出列,原后继节点变首节点 遵循FIFO原则
-
缓存队列Condition:
重入锁ReentrantLock
一个已经获取到锁的线程,没有释放锁的前提下任然可以获取到锁
重入的实现:
在获取锁的时候做了两步判断
1,锁是否可用
2,不可用,,获取锁的线程是不是当前的线程。
是 —> 返回true state ++
否 -> 返回false 获取锁失败
公平和非公平的实现:
tryAcquire
在获取锁状态时候对了一个判断,当前节点是否有前驱节点
是 -> 说明前驱节点比当前节点更早的请求锁,等待前驱节点释放以后才能执行该线程
否 -> 当前节点就是首节点,执行
非公平锁是默认的(线程切换少,有更大的吞吐量,但是可能会线程饥饿)
读写锁(ReentrantReadWriteLock)
之前的Mutex 和 ReentrantLock 都是排他锁也就是同一时刻只允许一个线程加锁。
特性 :
-
支持公平性和非公平性
-
支持重进入
-
支持降级 级别 写锁 > 读锁 写锁可以降级为读锁
-
读锁-写锁,写锁-写锁,之间是互斥的,,
-
读锁之间是共享的
ReentrantReadWriteLock 是ReadWriteLock的实现类。
ReentrantReadWriteLock的同步器的状态state
独占锁 state boolean true 获取锁成功 false 获取锁失败
共享锁 state int >=0 获取锁成功 < 0获取锁失败
读写锁 state int 高16位 读状态 低16 位
可降级(重要特性)两大特点
- 可以提高并发
- 可以保证线程的安全
while (true) {
try {
writeLock.lock(); //1 加写锁
System.out.println(">------>>>>>-------->>>>加写锁>------>>>>>-------->>>>加锁完成,当前线程为" + Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("---------write--------,我是-------------------------------------------------------------------------" + Thread.currentThread().getName());
// 此处写更新数据的业务逻辑 3
System.out.println("------------->>>>>-------->>>>加读锁>------>>>>>-------->>>>加锁完成");
readLock.lock(); //2 加读锁
System.out.println("--------<<<<-----<<<<<解写锁<<<<<-------<<<<<------------<<<<---------");
// 从此刻开始 其它的读线程可以加读锁读取数据 因而提高了并发 但是,读锁并没有释放,所以其它的线程不能加写锁,因而保证了数据安全,,后面的业务逻辑5处,保证了数据不会被其它线程更新
writeLock.unlock(); // 4 解写锁,,
//....... 此处业务逻辑 5
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("<<<<-----<<<<<解读锁<<<<<-------<<<<<释放------------<<<<,当前线程为" + Thread.currentThread().getName());
readLock.unlock();// 6 解读锁
}
}
降级
即支持 获取写锁->获取读锁->释放写锁->释放读锁
升级,不支持
不支持 获取读锁->获取写锁->释放读锁
JAVA并发容器和框架
ConcurrentHashMap 线程安全且高效的HasMap
hashMap 线程不安全的两点
- put 操作 两个线程同时put,key冲突导致最后执行put的元素覆盖掉之前的
- 扩容的时候
hashTable的低效率
- hasTable内部使用synchronized 效率低,在put的时候不能get
锁分段技术
在hashTable的基础上,将数据分段存储,分段加锁,,,段与段之间相互不影响。当对一段执行put时候,,,其它段的put和get均可以操作
- segment 锁
- hashEntry 数组,存放的数据 一个锁对应若干个hashEntry
ConcurrentLinkQueue 线程安全的队列
- head 节点
- tail 节点
在ConcurrentLinkQueue 队列中,,tail并不总是指向尾节点,同理head也并不总是指向头节点。。
入队
- 先去看自己的tail节点是否是尾节点,
如果是尾节点,用cas方法插入元素,成功-ok,失败-重新插入(说明有其他元素刚插入) - tail节点不是尾节点,通过循环查询,找到尾节点,在尾节点cas插入元素,并将tail指向尾节点
出队列
类似于入队,不过操作的是头节点。
线程安全是通过CAS方法实现的入队和出队
高效—hop ,因为并不是每次都会更新tail节点,
BlockingQueue 阻塞队列
阻塞队列 是线程安全的队列
两个特点
- 队列元素为满时候,继续插入元素的线程会阻塞
- 队列元素为空的时候,获取元素的线程会阻塞
三类方法
方法 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时推出 |
---|---|---|---|---|
插入方法 | add() | offer() | put() | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(e,time,unit) |
检查方法 | element() | peek() | 不可用,不可用 |
无界队列永远返回true,JDK7 提供的7个阻塞队列
- ArrayBlockingQueue() 数组结构 有界阻塞队列
- LinkedBlockingQueue 链表结构 有界阻塞
- PriorityBlockingQueue 支持优先级无界阻塞
- DelayQueue 使用优先级队列实现的无界阻塞
- SyncchronousQueue 一个不存储元素的阻塞队列
- LinkedTransferQueue 链表结构无界阻塞
- LinkedBlockingDeque 链表结构双向阻塞
Fork/join框架(工作窃取算法)
Fork - 切分任务
join - 合并子任务的执行结果
工作窃取 - 一个线程从其它线程的任务队列里面获取任务并执行
Fork/Join框架基本思想是将一个大任务分成若干个小任务,分在若干个双端队列里面,并给每个队列开设单独的线程。 工作窃取算法,在执行完自己队列里面的任务会从其它队列的尾部获取任务执行。
任务分割
ForkJoinTask - 分解任务
两个子类(继承它)
- RecursiveAction 用于没有返回结果的任务
- RecursiveTask 用于有返回结果的任务
任务执行
ForkJoinPool 执行任务
java中的13个原子操作类
原子方式更新基本类型
- AtomicBoolean 原子更新boolean类型
- AtomicInteger 原子更新整形
- AtomicLong 原子更新长整形
三个类几乎提供相同的方法
<!-- AtomicInteger 类 -->
int addAndGet(int data) //将括号里面的值与 AtomicInteger 里面的值原子性相加并返回结果
boolean compareAndSet(int expert,int update) // 如果输入的等于预期的,,将该值设置为输入的值
int getAndIncrement() // 以原子方式将当前值+1 返回的是+1 之前的值
void lazySet( int new value) // 最终会设置成新的值,,,但是使用该方法以后,其它线程可能在一小段时间内读到的还是之前的值
int getAndSet(int new value) //以原子方式设置为newValue的值 并返回旧值
int get() // 拿到值
原子更新CAS实现 : 比如 要将 1 设置成2 cas的两个参数 except(预期值)即为 1 update 为 2 再 cas的时候会先去get() 当前值是否是预期值1 是,说明没有被其它线程修改过,设置为2 返回true, 如果不是1 则说明被其它线程修改过了,不进行设置返回false
我的理解: 原子类的cas方法基本都有UnSafe 类实现
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
UnSafe 类只有三种cas方法 (AtomicBoolean 是将boolean转化为int 类型,然后使用int类型的cas方法)
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
原子更新数组
- AtomicIntegerArray 原子更新整形数组里面的元素
- AtomicLongArray 原子更新长整形数组里面的元素
- AtomicReferenceArray 原子更新引用数组里面的元素
- AtomicIntegerArray 原子的方式更新数组的整形
原子更新引用类型
- AutomicReference: 原子更新引用类型
- AtomicReferenceFieldUpdater: 原子更新引用类型里面的字段
- AtomicMarkableReference: 原子更新带有标记的引用类型
原子更新字段类
- AtomicIntegerFileldUpdater 原子更新整形字段的更新器
- AtomicLongFileldUpdater: 原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本的引用类型
JAVA中的并发工具类
CountDownLatch 等待多线程完成
传统的让当前线程等待其它线程执行完使用join
join的工作原理是,不停的去查询线程是否存活,如果其它线程存活,则让当前线程永远等待wait(0), 其它线程结束以后 使用notifyAll 因为notifyAll 封装再JVM里面,所以看不到。
使用步骤
- 1 构造方法参数设置点数。。
static CountDownLatch count = new CountDownLatch(2);
- 2 每个线程执行完成以后调用countDown()方法使得点数-1
- 3 等待线程中使用await()方法阻塞,当点数变成0的时候,会解阻塞,继续向下执行
CyclicBarrier 同步屏障
相当于创建了一个屏障,线程任务执行完成到达屏障调用await()方法通知屏障我已经到达,当满足某个条件的时候,屏障会打开,程序得以继续执行。 这个条件同样由构造方法的参数指定。
static CyclicBarrier cyclicBarrier = new CyclicBarrier(4); // 参数设置 点
// 高级用法,所有线程到达先执行methodFlag()自定义的线程,需要实现runable接口,重写run方法
static CyclicBarrier cyclicBarrier2 = new CyclicBarrier(4, new MethodFlag());
CyclicBarrier 和 CountDownLatch 区别
- CyclicBarrier 构造函数多了一个重载形式,第二个参数可以指定,所有线程到达屏障后优先执行的方法
- CyclicBarrier 可以重复使用,使用reset() CountDownLatch 只能用一次
,就像 CountDownLatch 是一次性班车,人满了20个发车,只发一次。
CyclicBarrier 是可以重复使用的,每20个人发一车然后reset 然后等到20个再发一车。
Semaphore 控制并发线程数量
控制并发的线程的数量,,多个线程读取 和少个线程处理做适配(100个线程从内存读取数据,然后写入数据库,数据库只允许最大10个连接,100个线程的读取可以同时进行,但是写入的时候一次只有10个线程可以执行,其它的需要阻塞)
使用步骤
- 构造方法参数指定允许的最大数量
- 在需要控制线程数量的处理逻辑之前使用Semple的accquire()方法获取许可证,之后使用relese() 释放许可证。
public class SemaphoreDemo {
static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
Task task = new Task();
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(task, "task-" + i);
thread.start();
}
}
static class Task implements Runnable {
@Override
public void run() {
// 此处不需要控制线程数量的逻辑处理
System.out.println("读取完成 准备写入--我是Thread---" + Thread.currentThread().getName());
try {
semaphore.acquire(); // 获取许可证
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此处写需要控制线程数量的逻辑处理
System.out.println("写入中---我是Thread---" + Thread.currentThread().getName());
semaphore.release(); // 释放许可证
}
}
}
Exchanger 线程之间交换数据
- 只能用于两个线程的交换,如果有多个线程执行交换
- 那么按照线程发生的顺序两两交换。
- 两个线程交换数据,一个线程执行了exchange,另一个没有执行,该线程会阻塞,等待另一个线程执行。
- 如果一个线程阻塞一致没有交换对象。那么该线程会一致阻塞,用exchange(dataB,5, TimeUnit timeUtil) 这个方法,交换,可以设置超时时间,超时没有交换对象会报异常
使用步骤
- 1 建立 Exchange 对象
private static final Exchanger<String> exchange = new Exchanger<>();
- 2 在需要交换的地方使用exchange()方法交换数据
static class JobA implements Runnable {
@Override
public void run() {
String dataA = "dataA";
System.out.println("我是线程---" + Thread.currentThread().getName());
try {
System.out.println("jobA 拿到了--" + exchange.exchange(dataA));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class JobB implements Runnable {
@Override
public void run() {
String dataB = "dataB";
System.out.println("我是线程---" + Thread.currentThread().getName());
try {
// exchange.exchange(dataB)
System.out.println("jobB 拿到了--" + exchange.exchange(dataB,5, TimeUnit.SECONDS));
} catch (InterruptedException | TimeoutException e) {
e.printStackTrace();
}
}
}
线程池
三大优点
- 降低资源消耗(免去了创建和销毁线程的消耗)
- 增加响应速度 (直接执行任务,而不用等待创建线程)
- 提高线程的可管理性(控制线程的数量)
当一个异步任务来了之后
- 1 判断核心线程池是否有空闲,否->分配线程执行任务,是->下一步
- 2 判断等待队列是否已满,否-> 将异步任务放入等待队列, 是-> 下一步
- 3 判断当前线程池中线程数量是否大于最大线程池数数量,否创建(或分配线程)执行任务,是-> 交由饱和策略。
刚开始我很迷惑,为什么 顺序是1->2->3 而不是1->3->2。
直到看到线程池创建线程的过程, 因为创建线程需要先获取全局锁,而获取全局锁将会是一个严重的性能瓶颈,所以尽量的避免获取全局锁,,而添加到队列不需要获取全局锁,所以将添加到队列放在了第二步。 绝大部分任务都是在第二步中被放入了等待队列。
使用步骤
1 ThreadPoolExecuter创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,10,1, SECONDS, new ArrayBlockingQueue<>(5));
必要参数
-
核心线程池数量
(这里创建的核心线程池,默认刚开始是没有创建线程的,等工作任务来了之后,才开始创建线程,当线程数小于核心线程池大小时候,来的任务都会创建新线程,即使其它线程空闲。直到线程数量=核心线程池的大小,这一步也称为预热,当然也可以通过) -
最大线程池大小:线程池中允许的最大线程数量,(等待队列满了以后,再来的任务会创建新线程,,线程空闲以后,时间超过aliveTime会被销毁,,如果等待队列是无限队列,那么这个值是没有意义的,因为任务会一致添加到等待队列中)
-
aliveTime 线程空闲以后保持存活的时间,(任务频繁的时候,适当增大该值,可以提高效率,但是任务如果是不频繁的那种,线程长时间空闲,会增加系统消耗
-
TimeUnit 时间工具类,只要是给前面的时间指定单位
-
BlockingQueue 等待队列,通常以下几种
ArrayBlockQueue 数组对列,FIFO ,有界
LinkBlockingQueue 链表队列,FIFO, 有界
SynchronousQueue 不存储元素的对列(一次只能进一个,必须被消费了,才能继续进队列)
PriorityBlockingQueue 一个具有优先级的无限阻塞队列
可选参数
ThreadFactory
- ThreadFactory: 用于设置创建线程的工厂,可以给线程起一些有意义的名字
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
饱和策略(默认的是直接抛出异常)
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
- AbortPolicy : 直接抛出异常
- CallerRuhnsPolicy 只用调用者的线程来执行任务
- DiscardOldestPolicy 丢弃队列里最近的一个任务并执行当前任务
- DiscardPolicy 不处理,丢弃掉
同样也可以实现RejectedExecutionHander 接口自定义策略
2 创建任务
简单 实现Runable接口,重写润方法
3 提交任务给线程池
submit()或者execute()方法
线程池的关闭
shutdown和shutdownNow
同:都是通过interrupt方法去中断线程(如果一个线程不能响应中断的话,那么线程任务可能永远无法中止)
关闭线程后isShutdown 就会返回true
所有任务都关闭以后,isTerminated方法会返回true
异:
shutdown: 将线程池状态设置为SHUTDOWN,然后尝试中断空闲的线程(通常调用这个)
shutdownNow: 将线程池的状态设置为SHUTDOWN ,然后尝试中断所有线程(如果不要求线程任务必须执行完的话也可以使用这个)
线程池的监控
executor.getTaskCount(); // 线程池需要执行的任务数量
executor.getCompletedTaskCount();// 已经执行的任务
executor.getLargestPoolSize(); // 曾经最大 (可以知道线程池是否满过)
executor.getPoolSize(); // 线程池的大小,只增不减??? >
coreSize+queueSize 之后创建的线程
executor.getActiveCount();// 获取活动的线程数
Executer框架
- java的线程即是工作单元,也是执行单元。
工作单元(任务具体)包括:Runable Callable。
执行单元(任务的执行): Execute 框架
Execute 框架的结构(3大部分)
- 任务:实现runable或者callable接口(工具类Executors可以将runable封装为callable对象)
- 任务的执行:Executor->ExecuterService->(ThreadPoolExecuter,ScheduledThreadPoolExecutor)
- 异步计算的结果:Future—>FutureTask
成员
- ThreadPoolExecutor // 创建线程池的类
- ScheduledThreadPoolExecutor // 执行周期任务的线程池
- Future // 以及其实现类FutureTask 用来表示异步计算的结果,当我们把实现runable接口,或者callable接口的实现类(submit)方法提交给ThreadPoolExecutor执行的时候,会返回一个Future对象
- Runable 不可以返回结果,可被执行
- Callable 可以返回结果,可被执行
runable 可以包装成Callable两种包装方法:
-
public static Callable callable(Runnable task) // 假设返回对象Callable1
-
2 public static Callable callable(Runnable task, T result) // 假设返回对象Callable2
两种包装方法的区别是
第一种 使用submit 提交给ThreadpoolExecute 提交的时候返回结果为null,第二种会返回结果result对象。
-
总结:
一个Executor的工具类,默认的创建线程池的有三种(原理上也是使用ThreadPoolExecuter创建的线程池,不过一些参数设置为了固定值), -
1 FixedThreadPool // 可重用固定线程数的线程池
-
2 singleThreadPool // 单线程的线程
与上面的唯一差别是线程数量为1 -
3 cacheThreadPool // 会根据需要创建新线程的线程池
-
4 ScheduledThreadPoolExecutor 继承自ThreadPoolExecuter //主要用来再指定延迟之后执行任务,或者定期任务,,可以在构造函数中指定多个后台线程