栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JAVA-线程,线程池,ThreadLocal,CAS,AQS,Volatile-个人笔记

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JAVA-线程,线程池,ThreadLocal,CAS,AQS,Volatile-个人笔记

自定义

	1.创建一个类继承Thread类,重写run()方法。
	2.创建一个类实现Runnable接口,重写run()方法,
		创建Thread对象,并将自定义类对象作为参数传入。
		可以多实现,可共享数据
	3.Callable/Future:
		callable可以返回执行结果
		future可以通过get的方式拿到结果(阻塞当前线程,直到该线程(callable)拿到结果)
	4.线程池创建

线程状态:新建/就绪/阻塞/运行/结束

 (new)                              (block)
  ||							  //  /
  ||				sleep结束//	  	  ||sleep
  ||start	   join结束//		  	  ||join
  ||	 IO完成//			  		  ||IO请求
  ||	//				  			  ||
  ||//							  	  ||
  /			===获取CPU===>>		  
(Runnable)                        (running)  ===正/异常结束===>>   dead
  ||   			<<===yield====		  ||
  ||								//	\
  /						//wait				\sync
  ||				//									\
  ||			   /									 /
  ||		 (等待blocked) ===notify/all/interupt===> (锁定blocked)
  ||														||
  ==<<================<<====获取同步锁=======<<================											

1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
	1) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
	2) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
	3) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程池(队列/策略):ExecutorService/CompletionService
参考 https://www.cnblogs.com/dolphin0520/p/3932921.html

	分类(区别/优缺点/如何实现):固定数量线程池/单线程池/定时任务执行线程池/缓存线程池
	
	拒绝策略:
		触发条件:当线程池中线程数达到max值,且任务队列任务已满,有新任务进来时,才会触发拒绝策略
		策略分类:
			丢弃任务并抛出RejectedExecutionException异常
			丢弃任务不抛异常
			丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
			交由提交任务的线程去处理

ThreadLocal/InheritableThreadLocal/TransmittableThreadLocal

ThreadLocal
	set():操作Thread中的ThreadLocalMap对象,将ThreadLocal作为键,目标value作为值存储。步骤大致如下:
		1.getMap:获取当前线程中的ThreadLocals属性,即获取ThreadLocalMap	(currentThread.ThreadLocals)
		2.createMap:如果获取不到,则重新创建一个ThreadLocalMap赋值给currentThread.ThreadLocals,并将threadLocal对象作为键,value作为值存到currentThread.ThreadLocals中
		3.如果可以获取到,则调用map的set方法将threadLocal对象作为键,value作为值存到currentThread.ThreadLocals中
	get何remove方法逻辑基本相似
	弊端:子线程无法访问到父线程中的本地变量

InheritableThreadLocal:继承了ThreadLocal,为了解决子线程无法访问到父线程中的本地变量的问题
	与ThreadLocal一样,都指向的是ThreadLocalMap,只是重写了getMap和createMap方法,这两个方法操作的都是Thread的inheritableThreadLocals属性
	线程本地变量是如何进行父子间的传递的?
		Thread在初始化时,调用了init方法,最终是把父线程的ThreadLocalMap赋给了子线程(类似于map的复制)
		1.Thread() {
			init(...);
		}
		2.init(...){
			...
			if (inheritThreadLocals && parent.inheritableThreadLocals != null)
	            this.inheritableThreadLocals =
	                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
	        ...
		}
	*注:子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承 InheritableThreadLocal 并重写 childValue 方法
	弊端:在线程池中,本地变量不可传递
	
