Java多线程(六)

CopyOnWriteArrayList用于哪些场景,优缺点

CopyOnWriteArrayList是一个并发容器,是线程安全的。
优点:
当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException,在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而原数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

使用场景:
适合读多写少的场景

缺点:
1.写操作需要拷贝数组,会消耗内存,如果原数组的内容较多的情况,可能导致young gc或者full gc
2.不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList能做到最终一致性,但还是无法满足实时性要求
3.如果CopyOnWriteArrayList中的数据过多,每次add/set都要重新复制数组,非常耗费CPU资源。

设计思想:
1.读写分离
2.最终一致性
3.使用另外开辟空间,解决并发冲突

并发容器ThreadLocal
ThreadLocal是什么,使用场景是什么
ThreadLocal是一个本地线程副本变量工具类,在每个线程中都创建了ThreadLocalMap对象,ThreadLocal是一种以空间换时间的做法,每个线程可以访问自己内部ThreadLocalMap对象内的value,通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。
但在管理环境下(如web服务器)使用线程局部变量时需要特别小心,这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,就可能存在内存泄漏的风险。

经典的使用场景是为每个线程分配一个JDBC连接Connection。保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关闭了B线程正在使用的Connection,还有Session管理等问题T。

ThreadLocal使用范例:

public class TestThreadLocal{
	//线程本地存储变量
	private static final ThreadLocal<Integer> THREAD_LOCAL_NUM
	=new ThreadLocal<Integer>(){
		@override
		protected Integer initialValue(){
			return 0;
		}
	};
	public static void main(String[] args){
		for(int i=0;i<3;i++){
			Thread t= new Thread(){
				@override
				public void run(){
					add10ByThreadLocal();
				}
			};
			t.start();
		}

	}
	private static void add10ByThreadLocal(){
		for(int i=0;i<5;i++){
			Integer n=THREAD_LOCAL_NUM.get();
			n+=1;
			THREAD_LOCAL_NUM.set(n);
			S.o.p(Thread.currentThread().getName()+": ThreadLoal num="+n);
		}
	}
}

什么是线程局部变量
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。

ThreadLocal造成内存泄漏的原因与解决方案
ThreadLocalMap中使用的key是ThreadLocal的弱引用,而value是强引用。如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样,ThreadLocalMap中就会出现key为null的Entry。如果不做任何措施,value就永远无法被GC回收,这个时候就可能产生内存泄漏。ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法时,会清理掉key为null的记录。使用完ThreadLocal方法后,需要手动调用remove()方法。

ThreadLocal内存泄漏解决方法
1.每次使用完ThreadLocal,都调用它的remove()方法,清除数据
2.在使用线程池的情况下,不及时清理ThreadLocal,不仅是内存泄漏问题,更严重的是可能导致业务逻辑出现问题。使用ThreadLocal用完需要清理。

并发容器之BlockingQueue
什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列

两个附加操作是:在队列为空时,获取元素的线程会等待队列变为非空(消费者从队列获取元素),当队列满时,存储元素的线程会等待队列可用(生产者会等待队列中的元素被使用)。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者只能从容器中拿元素。

JDK7提供了7个阻塞队列。分别是:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列

LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列

PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列

DelayQueue:一个使用优先级队列实现的无界阻塞队列

SynchronousQueue:一个不存储元素的阻塞队列

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列

LinkedBlockingQueue:一个由链表结构组成的双向阻塞队列

JDK5之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要技术是用好wait,notify,notifyAll,synchronized这些关键字。JDK5之后,可以使用阻塞队列来实现,此方法大大减少了代码量,使得多线程编程更加容易,安全方面也有保障。

BlockingQueue接口是Queue的子接口,它的用途不是作为容器,而是作为线程同步的工具,因此,它具有很明显的特性:
当生产者线程试图向BlockingQueue队列中放入元素时,如果队列已满,那么线程会被阻塞,当消费者线程试图从队列中获取元素时,如果队列为空,则线程会被阻塞,由于这种特性,在程序中多个线程交替向BlockingQueue中放入元素,取出元素,它可以很好地控制线程之间的通信。

阻塞队列使用最经典的场景是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,解析线程不断从队列取出数据进行解析。

线程池
Executors类创建四种常见线程池
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。JVM试图跟踪每个对象,以便能够在对象销毁后进行垃圾回收。所以,提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗费资源的对象的创建和销毁,这就是"池化资源"产生的原因。

线程池是事先创建若干个可执行的线程放入一个池中,需要的时候从池中获取线程不用自行创建,使用完毕后不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

Executor接口定义了一个执行线程的工具。它的子类型即线程池接口是ExecutorService。工具类Executors提供了一些静态方法,生成一些常用的线程池。
1.newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程工作,相当于单线程串行执行所有任务。如果此线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2.newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大,线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用newFixedThreadPool方法创建线程池,可以获得更好的性能。

