Java基础-多线程、Lambda表达式、Stream流

多线程

线程概述

在了解线程之前,要先理解程序和进程的概念。
程序:程序是一组指令的集合,是静态的,比如我们安装的软件,在没有运行时的状态。
进程:进程是程序动态执行的过程,如运行中的软件。程序是静态的,而进程是动态的。进程也是资源分配的基本单位,系统在运行时会为每个进程分配不同的内存区域。
线程:进程又可进一步细化为线程,一个进程至少有一个主线程。线程作为调度和执行的基本单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小。一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简便、高效,但是多个线程操作共享的系统资源可能会带来安全隐患(线程安全和线程不安全)。

Thread类实现多线程

Thread类在java.lang包下,通过继承该类并重写它的run()方法,然后调用start()方法即可开启一个线程。

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("线程2*****"+i+"*****");
        }
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        MyThread thread2 = new MyThread();
        thread2.start();//开启线程

        for (int i=0;i<100;i++){
            System.out.println("主线程-----"+i+"-----");
        }
    }
}

在这里插入图片描述
线程之间的运行是独立的,并不是执行完一个线程再去执行另一个线程。
注:main()方法是主线程。

Thread类中的一些常用方法

方法名说明
void start()启动线程,并执行对象的run()方法
void run()线程在被调度时会执行该方法
String getName()返回线程的名称
void setName(String name)设置该线程的名称
static Thread currentThread()返回当前线程
static void sleep(long millis)使当前线程在指定时间段内放弃对CPU的控制,时间到后重新排队
static void yield()线程让步(释放CPU,系统重新调度)
join()当线程A调用线程B的join()方法时,线程A将被阻塞,转而执行B线程,直至B线程执行完毕才会执行线程A
stop()强制结束当前线程的生命周期(已过时,不推荐使用)
boolean isAlive()判断当前线程是否还活着
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            if (i%10==0)
                this.yield();//当执行到10的倍数时,释放CPU
            System.out.println(this.getName()+"-----"+i);//获取当前线程名
        }
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        MyThread thread2 = new MyThread();
        thread2.setName("线程2");//为线程thread2设置线程名
        thread2.start();//开启线程

        Thread.currentThread().setName("线程1");//为主线程设置线程名
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"-----"+i);
            if (i==1)
                thread2.join();//当主线程执行到1时,让线程thread2执行完毕,然后主线程才会执行
        }
    }
}

结果就是在主线程执行到1之前,线程1和线程2是有交互的。当线程1执行到1后,线程1被阻塞,线程2将所有数打印完后,才会执行线程1.
在这里插入图片描述
在这里插入图片描述

线程的优先级

Thread类定义的一些优先级等级:

代表的数字
Thread.MAX_PRIORITY10
Thread.NORM_PRIORITY5
Thread.MIN_PRIORITY1

涉及Thread类的一些方法

方法名说明
void setPriority(int newPriority)设置线程的优先级
int getPriority()返回线程的优先级代表的数字
    public static void main(String[] args) throws InterruptedException {
        MyThread thread2 = new MyThread();
        thread2.setName("线程2");//为线程thread2设置线程名
        System.out.println("线程1的优先级:"+Thread.currentThread().getPriority());
        System.out.println("线程2的优先级:"+thread2.getPriority());

        thread2.start();//开启线程

        Thread.currentThread().setName("线程1");//为主线程设置线程名
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"-----"+i);
        }
    }

在这里插入图片描述

注:线程创建时继承父类的优先级。低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。

Runnable接口实现多线程

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

创建线程的第二种方式就是实现Runnable接口。主要步骤如下:

  1. 实现Runnable接口,实现它的run()方法。
  2. 创建Runnable接口实现类的对象。
  3. 将实现类对象作为参数传递到Thread类的构造器中,创建Thread类对象
  4. 调用Thread类对象的start()方法

测试:
(1)创建Runnable接口的实现类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

(2)创建测试类,开启多个线程

public class TestRunnable {
    public static void main(String[] args) {
        //创建实现类对象
        MyRunnable myRunnable = new MyRunnable();

        //创建Thread对象
        Thread thread1 = new Thread(myRunnable);
        Thread thread2 = new Thread(myRunnable);
        Thread thread3 = new Thread(myRunnable);

        //为线程设置线程名
        thread1.setName("线程1");
        thread2.setName("线程2");
        thread3.setName("线程3");

        //开启线程
        thread1.start();
        thread2.start();
        thread3.start();

    }
}