TransmittableThreadLocal:继承自InheritableThreadLocal,在父线程在向线程池提交任务时复制父线程的上下环境。解决在线程池中,本地变量不可传递的问题
	
	1.holder属性:是TransmittableThreadLocal中非常重要的一个属性,静态全局,类型InheritableThreadLocal, ?>>;
		具体使用如下:
		private static InheritableThreadLocal, ?>> holder =
            new InheritableThreadLocal, ?>>() {
                @Override
                protected Map, ?> initialValue() {
                    return new WeakHashMap, Object>();
                }

                @Override
                protected Map, ?> childValue(Map, ?> parentValue) {
                    return new WeakHashMap, Object>(parentValue);
                }
            };
		其实就是一个InheritableThreadLocal对象,存放的值类型是Map, ?>
			最终Map使用的其实是WeakHashMap, Object>()对象类型,存储key为TransmittableThreadLocal,值为value
	2.Transmitter类:是TransmittableThreadLocal的静态内部类,用于辅助包装的Ttlxxx任务对象与TransmittableThreadLocal的holder对象进行交互
	

	传值过程:
		1.使用set()设置本地变量:如果value不为空,最终会将当前包含了value的TransmittableThreadLocal对象作为键,null作为值存到holder.get()对象中。      
			public final void set(T value) {
		        super.set(value);  															//1 将value存储到Thread.inheritableThreadLocals中
		        // may set null to remove value
		        if (null == value) removevalue();
		        else addValue();  //方法内部逻辑如下
		    }

		    private void addValue() {
		        if (!holder.get().containsKey(this)) {
		            holder.get()   															//2 获取的是一个Map,结构:Map, ?>
		            .put(this, null); 														//3 此处只是将this存进去了(与1⃣️中的key相同),并没有存value
		        }
		    }

		2.任务提交(submit):提交的是Runnable/Callable等,
			因为使用的TtlExcutorService线程池进行任务提交,它会自动将Runnable/Callable进行包装
				将其变成TtlRunnable/TtlCallable;同时将提交任务线程中的InheritableThreadLocal保存下来
					1.包装逻辑:
						private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
					        this.capturedRef = new AtomicReference(capture());     	//4 capture() 进行"值传递",
					        																//*注:最终提交的任务中capturedRef指向的是提交任务线程的本地变量
					        this.runnable = runnable;
					        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
					    }
				    2.capture():是Transmitter类的方法,可以操作TransmittableThreadLocal中的holder对象,进行"值传递"。即获取提交任务线程中的本地变量,为传递做准备
				    	public static Object capture() {
				            Map, Object> captured = new HashMap, Object>();
				            for (TransmittableThreadLocal threadLocal : holder.get().keySet()) {//遍历holder的keySet
				                captured.put(threadLocal, 									//6 将threadLocal与1⃣️对应设置的value绑定,存储在captured中
				                	threadLocal.copyValue());   							//5 threadLocal.copyValue:获取1⃣️提交任务的线程中threadLocal对应存储的value
				            }
				            return captured;   												//7 此刻已经拿到了提交任务线程中的本地变量
				        }
		3.run():提交的任务最重都会执行run方法
			public void run() {
		        Object captured = capturedRef.get();
		        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
		            throw new IllegalStateException("TTL value reference is released after run!");
		        }

		        Object backup = replay(captured);											//8 将任务中的本地变量赋值给当前线程,并将当前线程的原本地变量返回以备恢复,
		        try {
		            runnable.run();															// 任务执行
		        } finally {
		            restore(backup);														//9 将8的返回值传入,进行线程原本地变量恢复操作
		        }
		    }
 

闭锁、栅栏、信号量、FutureTask

	1.闭锁(Latch):可用于命令一组线程在同一个时刻开始执行某个任务,或者等待一组相关的操作结束,才会继续各自后续工作。尤其适合计算并发执行某个任务的耗时。如计数器闭锁CountDownLatch
		闭锁是一次性对象,一旦进入终止状态,就不能被重置
	2.栅栏(CyclicBarrier):用于阻塞一组线程直到某个事件发生。所有线程必须同时到达栅栏位置才能继续执行下一步操作,且能够被重置以达到重复利用
	3.信号量(Semaphore):用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量可以用来实现某种资源池,或者对容器施加边界。
	4.FutureTask:当一个计算的代价比较高,譬如比较耗时,或者耗资源,为了避免重复计算带来的浪费,可将已参与计算加入缓存,
		一旦从缓存中得知某个计算过程已被其他线程启动,则当前线程不需要再重新启动计算,只需要阻塞等待计算结果的返回。FutureTask与ConrrentHashMap搭配就是实现该功能的最佳选择。

锁/结构/过程/升级/重入/中断/唤醒/原子性/CAS(BAB)/AQS

分类:
	公平锁/非公平锁;公平锁:多个线程按照申请顺序来获取锁(锁维护了一个顺序队列)
	可重入锁:已经获取锁的线程,可以在该同步块中继续获取该锁(count+1)
	独享锁/共享锁(互斥锁/读写锁);
	乐观锁/悲观锁:悲观的任务,共享数据操作一定会出现竞争;乐观锁反之
	分段锁;ConcurrentHashMap

实例对象在内存中的存储结构:对象头/实例数据/填充数据(对齐填充)

	1.对象头:主要是由MarkWord和ClassmetadataAddress(类型指针)组成。如果对象是数组对象,那么对象头占用3个字宽(多出来的一个字宽用来存储数组长度),如果对象是非数组对象,那么对象头占用2个字宽。
			(字宽:word,1word = 2 Byte = 16 bit)
		1).MarkWord:用于存储对象自身的运行时数据,存储对象的hashCode、锁信息或分代年龄或GC标志等信息;
			MArkWord被设定为非固定结构,随着对象状态的变化而变化,来复用自己的存储空间(其中始终包含一个(锁的)指针和一个标志位属性:无锁和偏向锁为01(有个字段单独标识是否为偏向锁),轻量级锁为00,重量级锁为10,GC标志位为11):
		2).lassmetadataAddress:类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
			类元数据(类型信息是由类加载器在类加载的过程中从类文件中提取出来的信息):类全限定名/父类全限定名/父接口全限定名列表/是类还是接口/类访问修饰符/字段信息/方法信息/类成员变量信息(非final)/类加载器信息/指向Class的引用/基本类型的常量池
		
	2.实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐
	3.填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