3.newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程(60s不执行任务),当任务数增加时,此线程池又可以智能地添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或JVM)能够创建的最大线程大小。

4.newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

线程池的优点:
1.降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
2.提高响应速度:可有效地控制最大并发线程数,提高系统资源的利用,避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需 要等到线程创建就可以立即执行。
3.提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
4.附加功能:提供定时执行、定期执行、单线程、并发数控制功能等。

线程池的状态
1.RUNNING:最正常的状态,接受新的任务,处理等待队列中的任务。
2.SHUTDOWN:不接受新任务的提交,但是会继续处理等待队列中的任务。
3.STOP:不接受新的任务的提交,不再处理等待队列中的任务,中断正在执行任务的线程。
4.TIDYING:所有任务都销毁,workCount为0,线程池的状态在转换为TIDYING状态时,会执行构造方法terminated()。
5.TERMINATED:terminated()方法结束后,线程池的状态就会变成此状态。

Executor框架
Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架
每次执行任务创建线程new Thread()比较消耗性能,所以创建一个线程池是个更好的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以很方便地创建一个线程池。

Executor和Executors的区别
Executors工具类的不同方法按照需求创建了不同的线程池。
Executor接口对象能执行线程任务
ExecutorService接口继承了Executor接口并进行了扩展,提供了更多的方法,可以获得任务执行的状态并且可以获取任务的返回值。
使用ThreadPoolExecutor可以创建自定义线程池
Future表示异步计算的结果,提供了检查计算是否完成的方法,以等待计算的完成,并可以使用get()方法获取计算的结果

线程池中submit()和execute()方法的区别
接收参数:execute()只能执行Runnable类型的任务。submit()可以执行Runnable和Callable类型的任务
返回值:submit()方法可以返回持有计算结果的Future对象,而execute()没有
异常处理:submit()方便Exception处理

线程池ThreadPoolExecutor
Executors和ThreadPoolExecutor创建线程池的区别

Executors方法的弊端
1.newFixedThreadPool和newSingleThreadExecutor
堆积的请求处理队列可能会消耗非常大的内存,甚至可能内存溢出
2.newCachedThreadPool和newScheduledThreadPool
线程数最大是Integer.MAX_VALUE,可能创建非常多的线程,引起内存溢出

ThreadPoolExecutor创建线程池方式只有一种,就是构造函数,参数自己设定。

如何创建线程池
ThreadPoolExecutor构造函数的3个重要参数
1.corePoolSize:核心线程数,线程数定义了最小可以同时运行的线程数量
2.maximumPoolSize:线程池中允许存在的工作线程的最大数量
3.workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到,任务会存放在队列中。

其它参数
1.keepAliveTime:线程池中的线程数量大于corePoolSize时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待的时间超过keepAliveTime才会被回收销毁。
2.unit:keepAliveTime参数的时间单位
3.threadFactory:为线程池提供创建新线程的线程工厂
4.handler:线程池任务队列超过maxinumPoolSize之后的拒绝策略

ThreadPoolExecutor跑和策略
定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了时
1.ThreadPoolExecutor.AbortPolicy:抛出rejectedExecutionException来拒绝新任务的处理
2.ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。不会任务请求,但这种策略会降低对于新任务提交速度,影响程序的整体性能。此外,这个策略喜欢增加队列的容量。如果应用程序可以承受此延迟并且不能丢弃任何一个任务请求的话,可以选择此策略,此策略提供了可伸缩队列。
3.ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃
4.ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理的任务请求

线程池Runnable+ThreadPoolExecutor
线程池实现原理:

提交任务
|
核心线程池是否满–Y--等待队列是否满—Y–线程池是否满—Y---策略
|N |N |N
创建线程 加入队列 创建线程

ex:
创建一个Runnable接口实现类

