第四章JAVA并发

4.1 并发基础 4.2 所有锁原理 4.3 votaile原理 4.4 并发容器原理 4.5 Fork_Join原理

4.1 并发基础

CPU原理(哈哈,以后我当面试官了就问CPU原理)
核心是晶体管(一种方式找到数字且能计算)
不导电的硅+加某些东东变成半导体
5V通电代表1,不通电代表0

进程和线程区别–略

线程中start和run区别

start会创建一个新的子线程启动,run方法只是thread下一个普遍方法调用

线程实现方式

1 extends Thread重写run方法
2 implement Runable接口重写run方法,一般用2不用1,因为接口可以impl多个,extend用了就继承不了别的类了
3 使用线程池

如何处理线程返回值

1 主线程循环等待法
2 join()阻塞当前线程来等待子线程处理完毕
3 通过callable接口实现FutureTask或者线程池
4 直接线程池拿(最好)

线程6种状态

1 New 创建状态
2 Runnable 运行中
3 Waiting 无限等待
4 Timed Waiting 等待一定时间后唤醒
5 Blocked 阻塞
6 Terminated 结束

sleep和wait区别

sleep是thread类中方法,sleep是object类中方法
wait 不仅让出cpu 还会释放已经占有的同步资源锁,sleep 是仅仅让出cpu

Yield

Thread.yield()通知改线程可以让出CPU了,但是程序不一定执行

如何中断线程

直接使用stop方法会造成清理问题,可能同时释放其他锁产生的问题
应该使用interrupt()通知线程该中断了
	1 如果线程被阻塞,该线程会立即退出阻塞并抛出异常
	2 如果线程在正常活动状态,该线程中断标志位会设为true,但是线程仍继续运行
	所以while(!Thread.currentThread().isInterrupted())不断校验中断标志位,如果true就自行停止

线程池

线程池执行流程 
 1 核心线程池是否满,不满就创建线程,满了走2
 2 等待队列是否满,不满放入等待队列,等待队列满了走3
 3 最大线程池是否满,不满创建线程,该线程池线程存活时间不能超过keepAliveTime。如果最大线程池满了走饱和策略
 4 饱和策略有 拒绝新任务,调用自己线程,不处理直接丢弃,丢弃最早未处理的
 
FixedThreadPool 固定核心线程池,等待队列无上限会OOM
SingleThreadPool 一个核心线程池,等待队列无上限会OOM
CachedThreadPool 允许创建线程无上限会OOM
ScheduledThreadPool 线程定时作用

题外话Netty源码中reactor设计模式中的线程继承了 
SingleThreadPool 和ScheduledThreadPool ,将来写Netty源码会继续写。

所以根据阿里开发手册的要求,我们自己配置线程池才是最好的。

线程通信

1 文件共享:多个线程处理同一个文件
2 变量共享:static变量
3 wait/notify/notifyAll 有顺序要求
4 park/unpark 无顺序,park多次后unpark一次也直接运行,

notify和notifyAll区别

锁池EntryList类似sleep不释放同步资源锁
等待池WaitList类似wait会释放同步资源给其他调用的
notify是随机选一个waitList中线程进入EntryList,
notifyAll是所有waitList中线程进入

4.2 所有锁原理

Java下Synchronzied锁

首先获取对象锁
1 同步代码块(synchronized(this))
2 同步非静态方法(synchronized method) public synchronized void xxx()

类锁
1 同步代码块(synchronized(类.class))
2 同步静态类(synchronized static class)

若锁同一对象,同步方法和同步方法块会互相阻塞,
但是类锁和对象锁互相不干扰

synconronzied信息保存在内存存储布局中对象头内,
该锁分无锁,偏向锁,轻量级锁,重量级锁四个状态

锁四个状态升级步骤:

当一个线程拿到锁,进入偏向锁状态,该线程再次反复请求时,只需要通过对象头中
所标记位为偏向锁且标记位的线程ID相同,则识别是同一线程可以继续使用。

