Java 多线程基础(黑马视频笔记)

文章目录

    • 线程实现方式
      • 线程的基本概念
      • 多线程的创建
        • Thread 类
        • Runnable 接口
        • 匿名内部类方式
    • 线程同步机制(线程安全问题)
      • 同步代码块
      • 同步方法
      • 静态同步方法
      • Lock 锁
    • 线程状态
      • Timed Waiting(计时等待)
      • BLOCKED(锁阻塞)
      • Waiting (无限等待)
    • 等待唤醒机制
    • 线程池
    • Lambda 表达式

线程实现方式

线程的基本概念

我们先学习两组概念。首先是并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生
  • 并行:指两个或多个事件在同一时刻发生(同时发生)

其次,进程和线程的区别是:

  • 进程:一个内存中运行的一个应用程序就是一个进程。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程。进程也是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序,就是一个进程从创建,到运行,消亡的过程

    所有的应用程序都需要进到内存中执行(临时存储RAM)。进入到内存的程序叫进程。结束进程就是将进程从内存中清除了

  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行。一个进程至少有一个线程,一个进程中可以有多个线程,这样的应用程序称为多线程程序,多线程之间互不影响

线程一般有两种调度方式:

  1. 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
  2. 抢占式调度:优先让优先级高的线程使用CPU,若线程优先级相同,则随机选择一个

Java中多线程采用抢占式调度。

多线程的创建

创建新执行线程有两种方法:

  1. 将类声明为 Thread 的子类,该子类重写 Thread 的 run() 方法,分配并启动该子类的实例
  2. 声明实现 Runnable 接口的类,该类实现 run() 方法,然后可以分配该类的实例,在创建线程时作为一个参数传递并启动

Thread 类

Java 使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

Java 中通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 创建一个 Thread 类的子类
  2. 在 Thread 类的子类中重写 Thread 的 run() 方法,设置线程的任务(开启线程要做什么)
  3. 创建 Thread 类的子类对象
  4. 调用 Thread 类中的方法 start() 方法,开启新的线程,执行 run() 方法
    其中,void start():使该线程开始执行,Java 虚拟机调用该线程的 run() 方法。结果是两个线程并发运行,当前线程(主线程)和另一个线程(创建的新线程,执行其run方法)
    注意:多次启动一个线程是非法的。特别是当线程已经执行完毕后,不能再重新启动。
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run:" + i);
        }
    }
}

/*创建多线程的第一种方式:创建Thread类的子类*/
public class Main {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("main:" + i);
        }
    }
}

输出结果是随机抢占的:

main:0
main:1
main:2
run:0
main:3
run:1
main:4
run:2
main:5
main:6
run:3
main:7
run:4
......

这里再理解一下多线程的原理:
当JVM执行main方法时,会开辟一条main方法通向CPU的路径,这个路径叫做main线程,主线程。CPU通过这个线程执行main方法
当我们new了一个Thread类的子类对象并调用run方法后,JVM会再开辟一条通向CPU的新线程(路径),用来执行run方法
对于CPU而言,就有了两条线程,两个线程会一起抢夺CPU的执行权,CPU会随意选择一条路径执行,我们无法控制CPU

上述代码中,多线程的内存图解如下所示:
由图我们可以看出,当创建了一个Thread类的子类对象并调用run方法后,会在内存中开辟一个新的栈空间,用来执行该run方法。
多个线程在不同的栈空间中执行,因此多个线程之间互不影响。
Java 多线程基础(黑马视频笔记)_第1张图片
Thread 类的常用方法

  1. 获取当前线程的名称
  • 使用 Thread 类中的 getName() 方法
    public String getName():获取当前线程的名称
  • 先获取到当前正在执行的线程,使用线程中的方法 getName()获取线程名称
    static Thread currentThraed():返回对当前正在执行的线程对象的引用
public class MyThread extends Thread{
    @Override
    public void run() {
        //方法一:使用Thread类中的getName()方法
        String name = getName();
        System.out.println(name);

        //方法二:使用静态方法currentThread()获取到当前正在执行的线程,使用线程中的方法getName()获取线程名称
        Thread th = Thread.currentThread();
        System.out.println(th);
    }
}
public class ThreadGetName {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start(); //Output:Thread-0
        new MyThread().start(); //Output:Thread-1
    }
}

输出结果:

Thread-0
Thread-1
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
  1. 设置线程名称
  • 使用 Thread 中的方法 setName()
    void setName(String name):改变线程名称,使之与参数 name 相同
  • 创建一个带参数的构造方法,参数传递线程的名称,调用父类的带参构造方法,将线程名称传递给父类,让父类 Thread 给自线程设置名称
    Thread(String name):分配新的 Thread 对象
public class MyThread extends Thread{
    public MyThread() {

    }
    //方法二:创建带参构造方法,参数传递线程的名称,调用父类带参构造方法,将线程名称传递给父类
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        //获取线程名称
        System.out.println(Thread.currentThread().getName());
    }
}

public class ThreadSetName {
    public static void main(String[] args) {
        //方法一:使用Thread中的方法setName()
        MyThread mt = new MyThread();
        mt.setName("小明");
        mt.start();

        new MyThread("旺财").start();
    }
}
  1. sleep() 方法
    public static void sleep(long millis):使当前正在执行的程序以指定的毫秒数暂停(暂时停止执行)。毫秒数结束后线程继续执行
public class Main {
    public static void main(String[] args) {
        //模拟秒表
        for (int i=1; i<=60; i++) {
            System.out.println(i);
            //使用Thread类的sleep()方法让程序睡眠一秒
            try {
                Thread.sleep(1000); //Thread.sleep()方法本身存在异常,所以需要进行异常处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//程序的运行结果为每隔一秒输出一个i

Runnable 接口

创建多线程程序的第二种方法:实现 Runnable 接口

java.lang.Runnable:
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个 run 的无参构造方法

java.lang.Thread 类中的构造方法:
Thread(Runnable target):分配新的 Thread 对象
Thread(Runnable target, String name):分配新的 Thread 对象

创建多线程步骤:

  1. 创建一个 Runnable 接口的实现类
  2. 在实现类中重写 Runnable 接口的 run() 方法,设置线程任务
  3. 创建一个 Runnable 接口的实现类对象
  4. 创建 Thread 类对象,构造方法中传递Runnable 接口的实现类对象
  5. 调用 Thread 类中的 start() 方法,开启新的线程,执行 run() 方法
//1.创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable {
    //2.在实现类中重写Runnable接口的run()方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        //3.创建一个Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t = new Thread(run);
        //5.调用Thread类中的start()方法,开启新的线程,执行run()方法
        t.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
/*输出结果为:
main--->0
main--->1
Thread-0--->0
Thread-0--->1
Thread-0--->2
main--->2
Thread-0--->3
......
*/

以上两种创建多线程的方式,有什么区别:
采用 Runnable 接口创建多线程的好处:

  1. 避免了单继承的局限性。一个类只能继承一个父类,若类继承了 Thread 类,就不能继承其他类。但实现 Rannable 接口,依然可以继承其他类,实现其他接口。
  2. 增强了程序的扩展性,降低了程序的耦合性(解耦)。实现 Runnable 接口的方式,将设置线程任务和
    开启新线程进行了分离(解耦)。创建不同的实例,实现不同的线程任务。

匿名内部类方式

匿名内部类的作用:简化代码
格式:

new 父类/接口 () {
	//重写父类/接口方法
}
public class InnerClassThread {
    public static void main(String[] args) {
        //Thread方法
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        }.start();
        
        //Runnable方法
        //Runnable r = new RunnableImpl();    //多态
        Runnable r = new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        };
        new Thread(r).start();
        
        //简化接口的方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        }).start();
    }
}

线程同步机制(线程安全问题)

当多线程访问同一资源时,且多个线程对资源有写的操作,则容易产生线程安全问题。

例:举例模拟电影票三个窗口,同时售卖1-100号的电影票

/*如电影院中,多个窗口同时卖1-100号的电影票*/
public class RunnableImpl implements Runnable{
    //多个线程共享票源
    private int ticket = 100;
    @Override
    public void run() {
        while(true) {   //重复卖票
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

在上述案例中,三个线程同时对共享数据 ticket 进行读写操作,最终的输出结果显示,出现了重复票源及非法票源,即出现了线程安全问题:

//上述案例输出结果:
Thread-0正在卖第100张票
Thread-1正在卖第100张票
Thread-2正在卖第100张票
Thread-2正在卖第97张票
Thread-1正在卖第97张票
Thread-0正在卖第96张票
...
Thread-0正在卖第1张票
Thread-2正在卖第0张票
Thread-1正在卖第-1张票

Java中提供了同步机制:synchronized
当某个线程修改共享资源时,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作。保证了数据的同步性,解决线程不安全的问题。

有三种方法完成同步操作:

  • 同步代码块
  • 同步方法
  • 锁机制

同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区域的资源进行互斥访问。

格式:
synchronized (同步锁对象) {
//需要同步操作的代码
}

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  1. 锁对象可以是任意类型
  2. 要保证多个线程使用的锁对象相同

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程只能在外等着(BLOCKED)

如上述卖票案例,修改 run() 方法如下,即可解决线程安全问题:

/*第一种方法:采用同步代码块*/
public class RunnableImpl implements Runnable{
    private int ticket = 100;   //多个线程共享票源
    //创建一个锁对象
    Object obj = new Object();
    @Override
    public void run() {
        while(true) {
            synchronized (obj) {
                if(ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}
public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

原理:
线程t0获取锁对象,进入同步代码块执行
线程t1发现没有锁对象,就会进入阻塞状态,等待t0线程归还锁对象
同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁,无法进入同步代码块

上述方法的不足之处是:程序需要频繁的判断锁,获取锁,释放锁,程序的效率会降低

同步方法

使用 synchronized 修饰的方法为同步方法。保证某线程执行该方法时,其他线程只能在方法外等候。

格式:
public synchronized void method() {
//可能会产生线程安全问题的代码
}

如上述卖票案例,定义一个同步方法,即可解决线程安全问题:

/*第二种方法:采用同步方法*/
public class RunnableImpl implements Runnable{
    private int ticket = 100;   //多个线程共享票源
    @Override
    public void run() {
        System.out.println("this:" + this); //this:com.example.demo.Demo03Thread.Demo05.RunnableImpl@33c7353a
        while(true) {
            playTicket();
        }
    }

    //定义一个同步方法
    public synchronized void playTicket(){
        if(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}
public class Main {
    //模拟卖票,三个线程同时开启,对共享票进行出售
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:" + run);   //run:com.example.demo.Demo03Thread.Demo05.RunnableImpl@33c7353a
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

通过上述代码中的输出测试可以得到验证:
同步方法的锁对象,其实就是实现类对象(即上述的new RunnableImpl()),也就是this。

静态同步方法

静态同步方法的锁对象不能是 this,this 是创建对象后产生的,但静态方法优先于对象。
静态同步方法的锁对象是本类的 class 属性,也是 class 文件对象(反射)

public class RunnableImpl implements Runnable{
    private static int ticket = 100;
    @Override
    public void run() {
        while(true) {
            playTicket();
        }
    }

    public static /*synchronized*/ void playTicket(){
        /*这种写法会产生错误。因为 this 是在静态方法之后产生的*/
        //synchronized (this) {
        synchronized (RunnableImpl.class){
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

Lock 锁

java.util.concurrent.locks.Lock 接口,提供了比 synchronized 代码块和 synchronized 方法更广泛的锁定操作。

Lock 锁也称为同步锁,Lock 中的方法:
void lock():获取锁
void unlock():释放锁
java.util.concurrent.locks.ReentrantLock implements Lock 接口

Lock 锁的使用步骤:

  1. 在成员位置创建一个 ReentrantLock 对象
  2. 在可能会出现安全问题的代码前调用 Lock 接口的 lock() 方法获取锁
  3. 在可能会出现安全问题的代码后调用 Lock 接口的 unlock() 方法释放锁

如下代码示例,建议将 unlock() 放入 finally 中,这样无论程序是否发生异常,都会释放锁对象,提高程序效率。

/*第三种方法:采用Lock锁*/
public class RunnableImpl implements Runnable{
    private static int ticket = 100;
    //1. 在成员位置创建一个 ReentrantLock 对象
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        //2. 在可能会出现安全问题的代码前调用 Lock 接口的 lock() 方法获取锁
        l.lock();
        while(true) {
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //3. 在可能会出现安全问题的代码后调用 Lock 接口的 unlock() 方法释放锁
                    l.unlock();
                }
            }
        }
    }
}
public class Main {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}

线程状态

java.lang.Thread.State 中给出了六种线程状态

线程的状态图如下:
Java 多线程基础(黑马视频笔记)_第2张图片
其中,阻塞状态指:具有CPU的执行资格,等待CPU空闲时执行
休眠状态指:放弃CPU的执行权,CPU空闲也不执行

Timed Waiting(计时等待)

在之前的案例中,我们在 run() 方法中通过 sleep(),强制当前正在运行的线程休眠(暂停执行),以“减慢进程”。
当调用 sleep() 方法时,当前执行的线程就会进入休眠状态,也就是所谓的 Timed Waiting(计时等待)。

注意:

  • 进入Timed Waiting 状态的一种常见情况是调用 sleep() 方法,单独的线程也可以调用

  • 为了让其他线程有机会执行,可以将 Thread.sleep() 的调用放 run() 之内。这样才能保证该线程在执行过程中睡眠

  • sleep 与锁无关,线程睡眠到期自然苏醒,并返回到 Runnable(可运行)状态

    Java 多线程基础(黑马视频笔记)_第3张图片

BLOCKED(锁阻塞)

如线程A和线程B使用同一锁,如果线程A获取到锁,线程A进入到 Runnable 状态,那么线程B就会进入到Blocked锁阻塞状态
这是由 Runnable 状态进入Blocked状态,除此之外,Waiting 和 Time Waiting 状态也会在某种情况下进入阻塞状态。

Java 多线程基础(黑马视频笔记)_第4张图片

Waiting (无限等待)

Object 中的方法:
void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void notify():唤醒在此对象监视器上等待的单个线程。会继续执行 wait() 方法之后的代码

/*等待唤醒案例:线程之间的通信*/
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证锁对象唯一
        Object obj = new Object();
        //创建顾客线程
        new Thread(){
            @Override
            public void run() {
                while(true) {
                    //保证等待和唤醒的线程只能有一个执行,所以需要使用同步技术
                    synchronized (obj) {
                        System.out.println("告诉老板需要的包子数量和种类");
                        try {
                            obj.wait(); //调用wait()方法,放弃cpu的执行,进入到waiting状态(无限等待)
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("包子已经做好了,开吃");
                        System.out.println("====================");
                    }
                }
            }
        }.start();
        //创建老板线程
        new Thread(){
            @Override
            public void run() {
                while(true) {
                    //花了5s做包子
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj) {
                        System.out.println("老板5s之后做好的包子,并告知顾客");
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}
//告诉老板需要的包子数量和种类
//老板5s之后做好的包子,并告知顾客
//包子已经做好了,开吃
//====================
//告诉老板需要的包子数量和种类
//老板5s之后做好的包子,并告知顾客
//包子已经做好了,开吃
//====================
//......

wait 有一个带参数的方法 wait(long m),线程进入到 TimeWaiting(计时等待)有两种方法:

  1. 使用 sleep(long m) 方法,在毫秒值结束之后,线程睡醒进入到 Runnable/Blocked 状态
  2. 使用 wait(long m) 方法,wait 方法如果在毫秒值结束之后还没有被 notify 唤醒,就会自动醒来,线程睡醒进入到 Runnable/Blocked 状态

唤醒除了 notify() 方法之外,还有方法 notifyAll() 方法,若有多个等待线程,notify() 只能随机唤醒其中的一个线程,notifyAll() 可以唤醒所有等待中的线程

等待唤醒机制

多个线程并发执行时,在默认情况下CPU是随机切换线程的,若需要多个线程共同完成某一任务,并且有规律执行,则需要多线程之间协调通信,以达到多线程共同操作同一份共享数据,避免对同一共享数据的争夺 。
我们需要一定手段使各个线程有效利用资源——等待唤醒机制。

等待唤醒机制常用到的三种方法,即上面学到的三种:

  • wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费CPU资源,也不会竞争锁,这时线程的状态为 WAITING,需要 notify 在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列中
  • notify
  • notifyAll

即使只通知了一个等待的线程,被通知的线程也不能立刻恢复执行,因为此刻它已经不持有锁,需要再次尝试获取锁,成功后才能在当初调用 wait 方法之后的地方恢复执行

注意:

  1. wait() 和 notify() 必须要由同一个锁对象调用。对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的wait方法后的线程
  2. wait() 和 notify() 是属于 Object 类的方法的,因此锁对象可以是任意对象,任意对象的所属类都是继承 Object 类
  3. wait() 和 notify() 必须在同步代码块或者同步函数中使用。因为必须通过锁对象调用这两个方法

下面是一个店家卖包子,顾客吃包子的实例,实现了等待唤醒机制:

//包子类
public class BaoZi {
    String pi;  //包子皮属性
    String xian;    //包子馅属性
    boolean flag = false;   //包子的状态,有包子true和没有包子false
}

//包子铺类。包子铺线程和包子线程之间的通信是互斥关系,采用同步技术,保证两个线程同时只有一个执行。
//锁对象必须唯一。可采用包子对象作为锁对象
public class BaoZiPu extends Thread{
    private BaoZi bz;   //创建包子对象,作为锁对象

    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }
    //设置线程任务,制作包子
    @Override
    public void run() {
        int count = 0;
        while(true) {
            synchronized (bz) {
                if(bz.flag == true) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒之后执行,包子铺生产包子
                if(count%2 == 0) {
                    bz.pi = "薄皮";
                    bz.xian = "三鲜";
                } else {
                    bz.pi = "冰皮";
                    bz.xian = "牛肉";
                }
                count++;
                System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
                try {
                    Thread.sleep(3000); //生产包子需要三秒钟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                bz.flag = true; //唤醒顾客线程,让顾客吃包子
                bz.notify();
                System.out.println("包子铺已经生产好了" + bz.pi + bz.xian + "包子,顾客可以开吃了");
            }
        }
    }
}

//顾客类
public class GuKe extends Thread{
    private BaoZi bz;   //创建包子对象,作为锁对象

    public GuKe(BaoZi bz) {
        this.bz = bz;
    }
    //设置线程任务,制作包子
    @Override
    public void run() {
        while(true) {
            synchronized (bz) {
                if(bz.flag == false) {  //没有包子,则等待生产包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒之后执行的代码,吃包子
                System.out.println("顾客正在吃:" + bz.pi + bz.xian + "的包子");
                bz.flag = false;
                bz.notify();    //唤醒包子铺线程,继续生产包子
                System.out.println("顾客已经把" + bz.pi + bz.xian + "的包子吃完了,包子铺开始生产包子");
                System.out.println("====================");
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi(); //创建包子对象
        new BaoZiPu(bz).start();    //创建包子铺线程
        new GuKe(bz).start();   //创建顾客线程
    }
}

线程池

若并发的线程数量很多,并且每个线程都是执行一段很短的时间就结束了,这样频繁创建线程会大大降低系统的效率,因为频繁创建线程和销毁线程都需要消耗时间。

Java中可以通过线程池实现线程的复用,其中的线程可以反复使用
若线程池中已无空闲线程,则任务队列中的剩余任务等待执行,等待其他某个任务执行完毕后,归还线程到线程池,再从线程池中获取线程,执行任务

线程池的底层原理其实就是一个容器,一个集合(比如ArrayList,HashSet,LinkedList,HashMap),应该优先选用LinkedList< Thread >
当程序第一次启动时,创建多个线程并保存到一个集合中
当要使用线程时,从集合中取出线程使用 Thread t = linked.removeFirst();
当使用完毕线程后,将线程归还给线程池 linked.addLast(t);
在JDK1.5之后,jdk内置了线程池,可直接使用

java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors 类中的静态方法:

  • static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
    参数:int nThreads,创建线程池中包含的线程数量
    返回值:ExecutorService 接口,返回的是 ExecutorService 接口的实现类对象,可以使用 ExecutorService 接口接收(面向接口编程)

java.util.concurrent.ExecutorService:线程池接口,用来从线程池中获取线程,调用 start() 方法执行线程任务
其中包含方法:
submit(Runnable task):提交一个 Runnable 任务用于执行
void shutdown():关闭/销毁线程池

线程池的使用步骤:

  1. 使用线程池的工厂类 Executors 中提供的静态方法 newFixedThreadPool() 生产一个指定线程数量的线程池
  2. 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
  3. 调用 ExecutorService 中的方法 submit(),传递线程任务(实现类),开启线程,执行 run 方法
  4. 可以调用 ExecutorService 中的方法 shutdown(),销毁线程池。(不建议执行)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors中提供的静态方法newFixedThreadPool()生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit(),传递线程任务(实现类),开启线程,执行run方法
        //线程池会一直开启,使用完线程后,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());  //pool-1-thread-1创建了一个新线程执行
        es.submit(new RunnableImpl());  //pool-1-thread-2创建了一个新线程执行
        es.submit(new RunnableImpl());  //pool-1-thread-1创建了一个新线程执行

        //(不建议执行)4.可以调用 ExecutorService 中的方法 shutdown(),销毁线程池
        es.shutdown();
        es.submit(new RunnableImpl());  //会抛出java.util.concurrent.RejectedExecutionException异常,线程池被销毁,不能获取线程
    }
}

//2. 创建一个类,实现 Runnable 接口,重写 run 方法,设置线程任务
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+ "创建了一个新线程执行");
    }
} 

Lambda 表达式

函数式编程思想概述
数学中,函数即有输入量,有输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法,强调做什么,而不是以什么形式做

冗余的 Runnable 代码
比如我们在采用 Runnable 的匿名内部类实现多线程时,

//创建Runnable接口实现类
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "新线程创建");
    }
}
public class Main {
    public static void main(String[] args) {
        Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建");
            }
        };
        new Thread(r);

        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建");
            }
        }).start();
    }
}

我们通过以上方法可以发现:

  • Thread 类需要 Runnable 接口作为参数,其中的抽象方法 run 是用来指定线程任务内容的核心
  • 为了指定 run 方法,不得不需要 Runnable 接口的实现类
  • 为了省去定义 Runnable 实现类,不得不采用匿名内部类
  • 必须要覆盖重写 run 方法,所以方法名称/参数及返回值必须重写
  • 而实际上,只有方法体才是关键所在。我们的最终目的是将 run 方法传递给 Thread 类,创建对象只是受限于面向对象语而不得不采取的一种手段方式

在Java8(JDK1.8)中,加入了 Lambda 表达式

Lambda 标准格式
Lambda 格式: (参数列表) -> {一些重写方法的代码}

如Runnable 接口中 run 方法的定义:public static void run(),即制定了一种做事情的方案,无参数,无返回值,代码块(方法体)为方案的具体执行步骤。
同样的语义体现在 Lambda 语法中:
() -> System.out.println("新线程创建");

  • 小括号() 即 run 方法的参数(无),代表不需要任何条件
  • 中间箭头表示将前面的参数传递给后面的代码
  • 后面的输出语句为业务逻辑代码

如,通过 Lambda 表达式实现多线程:

/*使用Lambda实现多线程*/
public class LambdaMain {
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "新线程创建");
        }
        ).start();
    }
}

Lambda 的无参数无返回值

public interface Cook {
    public abstract void makeFood();
}
public class Main {
    public static void main(String[] args) {
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭了");
            }
        });

        //使用Lambda表达式
        invokeCook(()->{System.out.println("吃饭了");});
        //使用Lambda省略格式
        //invokeCook(()->System.out.println("吃饭了"));
    }

    public static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