import util.Date;
public class MyRunnable implement Runnable{
	private String command;
	public MyRunnable(String s){
		this.command=s;
	}
	@override
	public void run(){
		S.o.p(Thread.currentThread().getName()+"StartTime="+new Date());
		processCommand();
		S.o.p(Thread.currentThread().getName()+"EndTime="+new Date());
	}
	private void processCommand(){
		try{
			Thread.sleep(5000);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
	@Override
	public String toString(){
		return this.command;
	}
}
import util.concurrent.ArrayBlockingQueue
import util.concurrent.ThreadPoolExecutor
import util.concurrent.TimeUnit
public class ThreadPoolExecutorDemo{
	private static final int CORE_POOL_SIZE=5;
	private static final int MAX_POOL_SIZE=10;
	private static final int QUEUE_CAPACITY=100;
	private static final long KEEP_ALIVE_TIME=1L;
	public static void main(String[] args){
		ThreadPoolExecutor executor=new ThreadPoolExecutor(
			CORE_POOL_SIZE,
			MAX_POOL_SIZE,
			KEEP_ALIVE_TIME,
			TimeUnit.SECONDS,
			new ArrayBlockingQueue<>(QUEUE_CAPACITY),
			new ThreadPoolExecutor.CallerRunsPolicy());
		
		for(int i=0;i<10;i++){
			//创建任务
			Runnable worker=new MyRunnable(""+i);
			//执行Runnable类型的任务,调用run()方法
			executor.execute(worker);
		}		
	}
	executor.shutdown();
	while(!executor.isTerminated()){
	}
	S.o.p("ok");
}	

corePoolSize:核心线程数为5
maximumPoolSize:最大线程数10
keepAliveTime:等待时间为1L
unit:等待时间单位TimeUnit.SECONDS
workQueue:任务队列为ArrayBlockingQueue,容量为100
handler:饱和策略为CallerRunsPolicy

FutureTask
原子操作类
什么是原子操作?有哪些原子类

原子操作(atomic operation)指"不可被中断的一个或一系列操作",一个事件要么都完成,要么都不完成。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。可以通过锁和循环CAS的方式来实现原子操作。CAS操作Compare&Set,或者Compare&Swap,几乎所有的CPU指令都支持CAS的原子操作

原子操作是不受其它操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

i++;并不是一个原子操作,当一个线程读取它的值并+1时,另外一个线程有可能会读到之前的值。

线程A读到i;i随后+1.线程B读到i+1
JDK1.5之前可以使用同步技术来使它变成原子操作,JDK1.5以后java.util.concurrent.atomic包提供了int和long类型的原子包装类,它们可以自动地保证对于它们的操作是原子的并且不需要使用同步。

java.util.concurrent包里面提供了一组原子类。其基本特性是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其它线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入。

原子类:
AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
原子数组:
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
原子属性更新器:
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决ABA问题的原子类
AtomicMarkableReference(通过引入一个boolean来反应中间是否变过)
AtomicStampedReference(通过引入一个int来累加反应中间有没有变过)

atomic原理
Atomic包中的类基本的特性是在多线程环境下,当有多个线程同时对单个变量(包括基本变量类型,和引用变量类型)进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能够成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。

AtomicInteger类部分源码

//更新操作时提供"比较并替换"的作用
private static final Unsafe unsafe= Unsafe.getUnsafe();
private static final long valueOffset;
static{
	try{
		//获取"原来的值"的内存地址
		valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclareField("value"));
	}
	catch(Exception ex){
		throw new Error(ex);
	}

}
private volatile int value;

AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

Unsafe类的objectFieldOffset()方法是一个本地方法,这个方法是用来获取"原来的值"的内存地址,返回值是valueOffset。value是一个volatile变量,在内存中可见,因此,JVM可以保证在任何时刻任何线程都能拿到该变量的最新值。

并发工具CountDownLatch和CyclicBarrier的区别
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,可以理解成维护的就是一个计数器,但两者还是各有不同的侧重点:

CountDownLatch一般用于某个线程A等待若干个其它线程执行完任务后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调的是一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再同时运行。

调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法时,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才会继续往下执行。

CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如可以通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能。

CountDownLatch是不能复用的,而CyclicBarrier是可以复用的

并发工具之Semaphore与Exchanger
Semaphore的作用
Semaphore是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只能有n个线程可以访问,如果超出n,那么请等待,等到某个线程执行完毕这段代码块后,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入int型整数n=1,相当于变成了一个synchronized。

Semaphore(信号量)允许多个线程同时访问:
synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore可以指定多个线程同时访问某个资源。

线程间交换数据的工具Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法,它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程可以交换数据。

常用的并发工具类
Semaphore(信号量)
允许多个线程同时访问
synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源

CountDownLatch(倒计时器)
CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行

CyclicBarrier(循环栅栏)
CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的计数等待,但是它的功能比CountDownLatch更复杂和强大。主要应用场景和CountDownLatch类似。CyclicBarrier的字面意思是可循环使用的屏障,它的功能是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier的默认构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier已经到达屏障,然后当前线程被阻塞。

弱引用和强引用的区别
使用的大部分引用都是强引用
例如:
Object obj=new Object();
String str=“StrongReference”;
如果一个对象具有强引用,那就类似于比不可少的物品,不会被垃圾回收器回收。当内存空间不足,JVM宁可抛出OutOfMemoryError错误,使程序异常终止,也不会回收这种对象。

弱引用:
弱引用是非必需对象,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
弱引用ThreadLocal
当ThreadLocal用完后,将其置为null,这时ThreadLocal对象并不能被回收,他还有ThreadLocalMap->Entry->key的引用,直到线程被销毁,但是这个线程可能会被放到线程池中不被销毁,这就产生了内存泄漏,ThreadLocal就为弱引用,会被回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>