在这里插入图片描述
使用Runnable接口的方式创建线程,多个线程使用同一个Runnable实现类的对象,线程之间更容易共享资源,不用加static修饰就可以共享同一个变量。

public class MyRunnable implements Runnable{
    private int i = 0;//多个线程可以共享i变量
    @Override
    public void run() {
        for (;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

而通过继承Thread创建多线程,需要加static才能共享某一个变量。

public class MyThread extends Thread{
    private static int i = 0;//必须加static修饰,多个线程才能共享变量i
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(this.getName()+"-----"+i);
        }
    }

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        
        thread1.setName("线程1");
        thread2.setName("线程2");
        thread3.setName("线程3");
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

Runnable接口和Thread类的关系
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过源码可以看到,Thread类默认实现了Runnable接口。我们往Thread类里传Runnable实现类的对象,就是给变量target赋值了。

线程的生命周期

在jdk1.5之前有5种线程的状态:

  1. 新建:当一个Thread类或其子类的对象刚被创建时。
  2. 就绪:调用start()方法后,新建的线程将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没有分配到CPU资源。
  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,此时会执行run()方法内的代码。
  4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  5. 死亡:线程已完成全部的工作或被提前强制性终止或出现异常而导致结束。
    在这里插入图片描述
    在jdk1.5之后,线程的状态进行了一些细化,在Thread.State类中定义了线程的几种状态,该类定义在Thread类的内部:
    public enum State {
        NEW,

        RUNNABLE,

        BLOCKED,

        WAITING,

        TIMED_WAITING,

        TERMINATED;
    }

在这里插入图片描述

线程安全

当多个线程同时访问同一资源(变量、文件等)的时候,若多个线程只有读操作,那么是不会发生线程安全问题。但是如果多个线程对资源有读和写的操作,就容易出现线程安全问题。
如下两个代码就可能出现线程安全问题:

public class RATM implements Runnable{
    private int balance = 10000;//将要操作的资源

    @Override
    public void run() {
        if (balance>6000){
            try {
                Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                balance-=6000;
                System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //测试
    public static void main(String[] args) {
        RATM atm = new RATM();
        Thread atm1 = new Thread(atm,"atm1");
        Thread atm2 = new Thread(atm,"atm2");

        atm1.start();
        atm2.start();
    }
}
public class TATM extends Thread{
    private static int balance = 10000;//将要操作的资源

    @Override
    public void run() {
        if (balance>6000){
            try {
                Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                balance-=6000;
                System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //测试
    public static void main(String[] args) {
        TATM atm1 = new TATM();atm1.setName("atm1");
        TATM atm2 = new TATM();atm2.setName("atm2");

        atm1.start();
        atm2.start();
    }
}

它们的结果都是一样的
在这里插入图片描述
很明显,出现了线程安全的问题,出现了负值,解决的办法就是加锁,一个线程在操作共享数据时不允许别的线程操作,加锁使用synchronized关键字,有同步代码块和同步方法两种方式。

同步代码块

同步代码块格式

synchronized (同步监视器){
	//需要加锁的代码
}

同步监视器可以是任何对象,但必须保证多个线程的同步监视器是一样的。
将上述两个代码加锁,如下:

public class RATM implements Runnable{
    private int balance = 10000;//将要操作的资源

    @Override
    public void run() {
        synchronized (this){//加锁
            if (balance>6000){
                try {
                    Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                    balance-=6000;
                    System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //测试
    public static void main(String[] args) {
        RATM atm = new RATM();
        Thread atm1 = new Thread(atm,"atm1");
        Thread atm2 = new Thread(atm,"atm2");

        atm1.start();
        atm2.start();
    }
}
public class TATM extends Thread{
    private static int balance = 10000;//将要操作的资源

    @Override
    public void run() {//加锁
        synchronized (this.getClass()){
            if (balance>6000){
                try {
                    Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                    balance-=6000;
                    System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //测试
    public static void main(String[] args) {
        TATM atm1 = new TATM();atm1.setName("atm1");
        TATM atm2 = new TATM();atm2.setName("atm2");

        atm1.start();
        atm2.start();
    }
}

控制台结果如下
在这里插入图片描述

同步方法

如果操作共享数据的代码完整的生命在了一个方法中,那么我们可以将此方法声明为同步方法。声明格式如下

权限修饰符 [static] synchronized 返回值类型 方法名(方法参数){

}

注:无法写同步监视器,同步方法有默认的同步监视器,如果是普通方法,同步监视器就是this;如果是静态方法,同步监视器就是该类的class对象。

将上述两个同步代码块抽取出来为一个方法,修改如下:

public class RATM implements Runnable{
    private int balance = 10000;//将要操作的资源

    @Override
    public void run() {
        extracted();
    }

    private synchronized void extracted() {
        if (balance>6000){
            try {
                Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                balance-=6000;
                System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //测试
    public static void main(String[] args) {
        RATM atm = new RATM();
        Thread atm1 = new Thread(atm,"atm1");
        Thread atm2 = new Thread(atm,"atm2");

        atm1.start();
        atm2.start();
    }
}
public class TATM extends Thread{
    private static int balance = 10000;//将要操作的资源

    @Override
    public void run() {//加锁
        extracted();
    }
    private static synchronized void extracted() {
        if (balance>6000){
            try {
                Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                balance-=6000;
                System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //测试
    public static void main(String[] args) {
        TATM atm1 = new TATM();atm1.setName("atm1");
        TATM atm2 = new TATM();atm2.setName("atm2");

        atm1.start();
        atm2.start();
    }
}

测试效果和同步代码块一致,也能解决线程安全问题
在这里插入图片描述

synchronized的优缺点
优点:解决了线程的安全问题。
缺点:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

优化同步代码

懒汉式存在线程安全问题,如下是一个懒汉式的简单例子

public class RATMUtil {
    private static RATM instance;
    
    public static RATM getInstance(){
        if (instance==null)
            instance=new RATM();
        return instance;
    }
}

如果多个线程同时调用这个工具类,返回的instance实例可能会不同,解决的方法很简单,加锁

public class RATMUtil {
    private static RATM instance;

    public static RATM getInstance(){
        synchronized (RATMUtil.class){
            if (instance==null)
                instance=new RATM();
            return instance;
        }
    }
}

但是这样会有一个问题,instance不等于null时,多个线程还是会抢锁,同一时刻还是只能有一个线程调用该方法,所以可以将代码优化一下,如下:

public class RATMUtil {
    private static RATM instance;

    public static RATM getInstance(){
        if (instance==null){
            synchronized (RATMUtil.class){
                if (instance==null)
                    instance=new RATM();
            }
        }
        return instance;
    }
}

注意:使用两个if判断是很有必要的。

看样子已经优化的很好了,但是这样还有可能出现问题,instance在实例化时会有很多步骤,在其中一个步骤,instance已经不为null,但还没有初始化完成,这时instance可能被别的线程拿到而造成风险,解决办法就是给变量加一个关键字volatile。

public class RATMUtil {
    private static volatile RATM instance;

    public static RATM getInstance(){
        if (instance==null){
            synchronized (RATMUtil.class){
                if (instance==null)
                    instance=new RATM();
            }
        }
        return instance;
    }
}

死锁

不同线程分别占用对方需要的资源不放弃,都在等对方放弃自己需要的资源,就形成了线程的死锁。

诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(不可抢占)
  • 循环等待

以上4个条件同时出现就会触发死锁。

如何避免死锁
打破上述4个条件的其中一个就可以避免死锁,如下是一些方法:

  • 互斥条件基本无法破坏,因为线程需要通过互斥解决安全问题。
  • 考虑一次性申请所有需要的资源,这样就不存在等待问题
  • 占用部分资源的线程在进一步申请其它资源时,如果申请不到,就主动释放已经占用的资源
  • 可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题

Lock锁

除了使用同步代码的方式加锁,还可以使用Lock锁,Lock是jdk1.5推出的一个接口,使用接口的方式更加的灵活,本节主要讲它其中的一个实现类ReentrantLock的用法,主要使用两个方法进行加锁和解锁。

方法描述
lock()加锁
unlock()解锁

步骤:

  1. 创建Lock的实例,需要确保多个线程共用同一个Lock实例
  2. 执行lock()方法,锁定对共享资源的调用
  3. unlock()的调用,释放对共享资源的锁定

将之前使用synchronized加锁的代码使用Lock锁改写一下

public class RATM implements Runnable{
    private int balance = 10000;//将要操作的资源
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();//上锁
        try {
            if (balance>6000){
                try {
                    Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                    balance-=6000;
                    System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            lock.unlock();//解锁
        }
    }


    //测试
    public static void main(String[] args) {
        RATM atm = new RATM();
        Thread atm1 = new Thread(atm,"atm1");
        Thread atm2 = new Thread(atm,"atm2");

        atm1.start();
        atm2.start();
    }
}
public class TATM extends Thread{
    private static int balance = 10000;//将要操作的资源
    private static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();//上锁
        try {
            if (balance>6000){
                try {
                    Thread.sleep(10);//睡眠一段时间,增大线程出现问题的概率
                    balance-=6000;
                    System.out.println("线程"+Thread.currentThread().getName()+"取出6000元,当前余额:"+balance);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            lock.unlock();//解锁
        }

    }

    //测试
    public static void main(String[] args) {
        TATM atm1 = new TATM();atm1.setName("atm1");
        TATM atm2 = new TATM();atm2.setName("atm2");

        atm1.start();
        atm2.start();
    }
}

注:Lock接口有很多的实现类,我们测试使用的是其中一个实现类ReentrantLock,本节不做深入讨论,在学习JUC时会深入了解实现类之间的不同。

synchronized与Lock的对比:

  • synchronized不管是同步代码块还是同步方法,都需要结束在一对{}之后,释放对同步监视器的调用
  • Lock是通过对两个方法,控制需要被同步的代码,更灵活一些
  • Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高

线程通信

当我们需要多个线程来共同完成一件任务,并且我们希望他们它们有规律的执行,那么多线程之间就需要一些通信机制,可以协调它们的工作。

线程通信需要涉及Object类中的几个方法:

方法名说明
wait()线程不再活动,不再参与调度,也不会去竞争锁了,进入等待状态,等待其他线程执行notify()或notifyAll()来唤醒
wait(long timeout)线程不再活动,等待唤醒或到达指定时间后会自动唤醒
notify()唤醒优先级较高的一个线程,如果优先级都相同则会随机唤醒一个线程 ,被唤醒的线程会从当初wait()的位置继续执行
notifyAll()唤醒所有进入等待状态的线程

注意点

  • 这几个方法必须是在同步代码块或同步方法中使用
  • 这三个方法的调用者必须是同步监视器,否则会报异常
  • 这三个方法声明在Object类中

接下来使用一个生产者与消费者的例子:
(1)创建产品类

public class Product {
    private int productNum = 50;//产品,初始状态为50,最大不能超过100,最小不能小于0;

    public synchronized void increaseProduct(){//增加产品
        if (productNum<100){
            productNum++;
            System.out.println("生产了第"+productNum+"号产品");
            notifyAll();//生产产品后就可以唤醒消费者
        }else {
            try {
                wait();//生产数量满后就进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void reduceProduct(){//减少产品
        if (productNum>0){
            System.out.println("消费了第"+productNum+"号产品");
            productNum--;
            notifyAll();//消费者消费产品后就可以唤醒生产者生产产品
        }else {
            try {
                wait();//如果没有产品了,消费者就会休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

(2)创建生产者

public class Producer implements Runnable{
    private Product p;
    public Producer(Product product){
        this.p=product;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(50);//假设每50毫秒生产一个
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            p.increaseProduct();
        }
    }

}

(3)创建消费者

public class Consumer implements Runnable{
    private Product p;
    public Consumer(Product product){
        this.p=product;
    }
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(25);//假设每25毫秒消费一个
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            p.reduceProduct();
        }

    }
}

(4)创建测试类

public class TestProduAndConsu {
    public static void main(String[] args) {
        Product product = new Product();
        Consumer consumer = new Consumer(product);
        Producer producer = new Producer(product);

        //开两个消费者线程
        Thread t1 = new Thread(consumer);
        Thread t2 = new Thread(consumer);
        //开两个生产者线程
        Thread t3 = new Thread(producer);
        Thread t4 = new Thread(producer);
        t1.start();t2.start();t3.start();t4.start();
    }
}

在这里插入图片描述
注:调用wait()和notify()方法时要使用同步监视器调用,比如本节中同步监视器就是this,调用本类的方法可以省略this。

wait()与sleep()的区别:

  • wait()一旦执行,会释放同步监视器,而sleep()不会。
  • wait()可以被唤醒,而sleep()不行,必须到达指定时间才会结束阻塞。

Callable接口实现多线程

除了使用Thread类和Runnable接口实现多线程外,还可以使用Callable接口实现多线程,该接口是jdk1.5新增的。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

该接口是一个泛型接口。该接口与Runnable接口相比可以有返回值,并且可以通过throws处理异常,相比较起来更加灵活

步骤:

  1. 创建Callable接口的实现类
  2. 创建FutureTask类,将Callable接口的实现类传入该类中
  3. 创建Thread类,将FutureTask类的实例传入Thread类中(FutureTask实现了Runnable接口)
  4. 调用start()方法开启线程
  5. 使用FutureTask的get()方法获取callable接口的返回值
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i=1;i<=100;i++){
            System.out.println(i);
            sum+=i;
            Thread.sleep(10);//增加一个时间,观察主线程阻塞状态
        }
        return sum;
    }
}
public class TestCallable {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();//第一步
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);//第二步
        Thread thread = new Thread(futureTask);//第三步
        thread.start();//第四步,开启线程

        try {
            Integer sum = futureTask.get();//获取返回值,此时主线程(main)是阻塞状态,直至获得callable接口的返回值
            System.out.println("总和:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在这里插入图片描述
直到分线程执行完所有代码并返回结果时,主线程才会继续执行。

缺点:
如果在主线程中需要获取分线程call()的返回值,则此时主线程是阻塞状态的。

线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。这就引入了线程池的,提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。
在这里插入图片描述
使用线程池的优点:

  1. 提高程序执行的效率。(因为线程已经提前创建好)
  2. 提高资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其它的任务)
  3. 可以设置相关的参数,对线程池中的线程使用进行管理

步骤:

  1. 创建线程池,设置相关参数
  2. 执行任务
  3. 关闭线程池
public class MyRunnable implements Runnable{
    private int i = 0;
    @Override
    public void run() {
        for (;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i=1;i<=10;i++){
            System.out.println(i);
            sum+=i;
        }
        return sum;
    }
}

测试

public class TestThreadPool {
    public static void main(String[] args) {
        //创建线程池,设置相关参数
        ThreadPoolExecutor pool = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);//指定初始的线程数量
        pool.setMaximumPoolSize(8);//设置线程池最大线程数

        //执行线程任务
        pool.execute(new MyRunnable());//execute()方法适合执行Runnable接口的任务
        Future<Integer> submit = pool.submit(new MyCallable());//submit()方法适合执行Callable接口的任务

        try {
            Integer sum = submit.get();//获取Callable接口的返回值
            System.out.println("总和:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            pool.shutdown();//关闭线程池
        }
    }
}

在这里插入图片描述

Lambda表达式

函数式接口

函数式接口也是接口的一种,它是一种特殊的接口,有如下特点:

  • 接口内只有一个抽象方法
  • 使用@FunctionalInterface注解标注(非必须)

举例:

@FunctionalInterface
public interface MyFunctionInterface {
    void say(String message);
}

lambda表达式语法

(参数列表) -> {
	方法体
}

如何new一个函数式接口的实例呢,有三种方式:

  1. 创建一个接口的实现类,然后new实现类
  2. 直接new接口,实现方法
  3. 第三种就是使用lambda表达式

举例:

@FunctionalInterface
public interface MyFunctionInterface {
    void say(String message);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        
        MyFunctionInterface f = (message) -> {
            System.out.println(message);
        };
        f.say("你好");
    }
}

在这里插入图片描述
注:

  • 小括号内为参数列表,接口方法有几个参数就要写几个参数,参数列表中可以省略参数类型,只写参数名
  • 大括号内就是方法体,里面就写方法的实现代码

无返回值无参数

@FunctionalInterface
public interface MyFunctionInterface {
    void say();
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {

        MyFunctionInterface f = () -> System.out.println("你好");
        f.say();
    }
}

当接口方法没有参数时,参数列表内也不写任何参数。
注:如果方法体内就只有一条语句,可以省略大括号。

无返回值单参数

@FunctionalInterface
public interface MyFunctionInterface {
    void say(String message);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {

        MyFunctionInterface f = message -> System.out.println(message);
        f.say("你好");
    }
}

如果接口方法只有一个参数,可以省略形参列表的小括号。

无返回值多参数

@FunctionalInterface
public interface MyFunctionInterface {
    void say(String message1,String message2);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {

        MyFunctionInterface f = (message1,message2) -> {
            System.out.println(message1);
            System.out.println(message2);
        };
        f.say("你好","张三");
    }
}

如果有多个参数,参数列表的小括号就不可以省略了。

有返回值无参数

@FunctionalInterface
public interface MyFunctionInterface {
    String say();
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
/*
        MyFunctionInterface f = () -> {
            return "你好";
        };
*/
        MyFunctionInterface f = () -> "你好";        
        System.out.println(f.say());
    }
}

如果语句只有一条,大括号和return都可以省略。

有返回值单参数

@FunctionalInterface
public interface MyFunctionInterface {
    String say(String message);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
/*
        MyFunctionInterface f = (message) -> {
            return message;
        };
*/
        MyFunctionInterface f = message -> message;
        System.out.println(f.say("你好"));
    }
}

同样,只有一个参数可以省略小括号,并且,如果方法体也只有一条语句,则可以省略大括号和return。

有返回值多参数

@FunctionalInterface
public interface MyFunctionInterface {
    String say(String message1,String message2);
}

public class FunctionInterfaceTest {
    public static void main(String[] args) {

        MyFunctionInterface f = (message1,message2) -> {
            System.out.println(message1);
            System.out.println(message2);
            return "我很好";
        };
        System.out.println(f.say("你好","张三"));
    }
}

常用函数式接口

在java.util.function包下定义了很多函数式接口,如下介绍一些最常用的

消费型接口

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
}

消费一个T类型的数据

生产型接口

@FunctionalInterface
public interface Supplier<T> {

    T get();
}

函数型接口

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);
}

判断型接口

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}

方法引用与构造器引用

方法引用

方法引用可以看作时lambda表达式的进一步简化,使用方法引用或构造器引用时必定可以使用lambda。
也就是说,满足特殊情况下的lambda表达式可以使用方法引用或构造器引用替换。

格式:

(或对象)::方法名

举例:

public class FunctionInterfaceTest {
    public static void main(String[] args) {
        //使用lambda
        Consumer<String> c1 = (message) -> System.out.println(message);

        //使用方法引用
        Consumer<String> c2 = System.out::println;

        c1.accept("111");
        c2.accept("222");

    }
}

观察Consumer接口和println方法

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
}
    public void println(String x) {
    	...
    }

可以看到,这两个方法的返回值和形参列表是相同的,所以我们可以用println方法代替accept方法。

总结:当函数式接口的方法的形参列表、返回值与某一个类中的方法相同时,可以用方法引用的方式作为函数式接口的实现。

方法引用有如下三种方式:
类名::静态方法名
对象名::方法名

类名::实例方法名
观察如下代码:

@FunctionalInterface
public interface BiPredicate<T, U> {

    boolean test(T t, U u);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        //使用lambda
        BiPredicate<String,String> b1 = (s1,s2) -> s1.equals(s2);

        //使用方法引用
        BiPredicate<String,String> b2 = String::equals;

        System.out.println( b1.test("111","111"));
        System.out.println(b2.test("222","222"));

    }
}

其本质还是使用了”对象::方法“,只不过引用的方法是参数对象的方法。在本例中,调用的equals方法实际是参数s1的。

满足如下关系才能使用”类名::实例方法“方式:

  1. 函数式接口的方法返回值与引用方法返回值相同
  2. 函数式接口的方法参数为n个时,引用方法的参数应为n-1个
  3. 函数式接口的方法至少有一个参数
  4. 函数式接口的方法中第一个参数作为引用方法的调用者
  5. 函数式接口方法的后n-1个参数类型与引用的方法参数类型相同

构造器引用

格式:

类名::new

调用无参构造

public class Person {
    public Person(){}
    public Person(String name){
        this.name=name;
    }
    private String name;
}
@FunctionalInterface
public interface PersonInterface {
    Person getPerson();
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        //使用lambda
        PersonInterface p1 = () -> new Person();
        //构造器引用
        PersonInterface p2 = Person::new;

    }
}

调用有参构造

@FunctionalInterface
public interface PersonInterface {
    Person getPerson(String personName);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        //使用lambda
        PersonInterface p1 = name -> new Person(name);
        //构造器引用
        PersonInterface p2 = Person::new;

    }
}

总结:函数式接口的方法参数列表类型要和构造器参数一致。

数组引用

@FunctionalInterface
public interface PersonInterface {
    Person[] getPersons(int arrayLength);
}
public class FunctionInterfaceTest {
    public static void main(String[] args) {
        //使用lambda
        PersonInterface p1 = name -> new Person[5];
        //数组引用
        PersonInterface p2 = Person[]::new;

    }
}

参数为数组的长度

Stream流

使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。Stream API提供了一种高校且易于使用的处理数据的方式。

Stream的一些说明

  1. Stream自己不会存储元素
  2. Stream不会改变源对象。相反,它们会返回一个持有结果的新Stream
  3. Stream操作是延迟执行的。这意味着它们会等到需要结果的时候才执行。即一旦执行终止操作,才会执行中间的操作,并产生结果
  4. Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

步骤:

  1. 获取Stream实例
  2. 一系列中间操作
  3. 终止操作

获取Stream流对象

通过集合获取
调用集合的stream()方法或获得并行流parellelStream()
这两个方法定义在Collection接口中,所以只要实现了该接口的集合都可以获得它的Stream实例。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> l = new ArrayList<>();
        l.add(1);l.add(2);
        //获得Stream流
        Stream<Integer> stream = l.stream();
        Stream<Integer> integerStream = l.parallelStream();
    }
}

通过数组获取
使用Arrays.stream()方法可以获得数组的Stream实例。

public class StreamTest {
    public static void main(String[] args) {
        Integer[] i = new Integer[]{1,2};
        int[] i2 = new int[]{1,2,3};
        //获得Stream流
        Stream<Integer> stream = Arrays.stream(i);
        
        //如果是基本类型的数组,也会获得Stream流,不过类型不同(IntSteam、LongStream、DoubleStream),但是方法都一样
        IntStream stream1 = Arrays.stream(i2);
    }
}

通过调用Stream.of()

public static<T> Stream<T> of(T t){
	...
}
public static<T> Stream<T> of(T... values) {
	return Arrays.stream(values);//本质还是调用了Arrays.stream()方法
}

举例:

public class StreamTest {
    public static void main(String[] args) {
        //获得Stream流
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    }
}

Stream流中间操作

多个中间操作可以连接起来形成一个流水线,除非流水线触发终止操作,否则中间操作不会执行任何处理!而在终止操作时一次性全部处理,称为”惰性求值“。
中间操作就是调用方法。

筛选与切片

方法说明
Stream<T> filter(Predicate<? super T> predicate)从流中排除某些元素
Stream<T> distinct()去除重复元素。使用hashCode()和equals()方法去除重复元素
Stream<T> limit(long maxSize)获取指定数量的新流(从头开始)
Stream<T> skip(long n)去除前n个元素,若流中数量不足n个,则返回一个空流。与limit()互补

映射

方法说明
<R> Stream<R> map(Function<? super T, ? extends R> mapper)接受一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素
IntStream mapToInt(ToIntFunction<? super T> mapper)接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的IntStream
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream
LongStream mapToLong(ToLongFunction<? super T> mapper)接受一个函数作为参数,该函数会被应用到每个元素上,产生一个新的LongStream
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)接受一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流

排序

方法说明
Stream<T> sorted()按自然排序
Stream<T> sorted(Comparator<? super T> comparator)按比较器排序

注:使用第一个排序方法时一定要实现Comparable接口的类才能比较

Stream流终止操作

匹配与查找

方法说明
boolean allMatch(Predicate<? super T> predicate)检查是否匹配所有元素
boolean anyMatch(Predicate<? super T> predicate)检查是否至少匹配一个元素
boolean noneMatch(Predicate<? super T> predicate)检查是否没有匹配所有元素
Optional<T> findFirst()返回第一个元素
Optional<T> findAny()返回当前流中的任意元素
long count()返回流中的元素总数
Optional<T> max(Comparator<? super T> comparator)返回流中的最大值
Optional<T> min(Comparator<? super T> comparator)返回流中的最小值
void forEach(Consumer<? super T> action)接受一个函数,迭代每个元素

归约

方法说明
Optional<T> reduce(BinaryOperator<T> accumulator)将流中元素反复结合起来,得到一个值
T reduce(T identity, BinaryOperator<T> accumulator)将流中元素反复结合起来,得到一个值,identity也会参与结合

收集

方法说明
<R, A> R collect(Collector<? super T, A, R> collector)将流转换为其它形式。接收一个Collector接口的实现

注:如果想将处理好的流转换成List、Set集合,可以使用Collectors类中提供好的一些方法。

方法说明
toList()转换成list集合
toSet()转换成set集合

举例:

public class StreamTest {
    public static void main(String[] args) {
        //获得Stream流
        Stream<Integer> stream = Stream.of(1,1,2,3,4,5);
        
        List<Integer> collect = stream.distinct().collect(Collectors.toList());
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值