Lambda 的有参数有返回值

import java.util.Arrays;
import java.util.Comparator;

/*使用数组存储多个Person对象,对数组中的对象通过Arrays.sort()对年龄进行排序*/
public class Main {
    public static void main(String[] args) {
        Person[] arr = {
                new Person("小王", 23),
                new Person("小李", 14),
                new Person("小张", 27),
        };
/*        Arrays.sort(arr, new Comparator() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        });*/

        //使用Lambda表达式
        Arrays.sort(arr, (Person o1, Person o2)->{
            return o1.getAge() - o2.getAge();
        });
        //使用Lambda省略格式
        //Arrays.sort(arr, (o1, o2)-> o1.getAge() - o2.getAge());
        for (Person person : arr) {
            System.out.println(person);
        }
    }
}
public class Person {
    private String name;
    private int age;

    public Person(){
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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;
    }
}
public class Main {
    public static void main(String[] args) {
        invokeCalc(10, 20, new Calculator() {
            @Override
            public int calc(int a, int b) {
                return a+b;
            }
        });

        //使用Lambda表达式
        invokeCalc(10, 20, (int a, int b) -> {
            return a+b;
        });
    }
    public static void invokeCalc(int a, int b, Calculator c) {
        int sum =c.calc(a, b);
        System.out.println(sum);
    }
}

public interface Calculator {
    public abstract int calc(int a, int b);
}

Lambda 省略格式
Lamda 在标准格式基础上,可以进一步省略,省略写法的规则为:

  1. 小括号 () 内的参数类型可省略
  2. 若小括号内有且仅有一个参数,则小括号可省略
  3. 若大括号 {} 内有且仅有一个语句,则无论是否有返回值,都可以省略大括号,return关键字及语句分号(注意:要省略则这三个一起省略,否则会报错)

如上述例子中,Lambda 表达式可以省略为:

invokeCalc(10, 20, (a, b) -> a+b);

Lambda 的使用前提

注意:

  1. 使用 Lambda 必须具有接口,且接口中有且只有一个抽象方法
  2. 使用 Lambda 必须具有上下文推断
    即,方法的参数或局部变量类型必须为 Lambda 对应的接口类型,才能使用 Lambda 作为该接口的实例

有且仅有一个抽象方法的接口,称为“函数式接口”

你可能感兴趣的:(Java)