当第二个线程进入时,偏向锁升级轻量级锁。其他没有抢到锁的线程通过一个锁记录
CAS形式自旋不停争抢锁

当轻量级锁下有线程自旋超过设定限制,默认大于10次或者大于CPU核数一半,升级
到重量级锁

Java同时也有锁消除,锁粗化等来优化锁性能

JIT(Just Time Compiler)

经常被使用的代码进行即时编译(热编译)直接成机器语言,
下次使用不需要再走解释器,可以直接运行

CAS

CAS思想

三个操作数: 当前内存位置V,曾经内存位置E,当前新值A
当V和E相等时,则更新当前值为A,否则不更新。

但是会存在ABA问题,曾经内存位置E变成了D但是D之后又变回E当V和E比较时候发现内存位置一样更新值成功,但是V并不知道中间发生了其他变化,会产生很多问题。

ABA解决方案

1 通过加boolean判断是否已经修改
2 添加版本号,比较版本号和V值

Reentrantlock锁

主要原理是AQS等待队列+自旋锁
自旋锁又主要通过unsafe类中的park/unpark方法(用来睡眠线程)+CAS实现

简单版本自旋锁

int votaile state
while(!CAS(state))//state为0则可以拿到锁,state为1则拿锁失败{
	//拿锁失败,在while下不停自旋
	park(底层是当前线程进入AQS等待队列并且释放CPU)
}
//到这里表示已经拿锁成功
业务逻辑处理
最后unlock(unpark()修改states为0给别的线程抢锁)

AQS是包含线程的双向链表
如果设置公平锁则按照链表顺序拿锁
如果设置非公平锁则大家一起自旋抢

4.3 votaile原理

JVM层面内存屏障+操作系统层面缓存行间数据一致性

缓存行

缓存行是L1,L2,L3等(操作系统层面)缓存最小单位。一个缓存行会对齐64字节。
当数据完整在同一个缓存行时,代码会执行更快,因为如果一个数据用两个不
同缓存行存放,会增加不同缓存行调度的开销。所以比如disruptor底层,一个
重要long cursor变量,前面有7个无用的long,后面有7个无用的long,这样会
确保这个cursor变量一定在同一个缓存行内,加快对这个cursor变量调用的运
行速度。

那么操作系统是如何保证数据一致性呢?

1 英特尔实现方式 MESI Cache一致性协议
缓存行有4中状态 Modified/Exclusive/shared/invalid,当CPU1中L1缓存行下x
变量状态修改值变成Modified状态,CUP2中L1缓存行下x变量的状态变成invalid,
通过硬件上的缓存锁立刻更新最新的数据

2 如果MESI缓存一致性协议不行,则锁总线(这个CPU修改时,其他CPU不允许修改)

JVM内存屏障

屏障两端指令不允许重排序,保证可序性
四种内存屏障LoadLoad,StoreStore,LoadStore,StoreLoad四种屏障
比如StoreLoad表示,必须执行完Store才能执行Load

votaile写操作
会在votaile前面加一层StoreStoreBarrier后面加一层StroeLoadBarrier

votaile读操作
会在votaile前面加一层LoadLoad后面加一层LoadStore屏障

经典例题烂大街例题为什么单例模式双重校验加锁下的变量需要加votaile?

因为如果没有votaile禁止重排序,会因为java自己重排序在并发情况下导致第二个
线程发现变量不为null,直接使用第一个线程还没有初始化完毕的半初始化对象

4.4 并发容器原理

非并发容器
collection下实现了set和list
map下实现了hashmap和sortedmap,sortedmap实现了treemap(红黑树)

ArrayList – 初始0,add后变成10,然后扩容✖1.5

HashMap – bucket数组+链表,初始容量16。当bucket占满百分之75扩容✖2,当链表数量大于8链表变成红黑树

HashTable — 初始11,扩容2n+1

并发容器

CountDownLatch (倒计时器)
CyclicBarrier(循环栅栏)

concurrentHashTable 好像没人用,锁住整个table,性能低下

concurrentHashMap

老版本是用reentrantlock实现sgement数组,
每个 segment 守护着几个HashEntry数组里的元素