MarkWord中锁的指针,指向的是一个monitor,虚拟机使用ObjectMonitor来包装,结构精简如下:

		ObjectMonitor:
			_EntryList:存放等待获取锁的ObjectWaiter对象列表
			_WaitSet:存放被wait的ObjectWaiter对象列表
			_owner:指向持有ObjectMonitor对象的线程
			_count:线程持有锁的计数
			...
			
			*注:每个等待锁的线程都会被封装成ObjectWaiter对象

过程:

	1.当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合;
	2.当线程获取到对象的monitor后,进入 _owner 区域并把monitor中的 _owner 变量设置为当前线程,同时monitor中的计数器 count 加1;
	3.若线程调用 wait() 方法,将释放当前持有的monitor,_owner 变量恢复为null,_count 自减1,同时该线程进入 _WaitSet 集合中等待被唤醒;
	4.若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)

升级:无锁/偏向锁/轻量级锁(锁自旋)/重量级锁/锁消除/锁粗化;

1.偏向锁:实验证明,锁在大部分情况下,不存在竞争,只会被一个线程多次持有。
		核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作(即获取锁的过程),这样就省去了大量有关锁申请的操作,从而也就提供程序的性能
2.轻量级锁:绝大部分锁在同步周期内,不存在其他线程的竞争(多个线程交替获取锁),而当线程在获取锁的过程中,(根据标志位判断是否是轻量级锁)就不必从用户态切换到内核态,从而提升程序性能。
3.锁自旋:轻量级锁失败,虚拟机为了避免线程真实的在操作系统层面挂起,会让其进行适应性自旋。如果自旋后,获取锁,则进入 _owner 区域,如果不能则膨胀为重量级锁
	适应性自旋:自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
		思想:这是基于大部分情况下,线程持有锁的时间都不会太长,虚拟机假设当前线程会在不久的将来可以获取锁(自旋会消耗CPu资源)
4.重量级锁:当线程开始获取锁时,判断锁的标识位,如果是重量级锁(10),则直接将当前线程在操作系统层面挂起,涉及状态改变,比较重。
5.锁粗化:也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁
6.锁消除:虚拟机在JIT编译(某段代码在第一次运行时,进行即时编译),通过对运行上下文扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

	*注:锁可以升级,但不能降级。目的是为了提高获得锁和释放锁的效率。

中断:interrupt(百度一下吧)

CAS(Compare And Swap)

CAS(Compare And Swap),即比较并交换。
是解决多线程并行情况下使用锁造成性能损耗的一种机制;
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回该位置的值。

CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

ABA(问题举例)

前提:有一个用单向链表实现的FIFO堆栈,栈顶为A,且有两个线程同时操作该链表。
	1.线程1先拿到链表,已经知道A.next为B,然后希望用CAS将栈顶替换为B,;
	2.在线程1执行上面这条指令之前,线程2 介入,将A、B出栈,再push D、C、A,此时A位于栈顶,B已经不在栈中;
	3.此时线程1执行CAS,发现栈顶仍为A,所以CAS成功,即将栈顶变成B;
	4.但实际上此时B与 当前栈中元素D、C没有关系,B.next为null,这样一来就直接把C、D丢掉了。 
解决:
	1.对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号;
	2.每次改变时加1,即A —> B —> A,变成A(1) —> B(2) —> A(3)解决:
	3.增加版本号,每次使用的时候版本号+1,每次变量更新的时候版本号+1。
	扩展:java提供AtomicStampzedReference,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。

AQS(AbstractQueuedSynchronizer):

fifo队列 + 原子int(表示状态)+ 同步器:用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架

1.FIFO队列:是一个双向列表
2.原子int:用来表示同步状态,为了满足在高并发的情况下,原生的整形Integer数值自增线程不安全的问题,使用AtomicInteger这个类代替;
	比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)
	getState:获取当前同步状态
	setState:设置当前同步状态
	compareAndSetState:使用CAS设置当前同步状态,该方法可以保证状态设置的原子性
3.同步器:包含(FIFO队列)两个节点,一个节点指向队列头节点,一个指向队列尾节点
	同步队列遵循FIFO,首节点是获取同步状态成功的节点
	首节点的线程在释放锁时,将会唤醒后继节点。而后继节点将会在获取锁成功时将自己设置为首节点(同步器首节点指针随之改变)
	未获取到锁的线程,会创建节点线程,安全(compareAndSetTail)的加入队列尾部(同步器尾节点指针随之改变)

