一.多线程
1.概念
1.1并发:指两个或多个事件在同一时间段发生
并行:指两个或多个事件在同一时刻发生(同时发生)
1.2进程:加载进内存的应用程序,是OS资源分配的最小单元
线程:CPU调度的最小单元
线程调度:分时调度 抢占式调度(java)
2.创建线程的方式
2.1继承Thread
Thread thread1 = new Thread(() -> System.out.println("新线程01 = " + Thread.currentThread().getName()));
thread1.start();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
主线程 = main
新线程01 = Thread-0
2.2实现Runnable
Runnable run = () -> System.out.println("新线程02 = "+Thread.currentThread().getName());
run.run();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
新线程02 = main
主线程 = main
Runnable run = () -> System.out.println("新线程02 = "+Thread.currentThread().getName());
Thread thread2 = new Thread(run);
thread2.start();
System.out.println("主线程 = " + Thread.currentThread().getName());
----
主线程 = main
新线程02 = Thread-0
start():表示启动线程,使其成为一条单独的执行流,系统会分配线程相关资源,会有单独的程序计数器和 栈。(不能多次调用,否则发生llegalThreadStateException)
run():只是普通方法,不会生成新线程
2.3Callable配合FutureTask
继承关系:
1.public interface Future<V>
2.public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
3.public class FutureTask<V> implements RunnableFuture<V>{}
所以FutureTask就是Runnable的子类
Callable<String> callable = () -> {
System.out.println("新线程 = "+Thread.currentThread().getName());
return "我是新线程";
};
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
System.out.println(task.get());
----
新线程 = Thread-0
我是新线程
3.常用用方法
3.1 getState()获取线程状态
New Runnable Blocked Waiting Timed-waiting Terminated
3.2 interrupt方法可以用来请求终止线程
Thread.currentThread().isInterrupted() 获取中断状态
interrupted()是一个静态方法,检测当前的线程是否被中断,而且会清除该线程的中断状态,设置中断状态为false
isInterrupted()是一个实例方法,不会改变线程的中断状态
3.3 sleep方法 静态方法
会让当前线程状态从Runnable进入TimedWaiting状态
使用interrpt()方法打断正在sleep的线程,会抛出InterruptedException
3.4 yield方法
导致当前执行线程处于让步状态,具体的实现还是依赖任务调度器
3.5 join方法
等待指定线程执行结束(在哪个线程执行就需要哪个线程等待)
3.6 public final void setPriority(int newPriorit)
线程优先级从1到10,默认是5,
3.7 setDaemon()
标识线程为守护线程,唯一用途就是为其他线程提供服务【计时器的声音信号线程】
wait()和sleep()的区别:
1.wait是Object方法 sleep是Thread方法
2.sleep不需要synchronized配合使用
3.都会释放CPU使用权 ,但sleep不会释放锁、wait会
二. 线程安全
临界区:一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区
原子性 - 有序性- 可见性
解决线程安全问题:阻塞式- Synchronized/显式锁
非阻塞式-原子变量
解决内存可见性:volatile/Sychronized/显式锁
public class CountAddDemo {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(" counter = " + counter);
}
}
----
counter = 14516
public class CountAddDemo {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (CountAddDemo.class) {
for (int i = 0; i < 50000; i++) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (CountAddDemo.class) {
for (int i = 0; i < 50000; i++) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(" counter = " + counter);
}
}
----
counter = 0
2.1Sychronized 悲观锁
2.1.1修饰实例方法
多个线程是可以同时访问同一个sychronized实例方法,只要他们访问的对象时不同即可。
sychronized实例方法保护的是当前实例对象this
2.1.2修饰静态方法
保护的时类对象XX.class(每个对象都有一个锁和一个等待队列)
2.1.3代码块
可以使用单独对象作为锁的对象
2.1.4可重入性
Synchronized是可重入的,对同一个线程获得锁之后,在调用其他需要同样锁的代码时,可以直接调用。
可重入是通过记录锁的持有线程和持有数量来实现的。
2.1.5原理
synchronized(obj)
obj 指向操作系统的Monitor对象
[1.WaitSet等待队列-获得锁但是条件不满足 Waiting - 调用notify/notifyAll时唤醒
- EntryList阻塞队列-Blocked-在线程释放锁时唤醒
- Owner所有者]
实现原理[jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。
是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。]
使用synchronized关键字获取锁的过程不响应中断请求(局限性)
2.2线程协作机制 wait/notify
除了锁的等待队列,每个对象还有一个条件等待队列,用于线程间的协作。
调用wait()会把当前线程放到条件队列上并阻塞,需要一个条件才能继续执行,需要其他线程改变,当其他线程改变了条件之后,应该调用notify/notifyAll方法,notify方法就是从条件队列中选一个线程将其从队列中移除并唤醒
wait()-notify()/notifyAll()
-都是Object类的final方法
-只能在同步方法/代码块中调用
-如果调用方法前当前线程没有持有锁,会抛出 java.lang.IllegalMontorStateException
虚假唤醒(while(条件变量))
2.3LockSupport:
park():[使当前的线程放弃CPU并进入等待状态]
unpart(Thread thread):[使参数使用的线程恢复可运行状态]
注:暂停恢复具体线程 与执行顺序无关
三.原子变量/显示锁/显示条件
3.1原子变量与CAS
所有以原子方式实现组合操作的方法都依赖compareAndSet(int expect, int update)
--如果当前的值为expect,则更新为update,否则不更新,如果更新成功返回true,否则返回false
Unsafe
一般的计算机系统都在硬件层次上支持CAS指令,而java的实现会利用这些特殊指令
CAS更新存在ABA问题,可以用AtomicStampedReference解决,附带时间戳
3.2显式锁( 依赖于CAS和LockSupport )
Lock->ReentrantLock
ReadWriteLock->ReentrantReadWriteLock
相比sychronized,都支持可重入( 可重入性-当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的。 )
{ 显示锁:
a.支持以非阻塞的方式获取锁
b.可以响应中断(防止无线等待) rt.lockInterruptibly()
c.可以限时
d.可以设置公平锁
e.可以支持多个条件变量
Synchronized不用用户手动释放锁,Lock必须手动释放
}
Lock底层实现基于AQS(Abstract Queued Sychchronizer)(用于实现依赖先进先出FIFO等待队列阻塞锁和相关同步器)
lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
lock释放锁的过程:修改状态值,调整等待链表。
可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。
因此在非必要的情况下,建议使用synchronized做同步操作。
3.3显示条件
条件变量 Condition
newCondition()返回一个与锁相关的条件对象
await() 将该线程放到条件的等待集中
signalAll()解除该条件的等待集中的所有线程的阻塞状态
signal()从该条件的等待集合随机选择一个线程解除阻塞状态
3.4公平锁与非公平锁
synchronized内部锁是非公平的。
ReentrantLock提供构造方法可以设置为公平锁(内部维护一个有序队列)
-非公平锁系统倾向于让一个线程再次获得已经持有的锁,这种分配高效非公平
-公平锁多个线程不会发生同一个线程连续多次获得锁的可能,保证公平性
四.并发容器
4.1写时复制容器 CopyOnWriteArrayList CopyOnWriteArraySet
4.1.1 CopyOnWriteArrayList
-线程安全,可以被多个线程并发访问
-它的迭代器不支持修改操作,也不会抛出ConcurrentModificationException
-它以原子方式支持一些复合操作
读不需要锁,可以并行,读写可以并行,不能多个线程同时写。
不适合数组较大且修改操作频繁的操作。(缓存)
保证线程安全的思路:锁(sychronized和ReentrantLock) 循环CAS,都是控制对同一资源的访问冲突
写时复制通过复制资源减少冲突。
4.1.2 CopyOnWriteArraySet 基于CopyOnWriteArrayList实现
4.2 CurrentHashMap
-并发安全
-直接支持一些原子复合操作
-支持高并发,读写完全并行,写操作支持一定程度并行
(java7 分段锁 读不需要锁 数组+链表
java8 数组+红黑树 提高查找效率 细化锁粒度)
-与同步容器Collections.synchronizedMap相比,迭代不用加锁,不会抛出Concurre ntModificationException
-弱一致性(ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历元素,在遍历过程中如果内部元素发生变化,变化如果在已遍历部分迭代器不回反映出来,如果变化在未遍历部分则会反映处理)
java无并发版的hashset。
并发包中可排序的版本不是基于树,而是基于Skip List 跳表
4.3ConcurrentSkipListMap ConcurrentSkipListSet
4.3.1ConcurrentSkipListMap
-没有使用锁,所有操作都是无阻塞的,所有操作可以并行
-与ConcurrentHashMap类似,迭代器不会抛出ConcurrentModificationException,是弱一致的
-与ConcurrentHashMap类似,同样实现了ConcurrentMap接口,支持一些原子复合操作
-与TreeMap一样,可排序,默认按键的自然顺序,也可以传递比较器自定义排序,实现了SortedMap和NavigableMap接口
跳表是基于链表的,在链表的基础上家乐多层索引结构
4.4并发队列
无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
--基于链表,大小无限制且无界(内部使用循环CAS实现)
普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque
--ArrayBlockingQueue基于数组实现,有界需要指定大小
--LinkedBlockingQueue/LinkedBlockingDeque 基于链表实现(内部使用显式锁和显式条件实现)
优先级阻塞队列:PriorityBlockingQueue
延迟阻塞队列:DelayQueue
其他阻塞队列:SynchronousQueue和LinkedTransferQueue
--SynchronousQueue,无存储元素空间。适用于两个线程间直接传递消息。
五.同步协作工具
5.1读写锁 ReentrantReadWriteLock
synchonized内部锁和ReentrantLock都是独占锁(排它锁),同一时间只允许一个线程执行同步代码,可以保证线程安全性,但是执行效率低
ReentrantReadWriteLock读写锁是一种改进的排它锁,也称为共享锁。允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。
读锁可以同时被多个线程持有。线程在修改共享数据前必须持有写锁,写锁时排他的,
读写锁允许读读共享,读写互斥,写写互斥
readLock和writeLock 方法返回的锁对象时同一个锁的两个不同角色,不是两个不同的锁
5.2信号量Semaphore 可以限制对资源的并发访问 基于AQS实现
一般锁只能由持有锁的线程释放,而Semaphore表示的只是一个许可数,任意线程都可以调用其release方法。
一般锁实现类是可重入的,而Semaphore不是,每一次acquire都会消耗一个许可。
5.3倒计时门栓CountDownLatch
希望通过该门的线程都需要等待,倒计时变为0时,门栓打开,等待所有线程通过
--是一次性的,打开后无法再关闭。
同时开始,主线程依赖子线程结果
5.4循环栅栏CyclicBarrier
所有线程在到达该栅栏都需要等待其他线程,等所有线程到达后再一起通过,是循环的可做重复同步。
--适用于并行迭代计算。
CountDownLatch参与线程是不同角色,用于不用角色线程的同步。
CyclicBarrier参与线程角色是一样的。
CountDownLatch一次性的
CyclicBarrier可重复利用
5.5Exchanger 用于两个线程间交换信息。
仅可用作两个线程的信息交换。当超过两个线程调用同一个exchanger对象时,得到的结果是随机的,而剩下的未得到配对的
5.6ThreadLocal 线程本地变量
调用initialValue获取初始值
-日期处理
-随机数
每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键值读写当前线程的Map
六.异步任务
6.1解释
Runnable/Callable:表示要执行的异步任务。
Executor/ExecutorService 表示执行服务
Furure:表示异步任务的结果
Runnable没有返回结果,而Callable有,Runnable不会抛出异常,而Callable会
6.2线程池
--重用线程,避免线程创建的开销
-任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。
ThreadPoolExecutor->AbstractExecutorService->ExecutorService
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduleAtFixedRate -- 按照特定频率执行任务,如果任务时间大于间隔时间,则在任务执行完毕后执行下次任务
scheduleWithFixedDelay -- 在任务执行完毕间隔x后再次执行
6.2.1底层实现
ThreadPoolExecutor
public ThreadPoolExecutor(
int corePoolSize, -- 核心线程数量
int maximumPoolSize, -- 最大线程数量
long keepAliveTime, -- 当线程池数量超过corePoolSize时,多余的空闲线程的存活时长
TimeUnit unit, -- keepAliveTime时长单位
BlockingQueue workQueue, --任务队列,把任务提交到该队列等待执行
ThreadFactory threadFactory, -- 线程工厂,用于创建线程
RejectedExecutionHandler handler) --拒绝策略,当任务太多来不及处理时如何拒绝
6.2.2BlockingQueue队列功能分类
1.直接提交队列 -- SynchronousQueue,无容量,提交的线程池任务不会真是保存,总是将新的任务提交给线程池,如果线程达到MAXNUM则执行拒绝策略(newCachedThreadPool)
2.有界任务队列 -- ArrayBlockingQueue,可以指定一个容量。
如果线程池中线程数小于corePoolSize 创建新线程
如果大于corePoolSize ,则加入等待队列
如果队列已满且小于maximumPoolSize 继续创建
如果队列已满且大于maximumPoolSize 则执行拒绝策略
3.无界任务队列 -- LinkBlockingQueue 不存在任务入队失败的情况,除非系统资源耗尽(newFixedThreadPool newSingleThreadExecutor)
4.优先任务队列 -- PriorityBlockingQueue 带有任务优先级的队列,是一个特殊的无界队列
6.2.3
newCachedThreadPool() -- 每次提交都会创建线程执行。适合耗时较短,提交频繁的任务
newFixedThreadPool(int nThreads) --
newSingleThreadExecutor() --
newScheduledThreadPool()
6.2.4拒绝策略 - 当提交给线程池的任务量超过实际承载能力时,通过拒绝策略处理
四种策略:
(1)CallerRunsPolicy implements RejectedExecutionHandler
--只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务
(2)AbortPolicy implements RejectedExecutionHandler (默认策略)
---抛出异常
(3)DiscardPolicy implements RejectedExecutionHandler
--直接丢弃无法处理的任务
(4)DiscardOldestPolicy implements RejectedExecutionHandler
--将队列当中最老的任务丢弃,尝试再次提交新任务
6.2.5
a.ThreadFactory 创建线程的方式
b.监控线程池
beforeExecure/afterExecure/terminated
c.线程池适合提交相互独立的任务,而不是彼此依赖的任务。
对于彼此依赖的任务可以考虑分别提交给不同的线程池来执行
d.提交任务的方法
submit() execute() --对比
submit提交任务如果抛出异常无提示直接吃掉异常
6.3 ForkjoinPool
(和ThreadPoolExecutor一样,继承AbstractExecutorService)
ForkJoinPool和ExcutorService相比一个典型特性就是Work stealing.
submit()
ForkJoinTask
子类:RecursiveAction(无返回值 ) RecursiveTask(有返回值)
-普通线程池共享一个工作队列,有空闲线程时工作队列中的任务才得到执行
-ForkJoinPool中每个线程都有自己的工作队列,每个工作线程运行中产生的新的任务,放在队尾
-某个工作线程会尝试窃取别个工作线程队列中的任务,从队列头窃取
-遇到join时,如果join任务尚未完成,则可先处理其他任务
6.4CompletionService
ExecutorService启动多个Callable时,每个Callable返回一个Future,而当我们执行Future的get方法获取结果时,可能拿到的Future并不是第一个执行完成的Callable的Future,就会进行阻塞,从而不能获取到第一个完成的Callable结果,那么这样就造成了很严重的性能损耗问题
CompletionService可以按照任务顺序获取结果
public interface CompletionService {
Future submit(Callable task);
Future submit(Runnable task, V result);
Future take() throws InterruptedException;
Future poll();
Future poll(long timeout, TimeUnit unit) throws InterruptedException;
}
submit方法和ExecutorService一样
take() poll()获取下一个完成任务的结果
take会阻塞等待 poll立即返回,如果没有已完成的任务,返回null
实现类是ExecutorCompletionService
CompletionService会根据线程池中Task的执行结果按执行完成的先后顺序排序,任务先完成的可优先获取到
6.5CompletableFuture
ExecutorService可以方便地提交单个独立的异步任务,可以方便地在需要的时候通过Future接口获取异步任务的结果,但是Future是通过阻塞或者轮询的方式得到任务的结果。
--CompletableFuture,它是一个具体的类,实现了两个接口,一个是Future,另一个是CompletionStage,Future表示异步任务的结果,而CompletionStage字面意思是完成阶段,多个CompletionStage可以以流水线的方式组合起来,对于其中一个CompletionStage,它有一个计算任务,但可能需要等待其他一个或多个阶段完成才能开始,它完成后,可能会触发其他阶段开始运行
--使用CompletableFuture可以实现类似功能,不过,它不支持使用Callable表示异步任务,而支持Runnable和Supplier,Supplier替代Callable表示有返回结果的异步任务,与Callale的区别是,它不能抛出受检异常,如果会发生异常,可以抛出运行时异常
使用Future,我们只能通过get获取结果,而get()可能会需要阻塞等待,而通过CompletionStage,可以注册回调函数,当任务完成或异常结束时自动触发执行