新版本在synchronize锁优化后,
synchronized锁定当前链表或红黑二叉树的首节点

copyOnWriteArrayList
copyOnWriteArraySet
此部分节选自文章

当读操作会远远大于写操作情况下会用,通过创建底层数组的新副本来实现的。
当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,
将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就
可以保证写操作不会影响读操作了

Atomic原子类

CAS + volatile 和 native 方法来保证原子操作

非阻塞队列,性能相比阻塞队列好很多

concurrentLinkedQueue -- CAS

阻塞队列 都使用 ReentrantLock锁实现,所以可以实现公平竞争

ArrayBlockingQueue	
LinkedBlockingQueue
PriorityBlockingQueue

ThreadLocal

在Netty对象池源码的实现即用到了ThreadLocal
ThreadLocal变量每个线程独立存在,每个线程内部通过一个map维护它
原理
	Thread.currentThread()获取到当前线程对象后,
	直接通过getMap(Thread t) 可以访问到该线程的ThreadLocalMap对象,
	ThreadLocalMap的 key 就是 ThreadLocal对象,
	value 就是 ThreadLocal 对象调用set方法设置的值

ThreadLocal 内存泄露问题 此部分节选自文章

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,
key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就
会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 
回收,	这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了
这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 
的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

既然有内存泄漏风险,为什么key设为弱引用?

设计成弱引用是为了更好地对ThreadLocal进行回收,ThreadLocal的value
也就是强引用置为null后,这时候Entry中的ThreadLocal就应该被回收了,
但如果key是强引用则该ThreadLocal就不能被回收,只有整个线程回收了
才会回收这个map下ThreadLocal,就会产生内存泄漏。

4.5 Fork_Join原理

ForkJoinPool是ExecutorService接口的实现,本质就是线程池
通过多个fork join并行形式处理业务处理压力

简单思路(用于一个线程处理量太大的情况)

if(工作量小){
	直接执行
}else{
	当前工作fork()拆分两块
	调用这两部分并行执行
	join()合并执行结果
	返回最后结果
}

封装在ForkJoinTask子类后
RecursiveTask(返回结果)
RecursiveAction(不返回结果)

应用场景

1 后端分布式系统,拆解很多子系统,查很多系统速度慢
2 一个接口请求需要很多信息

工作原理

一个task会fork成多个小task,当别的线程空闲时,会窃取这个线程下
小task帮忙处理,从而实现并行处理,发挥多核CPU优势,加快运行速度

应用代码

~ 主程序 mian ~{
	ForkJoinPool pool = new ~(10//CPO核数,ForkJoinPool.defaultForkJoinWorkerThreadFactory, null//异常handler,true//asyncMode);

	ArrayList<String> urls = new ~;
	urls.add(xxx)//多次添加很多很多url

	HttpJsonRequest request = new ~(restTemplate,urls,0,urls.size()-1);
	pool.submit(request);

	JSONObject result = forkJoinTask.get();//获取最终结果
}

public class HttpJsonRequest extends RecursiveTask<JSONObject>{
	int start, end;
	RestTemplate restTemplate;
	ArrayList<String> urls;

	构造函数给上面变量赋值

	//核心代码,线程池实际执行入口,实现任务拆分
	@Override
	protected JSONObject compute(){
		int count = end - start;
		if(count==0){
			//立即调用,用restTemplate根据url进行业务处理
		}else{
			//递归拆分子任务
			int mid = (start+end)/2;
			HttpJSONObject request1 = new ~(restTemplate,urls,start,mid);
			request1.fork();//当前线程提交到线程池内
			HttpJSONObject request2 = new ~(restTemplate,urls,mid+1,end);
			request2.fork();//当前线程提交到线程池内
			//join整合结果
			JSONObject result = new ~;
			result.addAll(request1.join());
			result.addAll(request2.join());
			return result;
		}
	}
}
原创文章 5 获赞 5 访问量 286

猜你喜欢

转载自blog.csdn.net/weixin_40503364/article/details/106185163