代码Demo: JavaBaseThreadLearn
2. Java线程
2.1. 线程基本操作
2.1.1. 创建一个线程并运行
- 🆕一般来说,新建一个Thread对象,调用start()来启动,会把run()方法里定义的代码放在新建的线程里运行,而直接调用run()则不会。
- start()方法的具体实现是JNI实现的,具体不表,大概就是新建线程然后跑run()里的东西。
2.1.2. 终止线程
- ❌不建议使用stop()方法,因为这会强制终止,进而导致代码在错误的位置被停下,致使数据不一致,这里建议使用实例方法interrupt()
2.1.3. 中断线程
- ✅实例方法interrupt()中断线程,其实就是设置中断标示位为true(默认为false)。
- 可以通过实例方法isInterrupted()来判断当前线程是否被中断。进而在自己的代码里采取适当的处理措施。
- 不过还有一个静态方法interrupted()来判断当前线程是否被中断,并清空中断标识,即,重新置为false。
2.1.4. wait()和notify()
-
这两个方法都是Object类自带的。
-
wait()方法会把调用它的线程停下来,放进自己的等待队列里去;而如果想让刚刚被停下的线程重新运行,只要调用同一个对象的notify()或notifyAll()方法即可。
-
关于notify()和notifyAll()的区别,就是前者随机唤醒,后者全部唤醒。
-
⚠️注意️,wait()并不能随便调用,应该在同步语句块里调用,因为线程调用某一对象的wait()时,会进入等待队列;但如果这个对象之前获得了监视器,进入等待队列就没法释放,就会造成没有其他对象可以获得这个监视器,进而无法把等待队列上的其他线程唤醒,导致其他线程的无法正常执行,所以wait()应该放在同步块里调用。此时wait()会自动释放监视器。
-
wait()时被中断会把线程从等待队列移出,让其参与锁的竞争,当得到了锁才会抛出中断异常。
-
👀放在同步块只要是为了解决唤醒位丢失问题,这个操作系统有说,或者看这个文章:🔗为什么wait()和notify()需要放在同步块?
-
🔔wait()会自动释放锁,而Thread.sleep()则不会。同时,调用wait()的线程在被唤醒后,会尝试再次获得锁以运行,而不是直接运行。
2.1.5. join()和yeild()
- join()如其名,就是让这个方法的实例线程插队到当前线程前面去执行,一般用于A线程依赖B线程的计算结果,然后A就可以调用B.join()让B比自己先执行。
- yeild()会让调用者线程释放自己的CPU时间,然后重新参与调度竞争。
2.2. volatile和JMM
- volatile关键词告诉JVM被它修饰的变量极有可能被其他线程修改,所以要让这个修改立即被其他线程看到,但是⚠️这并不意味着修改一定会被看到!!!具体看这里:🔗Java volatile关键词
2.3. 守护线程
- 守护线程是在主线程背后运行的线程,一般在主线程结束后,守护线程就会自然退出。
2.4. 线程优先级
- 🛠可以通过设置线程优先级来控制线程被调度的顺序,但这并不具备强制性,也是一个仅供参考的存在,不过大多数情况下高优先级的线程更有可能被执行。
2.5. synchronized关键词
- 🔒synchronized关键词可作用于三个场景,分别是:🔐指定对象解锁,🔐直接作用于实例方法,🔐作用于静态方法。
- ✅第一种加锁方式本质是获得加锁对象的监视器;✅第二种即把加锁对象换成了当前对象实例;✅第三种即把加锁对象换成了当前类对象。
3. JDK并发包
3.1. 同步控制
3.1.1. synchronized的替代品——重入锁
- ReentrantLock锁可以被一个线程多次加锁,但是解锁也必须释放同样数量的锁。
- ReentrantLock的lockInterruptibly()会在线程被中断时,取消对于锁的尝试获取,进而释放锁,这种形式一定程度上可以缓解死锁的发生。
- tryLock()方法会直接返回,如果获取锁成功,返回true,否则返回false。另外,它可以设置请求超时,如果在请求时间内没有得到锁,就会返回false,否则true。所以相比之下tryLock()就是这个方法的立即形式。
- 同时因为tryLock()是立即返回的,所以可以写成while(true) {} 形式;会在一定程度上缓解死锁。
3.1.2. Condition介绍
- 🆚Condition之于ReentrantLock就像Object.wait()和Object.notify()之于synchronized。
- 通过newCondition()方法,可以获取与当前重入锁绑定的Condition对象。
- await()方法可以被中断,此时会跳出等待,过程和Object.wait()类似。另一个方法,awaitUninterruptibly()会忽略中断。
- 🔓signal()后应该释放锁,手动释放。
- 其他API详见源码
3.1.3. Semaphore介绍
- Semaphore是信号量,它像重入锁的扩展,可以指定资源数量,进而允许多个线程访问临界区。构造方法指出了详细数量。
- 🔒acquire()会申请一个信号量,如果无法得到会一直阻塞,acquireUninterruptibly()会忽略申请过程中的中断。
- tryAcquire()类似tryLock()。
- 🔓release()类似unlock()。
3.1.4. ReadWriteLock介绍
- 对于一些系统(比如数据库),其读操作比写操作多太多,那么就可以实现读写分离。
- 读写锁的读操作虽然也要加锁,但是✅多个读操作之间并不阻塞,只有:🚫读-写;🚫写-读;🚫写-写之间才可能阻塞。
3.1.5. CountDownLatch介绍
- CountDownLatch有一个构造方法,传入一个计数个数;以及一个实例方法countDown(),它会把计数器-1。
- 通过实例方法await()可以阻塞当前线程直到计数器为0,而这,就是倒计时计数器——倒数到零开始运行。
public class Solution {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(() -> {
LockSupport.parkNanos(Duration.ofMillis(100).toNanos());
System.out.println("thread1 started");
countDownLatch.countDown();
}).start();
new Thread(() -> {
LockSupport.parkNanos(Duration.ofMillis(1000).toNanos());
System.out.println("thread2 started");
countDownLatch.countDown();
}).start();
new Thread(() -> {
LockSupport.parkNanos(Duration.ofMillis(2000).toNanos());
System.out.println("thread3 started");
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println("main thread run");
}
}
3.1.6. CyclicBarrier介绍
- 这是一个功能更加复杂,也更加强大的倒数计数器。它的构造方法包含两个参数;一个是计数器个数,一个是计数器为0时执行的指令。
- 🆚它和CountDownLatch有点不同,因为它在调用await()方法时会自动-1计数器,当某一个线程调用await()而导致计数器等于0时,会触发构造方法里指明的Runnable()运行。♻️同时会设置新一轮触发,也就是计数器归位。
public class Solution {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
new Thread(() -> {
System.out.println("thread1 wait");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("thread1 run");
}).start();
LockSupport.parkNanos(Duration.ofMillis(100).toNanos());
new Thread(() -> {
System.out.println("thread2 wait");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("thread2 run");
}).start();
LockSupport.parkNanos(Duration.ofMillis(100).toNanos());
new Thread(() -> {
System.out.println("thread3 wait");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("thread3 run");
}).start();
}
}
3.1.7. LockSupport介绍
- LockSupport可以直接阻塞线程而不会抛出异常,且不用获得锁,也解决了resume()方法的问题。
- ⛔️LockSupport.park()可以阻塞当前线程;
3.2. 线程复用——线程池
3.2.1. 线程池的意义
- 频繁地创建和销毁线程,对CPU来说是很大的负担,所以可以使用线程池,做到对线程的随用随取,用完归还。
3.2.2. JDK提供的线程池
-
JDK的Executor接口仅提供一个execute(Runnable)方法。
-
其中,Executors为ExecutorService提供了一系列的静态方法来进行构造。
3.2.3. 探其实现
- 探究源码可发现,Executors提供的方法本质是对ThreadPoolExecutor的封装。
- ThreadPoolExecutor构造器中最重要的参数莫过于workQueue和handler,至于线程工厂,这个比较简单,使用默认的即可(源码默认实现非常简单)。
- 一般来说,工作队列的提供可以使用以下几种:
- SynchronousQueue(没有容量,直接提交,提交的任务不会保存,如果此时刚好有一个任务执行完毕,那么使用这个已完成任务的线程执行被提交的任务,否则创建新的线程,此时要记得设置很大的maximumPoolSize)。
- ArrayBlockingQueue(有界任务队列,提交任务,如果实际线程数小于corePoolSize,则优先创建线程执行任务,否则加入等待任务队列,如果队列已满,在线程数小于maxPoolSize时创建线程,否则执行拒绝策略)。
- LinkedBlockingQueue(无界任务队列,提交任务,如果实际线程数小于corePoolSize,则优先创建线程,否则添加至等待任务队列)。
- PriorityBlockingQueue(优先任务队列,无界,且根据优先级策略对任务进行排序)。
3.2.4. 拒绝策略
- 当系统无法接受更多的任务时,就会触发拒绝策略。
- JDK提供了一些默认拒绝策略,可以查看源码了解。
3.2.5. 扩展ThreadPoolExecutor
- ThreadPoolExecutor的beforeExecutor()和afterExecutor()方法可以进行扩展,实现debug功能。
3.2.6. 获取调用堆栈
- execute()会在出错时打印堆栈,但是submit()不会,不过可以通过submit()的返回值获取。
3.2.7. Fork/Join
- fork()划分任务,放入两个线程执行,join()合并子线程执行结果,返回到上层。
- fork/join存在工作窃取,就是某个线程完成计算后,会尝试拿其他线程的任务队列的任务来计算,一般从队尾获取,而获取自己的则从队头,这样可以减少一部分竞争。
- 具体用法见源码
3.3. JDK并发容器
3.3.1. 并发集合简介
- ConcurrentHashMap: 线程安全的HashMap。
- CopyOnWriteArrayList: 在读多写少的场合,这个List性能很好。
- ConcurrentLinkedQueue: 线程安全的LinkedList
- BlockingQueue: 阻塞队列,适合做数据共享的通道。
- ConcurrentSkipListMap: 跳表,适合做快速查找。
- Collections的工具类。
3.3.2. 探索ConcurrentLinkedQueue
4. 锁优化
4.1. 提高锁性能的建议
- 减少锁的持有时间(不把锁无关代码放在同步块里)。
- 减小锁粒度(不要对整个程序加锁,尽可能把操作分散,让每个线程请求不同的部分,这样有几率实现真正的并发,比如ConcurrentHashMap会使用散列把请求打散开,默认16个分区,每次get(), put()会先计算散列值,再去相应的块请求,即可实现不同线程不同同步块,做到真正的并发)。不过,这种方式在请求全局信息时会降低性能,因为这种方式适用于全局性不高,可以做到分散请求的程序。
- 使用读写分离锁(在读写请求次数差距很大时,这种锁可以很大的提高性能)。
- 锁分离(如果两个操作互不干扰,那么可以使用分离锁,比如同步队列的take()和put()方法。可以做到take()与take()之间同步,put()和put()之间同步,而take()与put()之间不需要同步,这就需要两把锁来实现)。
- 锁粗化(如果两个操作相离很近,那么可以考虑放在同一个同步块中,因为锁的申请和释放也是一个耗费资源的事)。
4.2. JVM对锁的优化
4.2.1. 锁偏向
- JVM会把程序优化为: 如果一个线程获得了锁,那么会倾向于让它下次再次获得锁,这在竞争比较小的场合比较适用。
4.2.2. 轻量级锁
- 通过把对象头引用到锁来实现,如果失败会升级为重量级锁(也就是普通的加锁/解锁)。
4.2.3. 自旋锁
- 每次申请不到锁就阻塞线程,然后再次调度,成本未免还有点大,所以会考虑把线程自旋一会再尝试,这适用于锁竞争不激烈会锁使用时间短的场景。
4.2.4. 锁消除
- JVM会自动去处代码里写了但是实际没用的锁,判断依据是逃逸分析技术。所谓逃逸分析就是看某个变量是否会逃出某一个作用域。如果这个变量被另一个方法使用了,就说明它有可能被修改,就不能删去锁。
4.3. ThreadLocal
- 用来保存每个线程独有的变量,通过set(T value)和get()方法进行设置和获取。
- 维护了一个(x, y) -> z的映射,其中,x是当前线程,y是ThreadLocal实例,z是变量。
- 每次像ThreadLocal实例set(T val),都会把这个值放进当前线程保存的ThreadLocalMap实例里去,其中key是ThreadLocal实例,val为value。如果当前线程的ThreadLocalMap为空,就创建,再设置值。
- 每次get()都会先获取当前线程的ThreadLocalMap实例,是一个Map,然后以当前ThreadLocal实例为key获取value。
- 关于线程池的ThreadLocal保存的变量释放问题,可以调用remove()来移除,或者可以把实例指针置为null来加速GC过程,比如让实例threadLocal = null即可。原因在于ThreadLocalMap的Entry是弱引用,一旦为空,则立刻回收。
- 关于使用场景,在对线程某一变量竞争比较激烈的时候,ThreadLocal可以很好的减轻这个问题。
4.4. 无锁
4.4.1. CAS(Compare And Set)操作
- CAS在于在指令层面提供原子操作,进行"比较","替换"。一般来说CAS(offset, oldVal, newVal)需要三个参数: offset指出需要更新的值在内存中的位置,oldVal是这个内存位置期望的值,newVal是想要进行替换的值。如果内存位置的值=oldVal,说明未被别的线程更新,则使用newVal替换,如果这个内存位置的值和oldVal不一致,说明已经被别的线程更新了,那么不进行操作。
- CAS指令现已被大多数现代CPU支持,JDK使用CAS实现了一些原子类,以此来进行无锁操作。
- JDK的atomic包下提供了一系列方便的包装类型,可以查看相关类了解用法。
4.4.2. AtomicXxxx类
- 诸如AtomicInteger,AtomicDouble,AtomicLong之类的,都是对基本类型的封装。
4.4.3. AtomicReference
- 此类旨在包装自定义类型,来实现原子操作。但是它没法保证更新同一个值的情况,也就是无法做到记录对象的状态值。比如把A更新为B,过会又更新成了A,那这是无法记录的。
4.4.4. AtomicStampedReference
- 此类引入了时间戳进行状态记录,确保可以在更新对象时校检对象状态。
4.4.5. AtomicXxxArray
- 此类型对数组进行了原子包装,可以使得数组操作享受原子操作。
4.4.6. AtomicXxxFieldUpdater
- 此类型可以在几乎不改动原有代码的情况下,对域变量进行原子操作包装。
4.4.7. SynchronousQueue
- 它的实现有点像AQS的acquire()方法,可以参考源码理解。
来看一张图指出了Java的锁(图来自美团技术团队)
5. 并行模式和算法
5.1. 单例模式
- 整个系统只有一个类实例,因此在多线程中有着更好地应用,但是如何确保在并发环境中只会被创建一个实例则是重中之重。
5.2. 不变模式
- 确保对象的绝对不可变——既不能被外界改变,也不能被自己改变,强于"只读模式"。
5.3. 生产者-消费者模式
- 这个,就是,怎么说呢...很熟悉就是了,多个生产者线程负责向缓冲区(在这里是阻塞队列)写数据,如果缓冲区满了,则挂起,直到某一个消费者线程发现缓冲区数据为0,随即唤醒一个生产者线程;消费者反之亦然。
5.4. 高效的生产者-消费者模型
- Disruptor是一个高性能的无锁内存队列,用来实现生产者-消费者模型很合适。
Future回调
- 首先明确一件事,就是在Java中,Thread.start()之后调用Runnable.run()方法。这里有两个限制,一是调用的必须是Runnable,而是必须是run()方法。
- Java中的Callable的V call()方法是不能被Thread的start()调用的,所以有了Future接口+FutureTask类。Future就是一个普通的接口。指出可以对线程的异步操作。FutureTask把Runnable和Future结合在一起实现Callable的功能。
- 或者可以这么理解,FutureTask是为了模拟Callable可以被Thread.start()而存在的。最明显的证据就是FutureTask重写了Runnable的run():
public void run() {
// 无关紧要的部分...
try {
// callable是作为构造参数设置的
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 在Runnable.run()里面直接调用Callable.call()并获取返回值(这是阻塞的)
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
// 设置返回值
set(result);
}
} finally {
// 无关紧要的balabala...
}
}
- 至此我们就非常明确了,FutureTask = 一个Runnable实现类,这个类会在run()里调用call()并设置返回值到私有域里去,通过get()可以获取这个返回值。Callable和Future都是普通的接口,没有任何附加含义,Runnable是一个特殊的,可以被线程调度的接口。
- 因为Callable()无法直接被线程start(),需要借助FutureTask来实现,所以可以封装一下再交由ExecutorService来实现调度,如果你直接提交Callable的话,ExecutorService会自动封装成FutureTask然后执行。
- 顺带一提,为了规范化,ExecutorService会把提交的Runnable封装为没有返回值的FutureTask来执行(其实是RunnableFuture,为了好理解故意这么说的)。
- 还有一点,可以重写FutureTask#done()来实现任务完成时的回调调用。
5.6. 并行流水线
- 如果一个操作依赖另一个操作的完成,那就不能简单的开启多线程,但是如果又想实现多线程来增加性能(尤其在多核CPU上),可以实现流水线思想。
- A->B->C,这样的流水线执行,彼此顺序无法被打乱,那么可以让线程A执行任务A,让线程B执行任务B,线程C执行任务C。一开始B和C处于睡眠状态。在A执行完毕后,把执行结果发给B并唤醒B,B执行完毕进入睡眠并把结果发给C,唤醒C。
5.7. 并行搜索
- 对于一个数组的检索,如果数组本身是乱序的,则只能老老实实地从头到尾检索,但是可以利用多线程,让每个线程检索一小部分,以提高检索效率。
- 这里会用到Future来实现结果的保存,以及一个线程安全的全局变量来记录检索结果,用来当其他线程检索成功时可以直接返回,而不用继续检索。
5.10. NIO
- 详见Demo
Java NIO处理网络的核心组件只有四个:Channel,Selector,SelectionKey和java.nio.Buffer。
说一下ServerSocketChannel,SocketChannel,Selector和SelectionKey之间的关系。 ServerSocketChannel和SocketChannel不说了,无非就是一个用来在服务端建立连接,一个处理连接(实际I/O交互)的区别,在这里统称为AbstractSelectableChannel,也就是它俩都继承的类。
Selector.select()调用系统调用,轮询端口,记录已注册的AbstractSelectableChannel感兴趣的事件,如果发生了所有已注册的AbstractSelectableChannel感兴趣的事件之一的话,就返回。否则阻塞。 对于AbstractSelectableChannel来说,怎么让Selector帮自己记录并轮询自己感兴趣的事件呢?答案是:注册到Selector上即可,同时设置感兴趣的事件类型。
在注册成功后,会返回一个SelectionKey类型的变量,通过它,可以操作AbstractSelectableChannel和Selector。SelectionKey本身就是AbstractSelectableChannel和它注册到的Selector的凭证。 就像是订单一样,记录着它们俩的关系,所以在注册成功的后续操作里,一般都是用SelectionKey来实现的。同时,SelectionKey还有一个attachment()方法,可以获取附加到它上面的对象。 一般我们用这个附属对象来处理当前SelectionKey所包含的AbstractSelectableChannel和Selector的实际业务。
刚才说到了Selector.select(),它会一直阻塞直到发生了感兴趣的事件,但是有时候我们这边可以确定某一事件马上或已经发生,就可以调用Selector.wakeup()方法,让Selector.select()立即返回,然后获取 SelectionKey集合也好,重新Selector.select()(这已经是下一次循环了)也罢。
注意!!!如果某一个AbstractSelectableChannel在同一个Selector上注册了两个不同的感兴趣的事件类型,那么返回的两个SelectionKey是没有任何关系的。虽然可以通过SelectionKey再次修改 AbstractSelectableChannel感兴趣的事件类型。SelectionKey只在注册时生成返回,所以有(Channel + Selector) = SelectionKey。但是吧,啧,注册多个时会卡死,所以千万不要同一个Channel和同一个Selector注册多个!!
5.11. AIO
- 详见Demo
- 顺带一提,AIO是全异步的,什么意思呢?就是建立连接后立即返回,至于连接后干啥,那属于异步调用的事,只管提供一个Handler,在连接建立后供其调用即可。
- 连接建立后的读,写操作也是异步的,要求读提供一个缓冲区,会在读完了后通知提前设置好的Handler,写同理,提供一个已经设置好写回数据的缓冲区,和一个Handler,并在写操作完成时调用Handler。
6. JDK流式编程和并发
6.1. 函数式编程
6.1.1. 函数作为一等公民
- 一个函数的返回值可以作为另一个函数的参数,多个函数之间连接起来实现复杂功能,但每个函数仅执行一小部分。
6.1.2. 无副作用
- 要求函数尽量不改变外部变量,或者限制其更改范围,尽可能把影响控制到最小。
6.1.3. 声明式的
- 声明式要求对于函数编程,不需要创建变量,指出数据变更,结构,循环,跳转,而仅仅在参数里支持想要什么即可。
6.1.4. 不变的对象
- 函数式编程会尽可能不修改输入的值,即使在函数体中输出了新的值,但是参数也是不变的。
6.1.5. 易于并行
- 所谓线程安全无非就是多线程时,对象会被写坏,但是因为函数式编程,入参不可变,所以便没有线程安全这一说,易于并行运行。
6.4.6. 更少的代码
6.2. 函数式编程基础
6.2.1. FunctionalInterface注解
- 这个注解标注的接口会指明它是一个函数式接口。
- 函数式接口指的是只有一个抽象方法的接口,由Object实现的方法和有默认实现的方法不叫抽象方法。
- 如果某个接口只有一个抽象方法,即使没有这个接口也可以认为是函数式接口。
6.2.2. Lambda表达式
- Lambda表达式没有方法名,只有参数和返回值。
- Lambda一样无法更新外部变量的值。
6.2.3. 方法引用
- 即[类名/实例方法/超类/类型]::[方法名/new]
6.3. 并行流
- 使用parallel类的方法可以实现并行流,每个流运行在一个线程上。
6.4. CompletableFuture接口
6.4.1. 完成后通知我
- 详见Demo
6.4.2. 异步执行
- 详见Demo
6.4.3. 流式调用
- 详见Demo
6.4.4. 异常处理
- 详见Demo
6.4.5. 组合多个CompletableFuture
- 详见Demo
6.4.6. 支持Timeout的CompletableFuture
- 详见Demo
6.5. 改进的读写锁: StampedLock
- 读写锁使用的是悲观策略,认为读写之间需要加锁,但是StampedLock使用的是乐观锁,读写之间不必加锁,读操作可以通过不停尝试地方式获取想要的值。
- 详见Demo
Java NIO处理网络的核心组件只有四个:Channel,Selector,SelectionKey和java.nio.Buffer。
说一下ServerSocketChannel,SocketChannel,Selector和SelectionKey之间的关系。 ServerSocketChannel和SocketChannel不说了,无非就是一个用来在服务端建立连接,一个处理连接(实际I/O交互)的区别,在这里统称为AbstractSelectableChannel,也就是它俩都继承的类。
Selector.select()调用系统调用,轮询端口,记录已注册的AbstractSelectableChannel感兴趣的事件,如果发生了所有已注册的AbstractSelectableChannel感兴趣的事件之一的话,就返回。否则阻塞。 对于AbstractSelectableChannel来说,怎么让Selector帮自己记录并轮询自己感兴趣的事件呢?答案是:注册到Selector上即可,同时设置感兴趣的事件类型。
在注册成功后,会返回一个SelectionKey类型的变量,通过它,可以操作AbstractSelectableChannel和Selector。SelectionKey本身就是AbstractSelectableChannel和它注册到的Selector的凭证。 就像是订单一样,记录着它们俩的关系,所以在注册成功的后续操作里,一般都是用SelectionKey来实现的。同时,SelectionKey还有一个attachment()方法,可以获取附加到它上面的对象。 一般我们用这个附属对象来处理当前SelectionKey所包含的AbstractSelectableChannel和Selector的实际业务。
刚才说到了Selector.select(),它会一直阻塞直到发生了感兴趣的事件,但是有时候我们这边可以确定某一事件马上或已经发生,就可以调用Selector.wakeup()方法,让Selector.select()立即返回,然后获取 SelectionKey集合也好,重新Selector.select()(这已经是下一次循环了)也罢。
注意!!!如果某一个AbstractSelectableChannel在同一个Selector上注册了两个不同的感兴趣的事件类型,那么返回的两个SelectionKey是没有任何关系的。虽然可以通过SelectionKey再次修改 AbstractSelectableChannel感兴趣的事件类型。SelectionKey只在注册时生成返回,所以有(Channel + Selector) = SelectionKey。但是吧,啧,注册多个时会卡死,所以千万不要同一个Channel和同一个Selector注册多个!!
5.11. AIO
- 详见Demo
- 顺带一提,AIO是全异步的,什么意思呢?就是建立连接后立即返回,至于连接后干啥,那属于异步调用的事,只管提供一个Handler,在连接建立后供其调用即可。
- 连接建立后的读,写操作也是异步的,要求读提供一个缓冲区,会在读完了后通知提前设置好的Handler,写同理,提供一个已经设置好写回数据的缓冲区,和一个Handler,并在写操作完成时调用Handler。
6. JDK流式编程和并发
6.1. 函数式编程
6.1.1. 函数作为一等公民
- 一个函数的返回值可以作为另一个函数的参数,多个函数之间连接起来实现复杂功能,但每个函数仅执行一小部分。
6.1.2. 无副作用
- 要求函数尽量不改变外部变量,或者限制其更改范围,尽可能把影响控制到最小。
6.1.3. 声明式的
- 声明式要求对于函数编程,不需要创建变量,指出数据变更,结构,循环,跳转,而仅仅在参数里支持想要什么即可。
6.1.4. 不变的对象
- 函数式编程会尽可能不修改输入的值,即使在函数体中输出了新的值,但是参数也是不变的。
6.1.5. 易于并行
- 所谓线程安全无非就是多线程时,对象会被写坏,但是因为函数式编程,入参不可变,所以便没有线程安全这一说,易于并行运行。
6.4.6. 更少的代码
6.2. 函数式编程基础
6.2.1. FunctionalInterface注解
- 这个注解标注的接口会指明它是一个函数式接口。
- 函数式接口指的是只有一个抽象方法的接口,由Object实现的方法和有默认实现的方法不叫抽象方法。
- 如果某个接口只有一个抽象方法,即使没有这个接口也可以认为是函数式接口。
6.2.2. Lambda表达式
- Lambda表达式没有方法名,只有参数和返回值。
- Lambda一样无法更新外部变量的值。
6.2.3. 方法引用
- 即[类名/实例方法/超类/类型]::[方法名/new]
6.3. 并行流
- 使用parallel类的方法可以实现并行流,每个流运行在一个线程上。
6.4. CompletableFuture接口
6.4.1. 完成后通知我
- 详见Demo
6.4.2. 异步执行
- 详见Demo
6.4.3. 流式调用
- 详见Demo
6.4.4. 异常处理
- 详见Demo
6.4.5. 组合多个CompletableFuture
- 详见Demo
6.4.6. 支持Timeout的CompletableFuture
- 详见Demo
6.5. 改进的读写锁: StampedLock
- 读写锁使用的是悲观策略,认为读写之间需要加锁,但是StampedLock使用的是乐观锁,读写之间不必加锁,读操作可以通过不停尝试地方式获取想要的值。
- StampedLock提供了一种写策略,两种读策略,先说读策略,首先获取锁,会先判断当前锁是否可用,如果是,直接返回当前邮戳+1的值,设置锁可用=false,否则添加到等待队列,阻塞。在释放锁时,会把邮戳再+1。
- 现在讨论读,读分为乐观读和悲观读,乐观读会假设数据不会被更改,它会尝试获取锁并把邮戳+1。然后在读完了后验证邮戳是否和刚刚那个一致,如果不一致说明在它读的期间发生了写操作;此时进行悲观读,也就是另锁可用=false,阻塞写。
- 关于其获取不到锁实现,是在一个循环中实现的,再循环最后一步是使用LockSupport阻塞当前线程,但是这里有一个坑,就是如果线程尝试获取读锁时被中断,会造成CPU占用100%。
- 实现原理大致如下: