目录
Java多线程并发基础(JUC)
什么是JUC
在 Java 5.0 提供了 java.util.concurrent
(简称JUC)包,此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中的 Collection 实现等。
并发编程的本质:充分利用CPU的资源
线程和进程
进程和线程主要区别在于他们是操作系统不同的资源管理方式:
进程是程序的一次执行过程(运行中的程序),是系统运行程序的基本单位。
一个进程至少包含一个线程(main),可以包含多个线程。(换言之,线程是进程内的执行单元)
线程与进程相似,它是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程。
同类线程有共享的堆和方法区(jdk8之后的元空间(MetaSpace)),每个线程又有自己的程序计数器,虚拟机栈,本地方法栈。系统在各个线程之间的切换工作要比进程负担低,因此线程又被称为轻量级进程。
Java不可以开启线程,需要调用本地方法去开启线程。
带native关键字的方法为本地方法,由底层的C++编写,Java无法直接操作硬件,只能通过调用本地方法来执行操作。比如Thread类的start方法开启一条线程时,就是调用了本地方法去开启一条线程。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
并发与并行
并发是指计算机在同一时间段内处理多任务的执行。比如:多个线程操作同一个资源,但这多个线程不一定同时执行;客户A和客户B同时访问淘宝网站,淘宝服务器同时处理他们得请求。
并行是指多任务同时执行,但是任务之间没有任何关系,不涉及共享资源。比如:小明一边看电视一边喝水,2件事互不干扰;100米赛跑多个人同时开始跑;cpu多个核心的情况下,多个线程同时执行。
线程的六种状态
NEW(新生)、RUNNABLE(运行)、BLOCKED(阻塞)、WAITING(等待,死死地等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)
线程状态的转换关系如下:
线程中wait方法和sleep方法的区别
- wait方法属于Object类,sleep方法属于Thread类
- 使用wait方法时会释放锁,而sleep不会。(sleep可以理解为睡觉了,抱着锁睡着了,不会释放锁)
- wait方法必须在同步代码块中使用,sleep方法可以在任何地方使用。
- 使用wait方法不需要捕获异常,使用sleep方法必须捕获异常。
Synchronized锁和Lock锁
Synchronized和Lock的区别
- Synchronizd 是内置的Java关键字,而Lock是一个Java类。
- Synchroized 无法判断获取锁的状态,Lock可以判断是否获取到了锁。
- Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Synchronized:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待;Lock:分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待
- Synchronized:可重入锁、非中断锁、非公平锁;Lock:可重入锁、可判断锁、公平锁(两者皆可)。由于Synchronized是非中断锁,必须等待线程执行完成才能释放锁。
- Synchronized 适合锁少量的同步代码,Lock适合锁大量的同步代码。
Synchronized锁和Lock锁使用示例:
Synchronized锁
public class SaleTicketDemo01 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"线程A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"线程B").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"线程C").start();
}
}
class Ticket{
//属性、方法
private int number = 30;
//卖票的方式
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}
}
Lock锁
随着这种增加的灵活性,也会产生额外的责任。没有块结构化锁定会删除使用synchronized方法和语句发生的锁的自动释放。在大多数情况下,应使用以下惯用语:
Lock l = ...;
l.lock();
try{
// assess the resource protected by this lock
} finally {
l.unlock();
}
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。
public class SaleTicketDemo02 {
public static void main(String[] args) {
//并发:多线程操作同一个资源类,把资源类丢入线程
Ticket2 ticket = new Ticket2();
//@FunctionalInterface 函数式接口,jdk1.8 lambda表达式(参数)->{代码}
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"线程A").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"线程B").start();
new Thread(()->{ for (int i = 0; i < 40; i++) ticket.sale(); },"线程C").start();
}
}
class Ticket2{
//属性、方法
private int number = 30;
Lock lock = new ReentrantLock();
public void sale() {
//加锁
lock.lock();
try {
//业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//解锁
lock.unlock();
}
}
}
生产者和消费者问题
Synchronized锁:通过wait和notify方法来实现通知和唤醒线程。
Lock锁:通过调用Condition的await和signal方法来实现精准的通知和唤醒线程。
Synchronized锁实现
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
}
}
class Data{
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notifyAll();
}
}
当为多条线程时,比如线程A、B、C、D,存在虚假唤醒(线程也可以唤醒,而不会被通知,中断或超时)的问题。虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。换句话说,等待应该总是出现在循环中,就像这样:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
如果当前线程中断(interrupted)任何线程之前或在等待时,那么InterruptedException被抛出。如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。
解决方法:将if改为while,保证多线程环境下的安全
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data{
private int number = 0;
public synchronized void increment() throws InterruptedException {
while (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
this.notifyAll();
}
}
Lock锁实现
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data2{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) {
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) {
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我-1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition精准通知线程等待和唤醒
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{for(int i = 0;i<10;i++)data3.printA();},"A").start();
new Thread(()->{for(int i = 0;i<10;i++)data3.printB();},"B").start();
new Thread(()->{for(int i = 0;i<10;i++)data3.printC();},"C").start();
}
}
class Data3{//资源类
//A执行完调用B,B执行完调用C
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
//1A 2B 3C
private int number = 1;
public void printA() {
lock.lock();
try {
while (number != 1) {
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAA");
// 唤醒,唤醒指定的人,唤醒B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (number != 2) {
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBB");
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (number != 3) {
condition3.await();
}
number = 1;
System.out.println(Thread.currentThread().getName()+"=>CCCCCC");
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8锁现象
下面使用一个手机实体类来测试8锁现象,手机实体类包含了两个方法,一个方法实现了打电话功能(直接打印),另一个方法实现了发短信功能(直接打印)。在标准情况下,开启两个线程来测试该类,并发执行时,看看是哪个方法先执行。
1、一个对象,两个同步方法
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
class Phone{
public synchronized void sendSms() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
发短信
打电话
Process finished with exit code 0
2、一个对象,两个同步方法,其中一个存在延迟
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
class Phone{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
发短信
打电话
Process finished with exit code 0
总结:上面两种情况执行结果都是先发短信然后再打电话,线程A虽然先开启了而且第二种情况中线程A调用的方法中还延迟了4秒,但不一定先执行,因为使用synchronized锁住两个方法时,synchronized锁的对象是方法的调用者(phone实例),此时两个方法用的是同一个锁,哪个方法先拿到锁就先执行。因为A、B线程哪条先执行由CPU决定,所以即便线程中含有延迟,也不会影响到线程的执行调度顺序。
3、一个对象,一个同步方法,一个普通方法
public class Test2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.hello();},"B").start();
}
}
class Phone2{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
结果:
hello
发短信
Process finished with exit code 0
总结:实体类中添加了普通方法hello(打印“hello”功能),因为普通方法没有加锁,不是同步方法,所以不受锁的影响,调用时会马上执行,而且发短信的方法中增加了延迟,执行时肯定慢于普通方法。
4、两个对象,两个同步方法
public class Test2 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{phone1.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone2.call();},"B").start();
}
}
class Phone2{
public synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
public void hello() {
System.out.println("hello");
}
}
结果:
打电话
发短信
Process finished with exit code 0
总结:实例化两个对象来分别调用打电话和发短信的两个方法,此时是两个对象对应两个不同的同步方法,即有两个调用者对应两把不一样的锁,执行不会受彼此的影响,线程A执行发短信方法时,期间延迟了4秒,因为线程B是另外一把锁,执行不受影响,所以线程B的打电话方法先执行完。
5、一个对象,两个静态同步方法
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
class Phone3{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
结果:
发短信
打电话
Process finished with exit code 0
总结:对两个方法加上static关键字,使这两个方法成为静态方法,类一加载时就有了这两个方法,此时synchronized锁的是Class模板(类模板),一个实体类只有一个类模板(可以理解为编译时只能生成一个.class文件),一个类对应一个全局唯一的类模板;因为锁的是同一个类模板,所以哪条线程先拿到锁就先执行,无论方法中是否有延迟。
6、两个对象,两个静态同步方法
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
class Phone3{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
结果:
发短信
打电话
Process finished with exit code 0
总结:这里的执行结果依然为先发短信然后再打电话,上面情况四种提到了(实例化两个对象来分别调用打电话和发短信的两个方法时,此时是两个对象对应两个不同的同步方法,即有两个调用者对应两把不一样的锁,执行不会受彼此的影响),但该情况的两个方法是静态方法,静态方法在类加载时就存在了,此时synchroized锁的是Class模板,虽然是两个对象,但两个对象的类模板只有一个,因此哪条线程先拿到锁就先执行,无论方法中是否有延迟。
7、一个对象,一个静态同步方法,一个普通同步方法
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone.call();},"B").start();
}
}
class Phone4{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
打电话
发短信
Process finished with exit code 0
总结:先打电话再发短信,普通同步方法锁的是调用者,静态的同步方法锁的是Class类模板,是两把不同的锁,执行互不影响,线程A调用的方法中存在延迟,因此线程B调用的方法先执行,于是先打电话再发短信。
8、两个对象,一个静态同步方法,一个普通同步方法
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{phone.sendSms();},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{phone2.call();},"B").start();
}
}
class Phone4{
public static synchronized void sendSms() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
打电话
发短信
Process finished with exit code 0
小结:先打电话再发短信,普通同步方法锁的是调用者,静态的同步方法锁的是Class类模板,是两把不同的锁,执行互不影响,线程A调用的方法中存在延迟,因此线程B调用的方法先执行,于是先打电话再发短信。
总结
synchronized锁的方法是静态方法时,锁的对象是类模板,一个类只有一个类模板,一个类模板对应一把锁;锁的方法是非静态方法时,锁的对象是方法的调用者,一个调用者对应一把锁。
并发情况下集合类不安全
并发情况下ArrayList、HashSet、HashMap的使用并不安全,会抛出并发修改异常。原因:当多个线程争抢修改信息时候,当一个线程正在修改却被其他线程抢占去同一个位置的修改权造成修改错误,丢失数据。
List
ArayList的底层是数组实现的,而且可以自动扩容,获得元素或者在数组尾段插入元素的效率高,ArrayList有其独特的优势。
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
结果:
[c861e]
[c861e, a9151]
[c861e, a9151, 0e989]
[c861e, a9151, 0e989, 69b90]
[c861e, a9151, 0e989, 69b90, 39d9f]
[c861e, a9151, 0e989, 69b90, 39d9f, 99abb]
[c861e, a9151, 0e989, 69b90, 39d9f, 99abb, 2e458]
[c861e, a9151, 0e989, 69b90, 39d9f, 99abb, 2e458, ed640]
[c861e, a9151, 0e989, 69b90, 39d9f, 99abb, 2e458, ed640, 9ea95, 84640]
Exception in thread "9" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.juc.unsafe.ListTest.lambda$main$0(ListTest.java:30)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
解决方案:
-
List<String> list = new Vector<>();
Vertor里的add方法加了synchronized,同一时刻只允许一个线程访问和修改,但效率会比较低
-
List<String> list = Collections.synchronizedList(new ArrayList<>());
-
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWrite(写入时复制),简称“COW”,是计算机程序设计领域的一种优化策略。
Set
HashSet底层其实就是HashMap,并发情况下HashMap不安全,所以HashSet也不安全,有关HashSet的部分源码如下:
public HashSet() {
map = new HashMap<>();
}
// add set 本质就是map key是无法重复的
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//不变的值
private static final Object PRESENT = new Object();
测试:
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
}).start();
}
}
}
结果:
Exception in thread "Thread-58" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.juc.unsafe.SetTest.lambda$main$0(SetTest.java:27)
at java.lang.Thread.run(Thread.java:748)
解决方案:
-
Set<String> set = Collections.synchronizedSet(new HashSet<>());
-
Set<String> set = new CopyOnWriteArraySet<>();
Map
public class MapTest {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map);
}).start();
}
}
}
结果:
java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$EntryIterator.next(HashMap.java:1471)
at java.util.HashMap$EntryIterator.next(HashMap.java:1469)
at java.util.AbstractMap.toString(AbstractMap.java:554)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.juc.unsafe.MapTest.lambda$main$0(MapTest.java:26)
at java.lang.Thread.run(Thread.java:748)
解决方案:
Map<String, Object> map = new ConcurrentHashMap<>();
JUC常用辅助类
CountDownLatch
使用原理:每次有线程调用的时候调用countDown()数量-1,直到计数器变为0,countDownLatch.await()就会被唤醒,继续向下执行。
countDownLatch.countDown();
//数量-1
countDownLatch.await();
//等待计数器归零,然后再向下执行
使用示例
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 出去了");
// 数量-1
countDownLatch.countDown();
}).start();
}
// 等待计数器归零,然后再向下执行
countDownLatch.await();
System.out.println("都出去完了,关门!");
}
}
结果:
Thread-1 出去了
Thread-2 出去了
Thread-0 出去了
Thread-3 出去了
Thread-4 出去了
Thread-5 出去了
都出去完了,关门!
Process finished with exit code 0
CyclicBarrier
使用原理:在执行指定的线程数后开启一条自定义的线程(自定义的线程指CycliBarrier中定义的线程)。
使用示例
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
// 此处自定义线程
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
结果:
Thread-0收集1个龙珠
Thread-1收集2个龙珠
Thread-2收集3个龙珠
Thread-4收集5个龙珠
Thread-3收集4个龙珠
Thread-5收集6个龙珠
Thread-6收集7个龙珠
召唤神龙成功
Process finished with exit code 0
Semaphore
Semaphore:信号量
使用原理
semaphore.acquire();
获得,如果一开始定义的资源(例如车位)已被使用完(已经满了),其他的线程(车辆)就需要等待,等待资源(车位)被释放为止
semaphore.release();
释放,会将当前的信号量(停车位)释放+1,然后唤醒等待的线程(车辆)
作用:多个共享资源互斥的使用时,可以用来并发限流,控制最大的线程数。
使用示例
以下代码模拟了在含有3个停车位的停车场中,车辆1、2、3先到,取得了车位,由于车位已被占用,后面过来的车辆4、5、6没有车位停,就需要等待车位,直到车辆1、2、3离开后,他们才有车位。
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
// acquire()得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// release()释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
结果:
1抢到车位
2抢到车位
3抢到车位
2离开车位
1离开车位
3离开车位
5抢到车位
4抢到车位
6抢到车位
4离开车位
6离开车位
5离开车位
Process finished with exit code 0
Callable接口
特点:可以有返回值、可以抛出异常、方法不同,run()
细节:有缓存、结果可能需要等待、会阻塞
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
//如果实现了callcable接口,要使用适配类FutureTask来启动线程
FutureTask futureTask = new FutureTask(myThread);
//启动Callable
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start();
//获取Callable的返回结果
//这个get方法可能会产出阻塞,把他放到最后
Integer integer = (Integer) futureTask.get();
//或者使用异步通信来处理
System.out.println(integer);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("call()");
//耗时的操作
return 1024;
}
}
读写锁ReadWriteLock
先来看看没有加读写锁的情况
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 1; i <= 500; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 500; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
class MyCache{
private volatile Map<String, Object> map = new HashMap<>();
//存,写数据
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入OK");
}
//取,读数据
public void get(String key) {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
}
}
结果:
1写入1
2写入2
2写入OK
1写入OK
3写入3
3写入OK
4写入4
4写入OK
5写入5
5写入OK
6写入6
6写入OK
131写入131
131写入OK
8写入8
8写入OK
9写入9
9写入OK
。。。。。。
402读取402
402读取OK
401读取401
401读取OK
。。。。。。
405写入405
405写入OK
403写入403
403写入OK
402写入402
402写入OK
400写入400
400写入OK
以上代码开启了500条线程同时进行读写操作,从结果中可以看到,402、401还没有写入,就被某线程进行读取,读取先于写入执行,读取到的是空数据,高并发情况下会造成不可预估的错误。
以下是加了读写锁的情况:
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
//写入
for (int i = 1; i <= 500; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 500; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
class MyCacheLock{
private volatile Map<String, Object> map = new HashMap<>();
//读写锁:更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//取,读,所有人都可以读
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
结果:
1写入1
1写入OK
2写入2
2写入OK
3写入3
3写入OK
4写入4
4写入OK
5写入5
5写入OK
8写入8
8写入OK
。。。。。。
130写入130
130写入OK
129写入129
129写入OK
128写入128
128写入OK
268写入268
268写入OK
129读取129
129读取OK
从结果可以看出,各线程虽然没有顺序执行,但所有的读操作都会在写入之后执行,比如线程129,先写入在读取,这就是读写锁的好处所在,读的时候可以被多个线程同时读,写的时候只能有一个线程去写
阻塞队列ArrayBlockingQueue
阻塞:写入,如果队列满了,就必须阻塞等待
队列:取出,如果队列是空的,就必须阻塞等待生产
使用场景:多线程并发处理、线程池
队列的基本使用其实就是添加和取出操作,下面是同步队列的4组Api实现的不同方式,可根据实际情景进行使用:
方式 | 抛出异常 | 有返回值 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek | - | - |
下面通过几个代码示例来理解这几组Api,先定义一个数组类型的同步队列,其初始容量为3,其中含有元素a、b、c,通过4个静态方法来测试这4组Api:
add()
public class Test {
public static void main(String[] args) throws InterruptedException {
test1();
}
/**
* 抛出异常,无返回值
*/
public static void test1(){
//队列的初始大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//查看队列首元素
System.out.println(blockingQueue.element());
System.out.println(blockingQueue.add("d"));
}
}
结果:
true
true
true
a
Exception in thread "main" java.lang.IllegalStateException: Queue full
at java.util.AbstractQueue.add(AbstractQueue.java:98)
at java.util.concurrent.ArrayBlockingQueue.add(ArrayBlockingQueue.java:312)
at com.juc.bq.Test.test1(Test.java:34)
at com.juc.bq.Test.main(Test.java:19)
队列初始容量为3,往其中添加a、b、c、d四个元素,添加a、b、c元素成功,会返回true,方法element()可以检测队首元素,在添加d元素时,add方法会抛出java.lang.IllegalStateException: Queue full
异常,提示队列已满。add()方法会抛出异常,没有返回值。
remove()
public class Test {
public static void main(String[] args) throws InterruptedException {
test1();
}
/**
* 抛出异常,无返回值
*/
public static void test1(){
//队列的初始大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
}
结果:
a
b
c
Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractQueue.remove(AbstractQueue.java:117)
at com.juc.bq.Test.test1(Test.java:41)
at com.juc.bq.Test.main(Test.java:19)
Process finished with exit code 1
队列初始容量为3,往其中放入a、b、c三个元素,此时队列已满,使用remove()方法取出其中的元素,在取出前3个元素时成功取出,取出第4次时报了java.util.NoSuchElementException
异常,提示没有这样的元素。remove()方法会抛出异常,没有返回值。
offer()
public class Test {
public static void main(String[] args) throws InterruptedException {
test2();
}
/**
* 不抛出异常,有返回值
*/
public static void test2() {
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
//返回false,不抛出异常
System.out.println(blockingQueue.offer("d"));
}
}
结果:
true
true
true
false
Process finished with exit code 0
队列的初始容量为3,添加前3个元素时都成功了,返回true,此时队列已满,添加第4个元素时,返回false,提示添加失败了。offer()方法不会抛出异常,有返回值。
poll()
public class Test {
public static void main(String[] args) throws InterruptedException {
test2();
}
/**
* 不抛出异常,有返回值
*/
public static void test2() {
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//返回null,不抛出异常
System.out.println(blockingQueue.poll());
}
}
结果:
a
b
c
null
Process finished with exit code 0
队列的初始容量为3,取出前3个元素时都成功了,此时队列已空,取出第4个元素时,返回null,提示没有此元素。poll()方法不会抛出异常,有返回值。
put()、take()
public class Test {
public static void main(String[] args) throws InterruptedException {
test3();
}
/**
* 等待,阻塞(一直等待)
* @throws InterruptedException
*/
public static void test3() throws InterruptedException {
//队列的大小
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.put("a");
arrayBlockingQueue.put("b");
arrayBlockingQueue.put("c");
//队列没有位置了,一直阻塞
arrayBlockingQueue.put("d");
}
}
结果:
public class Test {
public static void main(String[] args) throws InterruptedException {
test3();
}
/**
* 等待,阻塞(一直等待)
* @throws InterruptedException
*/
public static void test3() throws InterruptedException {
//队列的大小
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.put("a");
arrayBlockingQueue.put("b");
arrayBlockingQueue.put("c");
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
}
}
结果:
a
b
c
队列初始容量为3,当添加第4个元素时不会抛出异常也没有返回值,线程会一直阻塞处于执行状态;当取出第4个元素时,前3个元素成功取出,到第4个时一直阻塞,线程不能正常停止;put()和take()方法会一直阻塞等待,线程不会停止,直到队列中有容量可以进行操作。
offer()、poll()设置等待超时
public class Test {
public static void main(String[] args) throws InterruptedException {
test4();
}
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.offer("a");
arrayBlockingQueue.offer("b");
arrayBlockingQueue.offer("c");
//等待超过2秒就退出
arrayBlockingQueue.offer("d",2, TimeUnit.SECONDS);
}
}
结果:
Process finished with exit code 0
public class Test {
public static void main(String[] args) throws InterruptedException {
test4();
}
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.offer("a");
arrayBlockingQueue.offer("b");
arrayBlockingQueue.offer("c");
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
//等待超过2秒就退出
arrayBlockingQueue.poll(2,TimeUnit.SECONDS);
}
}
在使用poll和offer方法时可添加参数设置等待超时时间,如果遇到了阻塞,经过一定的时间后会动退出。
BlockingQueue实现是线程安全的,常用于生产者-消费者队列,BlockingQueue可以安全地与多个生产者和多个消费者使用。使用示例如下:
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while (true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
同步队列SynchronousQueue
使用原理:该同步队列最多只能放进一个元素,存进去一个元素,必须等待取出之后,才能重新放进去。
使用示例
public class SynchronousQueueDemo {
public static void main(String[] args) {
//同步队列
BlockingQueue synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 往同步队列中添加数据1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+" 往同步队列中添加数据2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+" 往同步队列中添加数据3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" 从同步队列中取出数据"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" 从同步队列中取出数据"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" 从同步队列中取出数据"+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B").start();
}
}
结果:
线程A 往同步队列中添加数据1
线程B 从同步队列中取出数据1
线程A 往同步队列中添加数据2
线程B 从同步队列中取出数据2
线程A 往同步队列中添加数据3
线程B 从同步队列中取出数据3
Process finished with exit code 0
线程池
线程池相关的三个知识点:三大方法、七大参数、四种拒绝策略
线程池的优点:降低资源的消耗、提高响应的速度、方便线程管理、可以控制最大并发数、线程可以复用
程序运行的本质就会占有系统资源,池化技术的出现可以优化资源的使用,池化技术通俗一点来理解就是事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我,降低资源创建与销毁的消耗,线程池、JDBC连接池、内存池、对象池等都是用到了池化技术。
创建线程池的三大方法
- 单个线程的线程池
ExecutorService threadExecutor = Executors.newSingleThreadExecutor();
- 创建一个固定大小的线程池,大小为5
ExecutorService threadExecutor = Executors.newFixedThreadPool(5);
- 可改变大小的线程池,线程池中线程的数量不固定,可以根据需求动态更改数量;
ExecutorService threadExecutor = Executors.newCachedThreadPool();
不建议使用Executors创建线程池,而是通过ThreadPoolExecuptor的方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。原因如下:
newFixedThreadPool
和newSingleThreadExecutor
:分别会创建固定数量的线程池和单线程线程池,尽管二者线程池数量有限,但是会创建长度为Integer.MAX_VALUE
长度的阻塞队列,这样可能会导致阻塞队列的任务过多而导致OOM(OutOfMemoryError);newCachedThreadPool
和newScheduledThreadPool
:会创建缓存线程池和周期任务线程池,二者线程池的最大线程为Integer.MAX_VALUE
,也可能会导致OOM。
Executors
工具类中创建线程池的方法其实是调用了ThreadPoolExecutor
类来创建线程池,推荐使用此类来创建线程池,ThreadPoolExecutor
的其中一个构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
构造方法中的七大参数如下:
corePoolSize
: 核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;maximumPoolSize
:线程池最大线程数,线程池中允许同时执行的最大线程数量,定义线程池最大线程数时,使用Runtime.getRuntime().availableProcessors()
来定义,这个方法是获得当前计算机的cpu核心数。keepAliveTime
:当表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;unit
:keepAliveTime
的时间单位workQueue
:任务队列,当有新任务来临时,如果核心线程数corePoolSize
被用完,此时如果workQueue
有空间,任务就会被放入workQueue
threadFactory
:创建工作线程的工厂,也就是如何创建线程的,一般采用默认的handler
:拒绝策略. 如果线程池陷入一种极端情况:工作队列满了,无法再容纳新的任务,最大工作线程也到达限制了,此时线程池如何处理这种极端情况。
ThreadPoolExecutor
提供了四种策略:
AbortPolicy
(是线程池的默认拒绝策略):如果还有新任务到来,那么拒绝,并抛出RejectedExecutionException异常CallerRunsPolicy
:这种策略不会拒绝执行新任务,但是由发出任务的线程执行,也就是说当线程池无法执行新任务的时候,就由请求线程自己执行任务DiscardPolicy
:这种策略会拒绝新任务,但是不会抛出异常DiscardOldestPolicy
:这种策略不会拒绝策略,他会抛弃队列中等待最久那个任务,来执行新任务
四大函数式接口
Consumer
消费型接口:只有输入,没有返回值
public class Demo3 {
public static void main(String[] args) {
Consumer<String> consumer1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer1.accept("111");
//lambda表达式写法,更加简易
Consumer<String> consumer2 = (str) -> {
System.out.println(str);
};
consumer2.accept("222");
}
}
结果:
111
222
Process finished with exit code 0
Function
函数型接口:有一个输入参数,有一个输出,只要是函数式接口,就可以使用lambda表达式来简化。
public class Demo1 {
public static void main(String[] args) {
Function<String, String> function = (str) -> {
return str;
};
System.out.println(function.apply("asd"));
}
}
结果:
asd
Process finished with exit code 0
Predicate
断定型接口:有一个输入参数,返回值只能是布尔值。
public class Demo2 {
public static void main(String[] args) {
Predicate<String> predicate = (s)->{ return s.isEmpty(); };
System.out.println(predicate.test("asd"));
}
}
结果:
asd
Process finished with exit code 0
Supplier
供给型接口:没有参数,只有返回值
public class Demo4 {
public static void main(String[] args) {
Supplier supplier = new Supplier<Integer>() {
@Override
public Integer get() {
System.out.println("get()");
return 1024;
}
};
System.out.println(supplier.get());
//lambda表达式写法,更加简易
Supplier supplier1 = () -> {
return 1024;
};
System.out.println(supplier1.get());
}
}
结果:
get()
1024
1024
Process finished with exit code 0
Stream流式计算
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。(这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等)
Stream(流)是一个来自数据源的元素队列并支持聚合操作
- 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
- 数据源: 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
- 聚合操作: 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同链式编程(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代,这叫做外部迭代。 Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。
示例代码:5个用户中输出一个ID必须是偶数、年龄必须大于23岁、用户名转为大写字母、用户名字母倒着排序的用户
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(6, "e", 25);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给Stream流
// 链式编程、函数式接口、Stream流式计算、lambda表达式
list.stream().filter(user -> {
return user.getId() % 2 == 0;
})
.filter(user -> {
return user.getAge() > 23;
})
.map(user -> {
return user.getName().toUpperCase();
})
.sorted((uu1, uu2) -> {
return uu1.compareTo(uu2);
})
.limit(1)
.forEach(System.out::println);
}
}
@Data
class User {
private int id;
private String name;
private int age;
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Fork/Join框架
什么是Fork/Join框架
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork Join的运行流程图如下:
工作窃取
工作窃取算法是指某个线程从其他队列里窃取任务来执行。
需要使用工作窃取算法的原因:
如果需要做一个比较大的任务,可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。
但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
Fork/Join框架的设计
一、分割任务
二、执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
Fork/Join使用两个类来完成以上两件事情
- ForkJoinTask:我们需要使用ForkJoin框架,必须创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
- RecursiveAction:用于没有返回结果的任务
- RecursiveTack:用于有返回结果的任务
-
ForkJoinPool:ForkJoinTask需要通过ForkJoinPool执行。
任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
使用示例
示例一:实现RecursiveTack接口的计算类(有返回值)
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
/**
* 临界值
*/
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
/**
* 计算方法
*/
@Override
protected Long compute() {
if ((end - start) < temp) {
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
// 中间值
long middle = (start + end) / 2;
// 新建一个任务task1,计算start到middle之间的数
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
// 拆分任务,把任务压入线程队列
task1.fork();
// 新建一个任务task2,计算middle+1到end之间的数
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
// 拆分任务,把任务压入线程队列
task2.fork();
// 返回两个任务的执行结果
return task1.join() + task2.join();
}
}
}
示例二:计算0到10亿之间的整数和:
public class Test {
/**
* 定义两个常量,计算0到10亿之间的累加和
*/
private static long min = 0L;
private static long max = 1000_0000_0000L;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
public static void test1() {
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = min; i <= max; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum="+sum+",for循环求和所用时间时间:"+(end-start)+",当前线程数:"+Runtime.getRuntime().availableProcessors());
System.out.println("==============================");
}
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(min, max);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum="+sum+",ForkJoin求和所用时间:"+(end-start)+",当前线程数:"+Runtime.getRuntime().availableProcessors());
System.out.println("==============================");
}
public static void test3() {
long start = System.currentTimeMillis();
//Stream并行流
long sum = LongStream.rangeClosed(min, max).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+sum+",Stream并行流求和所用时间:"+(end-start)+",当前线程数:"+Runtime.getRuntime().availableProcessors());
}
}
结果:
sum=500000000500000000,for循环求和所用时间时间:7529,当前线程数:4
==============================
sum=500000000500000000,ForkJoin求和所用时间:6550,当前线程数:4
==============================
sum=500000000500000000,Stream并行流求和所用时间:332,当前线程数:4
Process finished with exit code 0
异步回调
Future和前端的Ajax其实是一样的,都是为了给我们提供一个异步回调的通知,提高程序的运行的效率。Future设计的初衷是对将来会发生的结果进行建模。
runAsync
发起一个请求,没有返回值的runAsync异步回调
使用示例:先输出“1111”,再异步回调执行《输出1111》上面的语句
public static void test1() throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " runAsync=>Void");
});
System.out.println("1111");
//获取执行结果
completableFuture.get();
}
结果:
1111
ForkJoinPool.commonPool-worker-1 runAsync=>Void
supplyAsync
发起一个请求,有返回值的runAsync异步回调
使用示例
public static void test2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
//int i = 10 / 0;
System.out.println(Thread.currentThread().getName() + " supplyAsync=>Integer");
return 1024;
});
System.out.println(completableFuture.whenComplete((ok,error) -> {
// supplyAsync正常执行的返回结果:1024
System.out.println("t=>" + ok);
// supplyAsync错误执行的返回结果:233
System.out.println("u=>" + error);
}).exceptionally((e) -> {
// 执行出现错误时,打印错误信息,返回数值233,此方法可以获取到错误的返回的结果
System.out.println(e.getMessage());
return 233;
}).get());
}
结果:
ForkJoinPool.commonPool-worker-1 supplyAsync=>Integer
ok=>1024
error=>null
1024
这里error
输出为null,因为error为方法执行出现异常时的返回值,上面程序即成功执行返回1024(成功回调),执行失败返回233(失败回调)。
下面是出现异常,执行失败的方法:
public static void test2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
int i = 10 / 0;
System.out.println(Thread.currentThread().getName() + " supplyAsync=>Integer");
return 1024;
});
System.out.println(completableFuture.whenComplete((ok,error) -> {
//supplyAsync正常执行的返回结果:1024
System.out.println("ok=>" + ok);
//supplyAsync错误执行的返回结果:233
System.out.println("error=>" + error);
}).exceptionally((e) -> {
//执行出现错误时,打印错误信息,返回数值233,此方法可以获取到错误的返回的结果
System.out.println(e.getMessage());
return 233;
}).get());
}
结果:
ok=>null
error=>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
233
深入理解CAS
定义与原理
CAS:比较并交换(CompareAndSet),它将指定内存位置的值与给定值进行比较,如果两个值相等,就将内存位置的值改为给定值,如果不是就一直循环。CAS涉及3个元素:内存地址,期盼值和目标值。只有内存地址对应的值和期望的值相同时,才把内存地址对应的值修改为目标值。
CAS在JAVA中的底层实现(Atomic原子类实现)
-
Unsafe类。Unsafe类是CAS的核心类,由JDK自动加载,它的方法都是native方法。
因为Java无法像C/C++一样直接使用底层指针操作对象内存,Unsafe类的作用就是专门解决这个问题,它可以直接操作对象在内存中的地址。AtomicReference
类的部分源代码如下:具体步骤是
- 首先获取当前Atomic对象的value在内存中真实的偏移地址,再根据这个偏移地址
获取value的真实值。 - 然后再重复这个步骤,把两次获取到的值进行比较。
- 如果比较成功,则继续操作,否则继续循环比较。
- 而获取value在内存中真实的偏移地址和比较设置值方法都是native的。
- 首先获取当前Atomic对象的value在内存中真实的偏移地址,再根据这个偏移地址
-
volatile。Atomic原子类内部的value值是volatile修饰的,这就保证了value的可见性。
缺点
-
循环时间开销大。如果预期的值和当前值比较不成功,那么CAS会一直进行循环。如果长时间比较不成功就会一直循环,导致CPU开销过大。
-
只能保证一个共享变量的原子操作。在获取内存地址和设置值的时候都是当前Atomic对象的volatile值,如果要保证多个共享变量,那么可以通过加锁来保证线程安全和原子性。
-
ABA问题。尽管一个线程CAS操作成功,但并不代表这个过程就是没有问题的。假设2个线程读取了主内存中的共享变量,如果一个线程对主内存中的值进行了修改后,又把新值改回了原来的值,而此时另一个线程进行CAS操作发现原值和期盼的值是一样的,就顺利的进行了CAS操作。这就是CAS引发的ABA问题。
CAS出现ABA问题示例代码
定义一个共享变量,开启两条线程同时对其进行修改
public class CASDemo {
public static void main(String[] args) {
//创建一个线程池
ExecutorService threadExecutor = new ThreadPoolExecutor(
2, //线程池核心线程数为2
Runtime.getRuntime().availableProcessors(), //最大核心数
3, //阻塞等待时间,当前为3秒
TimeUnit.SECONDS, //阻塞等待时间单位
new LinkedBlockingQueue<>(3), //阻塞队列,当核心线程使用完时,后面的线程进入阻塞队列等待
Executors.defaultThreadFactory(), //创建工作线程的工厂
new ThreadPoolExecutor.DiscardPolicy() //工作队列和最大工作线程满时的拒绝策略,当前策略拒绝新任务,但是不会抛出异常
);
/**
* 在内存中定义一个共享变量,初始值为20
*/
AtomicReference<Integer> atomicReference = new AtomicReference<>(20);
//开启两条线程操作这个变量
threadExecutor.execute(()->{
System.out.println("线程"+Thread.currentThread().getName()+":执行20=>22");
if (atomicReference.compareAndSet(20, 22)) {
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}
System.out.println("线程"+Thread.currentThread().getName()+":执行22=>20");
if (atomicReference.compareAndSet(22, 20)) {
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}
});
threadExecutor.execute(()->{
System.out.println("线程"+Thread.currentThread().getName()+":执行20=>30");
if (atomicReference.compareAndSet(20, 30)) {
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}
});
threadExecutor.shutdown();
}
}
结果:
线程pool-1-thread-1:执行20=>22
修改成功
线程pool-1-thread-1:执行22=>20
修改成功
线程pool-1-thread-2:执行20=>30
修改成功
Process finished with exit code 0
从以上结果可以明显看出ABA问题的存在,线程1先将共享变量的值从20改为22,再从22改回20,线程2将共享变量的值从20改为30,这个三个过程都成功了,然而20变为30的这个过程中,已经被修改过,但是线程2却并不知道,这就是ABA问题。
解决ABA问题
JUC的atomic包下提供了AtomicStampedReference这个类来解决CAS的原子引用更新的ABA问题,它相较于普通的Atomic原子类多增加了一个版本号的字段(对应的思想:乐观锁),每次修改引用就更新版本号,这样即使发生ABA问题,也能通过版本号判断引用是否被修改过了。
通过使用AtomicStampedReference类来解决ABA问题的代码示例:
public class CASDemo {
public static void main(String[] args) {
//创建一个线程池
ExecutorService threadExecutor = new ThreadPoolExecutor(
2, //线程池核心线程数为2
Runtime.getRuntime().availableProcessors(), //最大核心数
3, //阻塞等待时间,当前为3秒
TimeUnit.SECONDS, //阻塞等待时间单位
new LinkedBlockingQueue<>(3), //阻塞队列,当核心线程使用完时,后面的线程进入阻塞队列等待
Executors.defaultThreadFactory(), //创建工作线程的工厂
new ThreadPoolExecutor.DiscardPolicy() //工作队列和最大工作线程满时的拒绝策略,当前策略拒绝新任务,但是不会抛出异常
);
//定义一个共享变量,初始值为20,初始版本号为1,如果泛型是包装类,注意对象的引用问题
AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(20, 1);
//开启两条线程操作这个变量
threadExecutor.execute(() -> {
System.out.println("当前原子变量的版本号为:" + atomicInteger.getStamp() + ",线程" + Thread.currentThread().getName() + ":执行20=>22");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicInteger.compareAndSet(20, 22, atomicInteger.getStamp(), atomicInteger.getStamp() + 1)) {
System.out.println("线程" + Thread.currentThread().getName() + ":执行20=>22" + "修改成功");
} else {
System.out.println("线程" + Thread.currentThread().getName() + ":执行20=>22" + "修改失败");
}
System.out.println("当前原子变量的版本号为:" + atomicInteger.getStamp() + ",线程" + Thread.currentThread().getName() + ":执行22=>20");
if (atomicInteger.compareAndSet(22, 20, atomicInteger.getStamp(), atomicInteger.getStamp() + 1)) {
System.out.println("线程" + Thread.currentThread().getName() + ":执行22=>20" + "修改成功");
} else {
System.out.println("线程" + Thread.currentThread().getName() + ":执行22=>20" + "修改失败");
}
System.out.println("当前原子变量的版本号为:" + atomicInteger.getStamp() + ",线程" + Thread.currentThread().getName());
});
threadExecutor.execute(() -> {
int stamp = atomicInteger.getStamp();
System.out.println("当前原子变量的版本号为:" + stamp + ",线程" + Thread.currentThread().getName() + ":执行20=>66");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicInteger.compareAndSet(20, 66, stamp, stamp + 1)) {
System.out.println("线程" + Thread.currentThread().getName() + ":执行20=>66" + "修改成功");
} else {
System.out.println("线程" + Thread.currentThread().getName() + ":执行20=>66" + "修改失败");
}
System.out.println("当前原子变量的版本号为:" + atomicInteger.getStamp() + ",线程" + Thread.currentThread().getName());
});
threadExecutor.shutdown();
}
}
结果:
当前原子变量的版本号为:1,线程pool-1-thread-1:执行20=>22
当前原子变量的版本号为:1,线程pool-1-thread-2:执行20=>66
线程pool-1-thread-1:执行20=>22修改成功
线程pool-1-thread-2:执行20=>66修改失败
当前原子变量的版本号为:2,线程pool-1-thread-2
当前原子变量的版本号为:2,线程pool-1-thread-1:执行22=>20
线程pool-1-thread-1:执行22=>20修改成功
当前原子变量的版本号为:3,线程pool-1-thread-1
Process finished with exit code 0
以上代码开启两条线程同时修改共享变量的值,线程1要将共享变量的值从20改为22再改回20,线程2要将共享变量的值改为66。从结果可以看出,线程2执行失败了,这里就是引入的AtomicStampedReference
类起的作用,线程1、2同时开启,线程2未开始修改之前获取到共享变量的版本号都是1,各自先输出一条语句后,线程1抢先执行,将共享变量的值从20改为22,版本号改为2,此时线程2执行,此时版本号为2,而线程2一开始获取到的版本号为1,前后两次的版本号不相同,说明被修改过,此时就会修改失败;线程1继续执行,此时版本号为2,线程1要将22改回20,重新获取版本号也为2,此时修改成功。
lambda表达式
- lambda表达式实质上是一个匿名方法,但该方法并非独立执行,而是用于实现由函数式接口定义的唯一抽象方法。
- 使用lambda表达式时,会创建实现了函数式接口的一个匿名类实例
- 可以将lambda表达式视为一个对象,可以将其作为参数传递
函数式接口
函数式接口是仅含一个抽象方法的接口,但可以指定Object定义的任何公有方法。
- 以下是一个函数式接口:
@FunctionalInterface
public interface IFuntionSum<T extends Number> {
T sum(List<T> numbers); // 抽象方法
}
- 以下也是一个函数式接口:
@FunctionalInterface
public interface IFunctionMulti<T extends Number> {
void multi(List<T> numbers); // 抽象方法
boolean equals(Object obj); // Object中的方法
}
- 但如果改为以下形式,则不是函数式接口:
@FunctionalInterface
public interface IFunctionMulti<T extends Number> extends IFuntionSum<T> {
void multi(List<T> numbers);
@Override
boolean equals(Object obj);
}
// IFunctionMulti 接口继承了 IFuntionSum 接口,此时 IFunctionMulti 包含了2个抽象方法
- 可以用
@FunctionalInterface
标识函数式接口,非强制要求,但有助于编译器及时检查接口是否满足函数式接口定义。- 在Java 8之前,接口的所有方法都是抽象方法,在Java 8中新增了接口的默认方法。
lambda表达式
-
lambda表达式的2种形式
包含单独表达式:
parameters -> an expression
list.forEach(item -> System.out.println(item));
包含代码块:
parameters -> {expressions}
list.forEach(item -> { int numA = item.getNumA(); int numB = item.getNumB(); System.out.println(numA + numB); });
左侧指定lambda表达式需要的参数,右侧指定lambda方法体。
-
lambda表达式无法独立执行,它必须是实现一个函数式接口的唯一抽象方法。
每个lambda表达式背后必定有一个函数式接口,该表达式实现的是这个函数式接口内部的唯一抽象方法。
例如以下lambda表达式:
list.forEach(item -> System.out.println(item));
ArrayList中的foreach方法:
@Override public void forEach(Consumer<? super E> action) { // 太长了,不看了~ }
其中Consumer就是一个函数式接口:
@FunctionalInterface public interface Consumer<T> { void accept(T t); // lambda 表达式 item -> System.out.println(item) 实现了该方法 default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
Consumer接口是Java 8中预先定义的函数式接口,java.util.function包下都是些预定义的函数式接口。
function包下的部分接口使用了泛型,具有很强的通用性,在自定义函数式接口前,可以试试去这个包下找找有没有能用的。
-
在执行lambda表达式时,会自动创建一个实现了目标函数式接口的类实例,该类实例是一个匿名内部类。
同样以list的
foreach
方法为例:public class LambdaDemo { public static void main(String[] args) { List<String> list = Arrays.asList("item1", "item2"); list.forEach(item -> System.out.println(item)); } }
用Java VisualVM追踪代码运行过程中的堆内存,发现会生成以下示例:
生成的实例类名为
LambdaDemo$$Lambda$1
,根据匿名内部类的命名规则可知,这是LambdaDemo的一个匿名内部类。 -
同样,由于lambda表达式在执行时会生成目标函数式接口的类实例,因此可以做如下操作:
// 有以下函数式接口 @FunctionalInterface public interface IFuntionSum<T extends Number> { T sum(List<T> numbers); } // 将一个lambda表达式赋值给函数式接口引用(类型须兼容) IFuntionSum<Long> function = list -> { Long sum = 0L; for (Long item : list) { sum += item; } return sum; }; function.sum(Arrays.asList(1L, 2L)); // 执行结果为3L
在开发过程中,我们可以将lambda表达式等同于一个对象使用,对其声明、引用、传递。
-
匿名内部类和lambda表达式匿名内部类的命名规则
内部类的命名规则:外部类名+$+内部类名
匿名类的命名规则:外部类名+$+(1,2,3,第几个匿名类就显示几)
lambda匿名内部类的命名规则:外部类名+$ + L a m b d a + +Lambda+ +Lambda++(1,2,3,第几个lambda表达式就显示几)
假设外部类中用到了2个lambda表达式,则生成的2个匿名内部类的命名分别为:
外部类名$$Lambda$1 和 外部类名$$Lambda$2
lambda表达式规约
-
lambda表达式的参数可以通过上下文判读,如果需要显示声明一个参数的类型,则必须为所有的参数声明类型。
@FunctionalInterface public interface IFunctionMod { boolean (int n, int d); } IFunctionMod function = (n, d) -> (n % d) == 0 // 合理,n 和 d 的类型通过上下文推断 IFunctionMod function = (int n, int d) -> (n % d) == 0 // 合理,指定 n 和 d 的类型 IFunctionMod function = (int n, d) -> (n % d) == 0 // 不合理,须显示声明所有参数类型
-
lambda表达式中抛出的异常需要与目标函数式接口的抽象方法抛出的异常类型兼容:
以下是合理的:
@FunctionalInterface public interface IFunctionMod { boolean (int n, int d) throw Exception; } IFunctionMod function = (n, d) -> { if (d == 0) { // IOException是EXception 的子类,通过类型转换,IOException 可转换为 Exception throw new IOException("test"); } return n % d == 0; };
如果反一下,就不行了:
@FunctionalInterface public interface IFunctionMod { boolean (int n, int d) throw IOException; } IFunctionMod function = (n, d) -> { if (d == 0) { // 父类不能通过自动类型转换转为子类,lambda 表达式抛出的异常类型与抽象方法抛出的异常类型不兼容 throw new Exception("test"); } return n % d == 0; };
-
lambda表达式中参数类型需要与目标函数式接口中抽象方法的参数类型兼容。
从接口与实现的角度,可以很容易理解抛出异常兼容和参数类型兼容这2点。
方法引用
可以引用已有方法构造lambda表达式,例子如下:
list.forEach(System.out::print)