volatile如何保证线程间的可见性?(volatile相对synchronized是一种轻量级同步策略)

线程需要修改一个数据时,实际步骤如下(整体是非原子的):
	1、将主存中的数据加载到(线程)缓存中
	2、CPU(线程)对缓存中的数据进行修改
	3、(JVM通知CPU)将修改后的值刷新到主存中
volatile的作用:
	禁止指令重排(加了这关键字,JVM(编译器)就不能进行指令重排序,无法优化代码执行)
	CPU(线程)缓存中修改的数据,会被立即刷新到主存中,其他线程缓存中的数据会被置为无效	
	*注:
		1.volatile无论是修饰实例变量还是静态变量,都需要放在数据类型关键字之前,即放在String、int等之前。
		2.volatile和final不能同时修饰一个变量,volatile 是保证变量被写时其结果其他线程可见,而final已经让该变量不能被再次写了
缺点:
	可能导致频繁刷新主存(性能瓶颈)
	volatile不具备互斥性
	volatile不能保证变量的原子性(尤其是复合操作,如num++)
(补充):编译器优化:
	在线程内,当读取到一个变量的时候,为了提高读写(存取)的速度,编译器在优化的时候,
		1.会先把变量读取到一个寄存器(对应上图子线程自己的内存或者是main线程自己的内存)中;以后在取这个变量的时候,就直接从寄存器中获取了;
		2.当变量的值在本线程里面改变的时候,会同时把变量的新值同步到该寄存器中,以便保持一致;
		3.同时JVM就会向处理器发送一条指令,将这个变量所在的寄存器的值回写到系统内存(对应上图中的主内存)中。
	(补充)造成数据不一致的原因:
		当前线程变量因为别的其他线程操作而改变了值,当前线程寄存器的值不会相应的改变,从而造成应用程序读取的值和实际的变量值不一致;

	(补充)CPU高速缓存模型
		CPU的运算速度要比内存的读写速度快很多,这就造成了内存无法跟上CPU的情况,由此出现了CPU缓存。其是CPU与内存之间的临时数据交换器,我们常见的CPU会有3级缓存,常称为L1、L2、L3

		-------------------------------------			-------------
		|CPU-0								|			|			|
		|		变量 						|			|			|
		|		 |							|			|			|
		|		 |							|			|			|
		|	------------	------------	|			|			|
		|	| L1-Cache |	| L1-Cache |	|			|			|
		|	------------  	------------	|			|			|
		|		 ||				||			|			|			|
		|		 /				/			|			|			|
		|	----------------------			|			|			|	
		|	|  L2-统一的高速缓存   |-------------------->>|			|				      	
		|	----------------------			|			|			|			-------------
		|									|			|	L 3		|			|			|
		-------------------------------------			|	统一 	|			|	  主	|
						...								|	高速 	|--------->>|	  存	|
						...								|	缓存 	|			|			|
		-------------------------------------			|			|			|			|
		|CPU-N								|			|	/ 		|			-------------
		|		变量 						|			|	所有		|
		|		 |							|			|	CPU		|
		|		 |							|			|	共享		|
		|	------------	------------	|			|	 /		|
		|	| L1-Cache |	| L1-Cache |	|			|			|
		|	------------  	------------	|			|			|
		|		 ||				||			|			|			|
		|		 /				/			|			|			|
		|	----------------------			|			|			|
		|	|  L2-统一的高速缓存   |-------------------->>|			|
		|	----------------------			|			|			|
		|									|			|			|
		-------------------------------------			-------------

	(补充)缓存一致性协议(MESI),它的方法是在CPU缓存中保存一个标记位。
		这个标记位有四种状态:
			M: Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中数据已经不一致了;
			E: Exclusive,独占缓存,当前CPU的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据;
			S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段;
			I: Invalid,无效缓存,这个说明CPU中的缓存已经不能使用了
		CPU的读取遵循下面几点:
			如果缓存状态是I,那么就从内存中读取,否则就从缓存中直接读取。
			如果缓存处于M或E的CPU读取到其他CPU有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为S。
			只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为M
	可见性原理:
		首先被volatile关键字修饰的共享变量在转换成汇编语言时,会加上一个以lock为前缀的指令
		当CPU发现这个指令时,立即做两件事: 
			1.将当前内核高速缓存行的数据立刻回写到内存
			2.通过MESI协议使在其他内核里缓存了的数据无效
		这两个操作是非常非常快的,这样其他线程也必须从内存中重新读取数据了。(*注:不久的将来也许就不算快了)
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号