JavaSE高级编程之多线程

4. 多线程

4.1 基本概念:程序、进程和线程

程序、进程和线程

程序:为了完成特定的任务,用某种语言编写的一组指令的集合。程序是一段静态的代码,静态对象。

进程:是程序的一次执行过程或正在运行的程序。(进程是一个任务)。进程是一个动态的过程:有产生、存在和消亡的过程——即拥有生命周期。

  • 程序是静态的,进程是动态的
  • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存空间

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。一个进程可以包含一个或多个线程。

  • 若一个进程同一时间执行多个线程,就是支持多线程。
  • 线程作为执行和调度的单位,每个线程拥有独立的运行栈和程序计数器(pc, program counter register),线程切换开销比较小。
  • 一个进程中的多个线程可以共享相同的内存单元/内存地址空间(它们从同一堆中分配对象,方法区、堆),可以访问相同的变量和对象。使得线程间通信更高效、便捷。但是多个线程操作共享的系统资源可能会带来安全隐患。

并行与并发

  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU(采用时间片)同时执行多个任务。如:秒杀。

多线程的优点

  • 提高程序的响应
  • 提高CPU的利用率
  • 改善程序结构

何时需要多线程

  • 程序需要同时执行两个或多个任务
  • 程序需要实现等待的任务
  • 需要后台运行的程序

多进程和多线程比较

和多线程相比,多进程缺点:

  • 创建进程比创建线程开销大
  • 进程间通信比线程间通信慢,因为线程间通信是读写同一个变量,速度很快

多进程优点:

  • 多进程稳定性比多线程高,以为多进程下,一个进程崩溃不会影响其他进程。而在多线程下,任何一个线程的崩溃会直接导致整个进程崩溃

Java语言内置了多线程支持:一个Java应用程序实际上是一个JVM进程,JVM进程用一个主线程执行main()方法,在main()内部,又可以启动多个线程。(一个java.exe,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。)

线程的分类

Java线程分为两类:一种是守护线程,一种是用户线程。

  • 在各当面几乎是一样的,唯一区别是判断JVM何时离开。
  • 当用户线程执行结束,守护线程也会结束。
  • 守护线程用来服务用户线程的,在start() 前调用 thread.setDaemon(true) 可以把一个用户线程变为守护线程
  • Java垃圾回收机制就是一个典型的守护线程
  • 若JVM中都是守护线程,当前JVM将退出

4.2 线程的创建和使用

创建多线程有四种方法,这里有两种,后续java 1.5增加了两种新方法。

Java语言的JVM允许程序运行多个线程,通过 java.lang.Thread类来体现。

创建一个新线程有两种方法:

第一种:继承Thread 的方式

将一个类声明为 Thread 的子类,子类应重写Thread类的run方法,然后分配并启动子类的实例。

创建线程步骤

  1. 创建一个继承于 Thread 类的子类
  2. 重写 Thread 类的 run() 方法
  3. 创建 Thread 类的子类的对象
  4. 通过对象调用 start() 方法

示例:

// 1. 继承于Thread
class MyThread extends Thread{
    // 2. 重新run方法
    @Override
    public void run() {
        System.out.println("child thread name: "+getName()); // 也可以用 Thread.currentThread().getName()
        // 业务写在这个方法中
    }
}

public class TheadTest {

    public static void main(String[] args) {
        // 3. 创建对象
        MyThread myThread = new MyThread();
        myThread.setName("子线程"); // 设置线程名称
        // 4. 通过对象调用 start 方法
        myThread.start();
      
        Thread.currentThread.setName("主线程"); // 设置主线程名称
        System.out.println("main thread name: "+Thread.currentThread().getName());

    }
}

输出结果:
main thread name: 主线程
child thread name: 子线程

如果要创建多个线程

// 如果创建多个子线程的话,需要
MyThread myThread = new MyThread();
myThread.start();
MyThread myThread2 = new MyThread();
myThread2.start();
MyThread myThread3 = new MyThread();
myThread3.start();

多个线程共享一个静态变量:

当多个线程都想要共用一个值时,比如卖票时的票数,可以将类变量设置为 staitc。

start() 方法的作用

  • 启动当前线程
  • Java虚拟机调用此线程的run方法

创建Thread 的匿名子类

简单一点的话,可以写Thread 的匿名子类

// 创建Thread 的匿名子类
new Thread(){
    @Override
    public void run() {
        System.out.println("test2");
    }
}.start();

使用Java8引入的 lambda

new Thread(()->{
    System.out.println("t3");
}).start();

线程的常用方法

  • start() 启动当前线程;调用run方法
  • run() 通常需要重写Thread类的此方法,将业务写在此方法中
  • currentThread() 静态方法,返回执行当前代码的线程
  • getName() 获取当前线程的名称
  • setName() 设置当前线程的名称
  • yield() 线程让步,释放当前CPU的执行权。(暂停当前正在执行的线程,给优先级相同或更高的线程让步)
  • join() 当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被调用线程执行完为止。(在线程a中调用线程b的join(), 此时线程a进入阻塞状态,直到线程b执行完之后,线程a才结束阻塞状态,继续执行后续程序。)
  • stop() 强制线程生命期结束,不推荐
  • sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
  • isAlive() 判断当前线程是否存活
  • getPriority() 返回此线程的优先级
  • setPriority(int newPriority) 更改线程的优先级

线程的调度

调度策略:

  • 时间片
  • 抢占式:高优先级的线程抢占CPU

Java的调度方法:

  • 同优先级线程组成先进先出队列,使用时间片策略
  • 对高优先级,使用优先调度的抢占式策略

线程的优先级

  • MAX_PRIORITY: 10 (最高优先级)
  • MIN_PRIORITY: 1 (最低优先级)
  • NORM_PRIORITY: 5 (普通优先级)

线程创建时继承父线程的优先级。

低优先及只是获得调用的概率低,并不意味着只有当高优先级的线程执行完以后才会执行低优先级。

第二种:实现 Runnable 接口的方式

第二种创建线程的方式是:创建一个实现 Runnable 接口的类,类实现了run() 方法,将类的对象传递给Thread() 并启动。

创建线程步骤

  1. 创建一个实现了 Runnable 接口的类
  2. 实现类去实现 Runnable 的抽象方法 run()
  3. 创建实现类的对象
  4. 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
  5. 通过 Thread 类的对象调用 start() 方法

示例:


/**
 * 多线程的创建,方式二,实现Runnable接口的方式
 * @author chadJ
 * @create 2022-03-04 18:49
 */

//1. 创建一个实现了 Runnable 接口的类
class MyThread2 implements Runnable{

    //2. 实现类去实现 Runnable 的抽象方法 run()
    @Override
    public void run() {
        System.out.println("t2");
    }
}

public class ThreadRunnable {

    public static void main(String[] args) {
        //3. 创建实现类的对象
        MyThread2 t2 = new MyThread2();
        //4. 将对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
        Thread thread = new Thread(t2);
        //5. 通过 Thread 类的对象调用 start() 方法
        thread.start();
        
        

        
    }
}

如果要创建多个线程

// 如果创建多个子线程的话
// t2 对象可以直接使用,也能共享里面的变量,因为只有一个对象
MyThread2 t2 = new MyThread2();

Thread thread = new Thread(t2);
thread.start();
Thread thread2 = new Thread(t2);
thread2.start();
Thread thread3 = new Thread(t2);
thread3.start();

start()方法如何执行到run

这里,最后的 start() 方法,是怎么执行到 MyThread2 中的 run() 方法的呢。原来在

Thread.run中有如下代码,如果target 不为空,则执行 target 的run() 方法

  public void run() {
      if (target != null) {
          target.run();
      }
  }

其中,target 是我们调用 Thread(t2) 时通过构造函数传过去的 Runnable

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

两种方式的比对

一种是继承Thread, 一种是实现Runnable 接口。

开发中,优先选择:实现 Runnable 接口的方式,

原因:

  • 实现的方式没有类的单继承性的局限性,可以实现了Runnable接口后,实现其他接口
  • 实现的方式更适合来处理多个线程有共享数据的情况(实现自然而然能共享数据,继承得给变量加 static)

联系:Thread 也实现的 Runnable。

相同点:两种方式都需要重写 run() 方法,将线程要执行的逻辑声明在 run() 中。

第三种:实现 Callable 接口

JDK5.0 新增两种线程创建方式:实现 Callable 接口、使用线程池 方式。

与使用 Runnable 相比,Callable 功能更强大:

  • 相比 run() 方法,可以有返回值

  • 方法可以抛出异常(之前的run()不可以抛出,因为原方法没抛出异常,重写的不能抛出)

  • 支持泛型的返回值

  • 需要借助 FutureTask 类,比如获取返回结果

    Futrue 接口

    • 可以对具体Runnable Callable 任务的执行结果进行取消、查询是否完成、获取结果等操作。
    • FutureTask 是 Future 接口的唯一实现类
    • FutureTask 同时实现了 Runnable 和 Future接口。即可以作为Runnable 被线程执行,又可以作为 Future 得到Callable 的返回值。

创建线程步骤

  1. 创建一个实现 Callable 的实现类
  2. 实现 call() 方法,将业务放在call()中。可以有返回值,可以抛出异常
  3. 创建 Callable 接口实现类的对象
  4. 将此 Callable 实现类的对象作为参数传递 FutureTask 构造器中,创建 FutureTask 对象
  5. 将FutureTask 对象作为参数传递到 Thread类构造器中(Runnable多态,因为FutureTask实现了Runnable接口),创建 Thread 对象,并调用 start()
  6. 可以使用 get() 获取call() 中的返回值。( get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写 call()方法的返回值。)
package com.acfuu.java;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创建线程方式三:实现Callable接口
 * @author chadJ
 * @create 2022-03-06 14:11
 */

// 1.创建一个实现 Callable 的实现类
class NumThread implements Callable{

    // 2.实现 call() 方法,将业务放在call()中。可以有返回值,可以抛出异常
    @Override
    public Object call() throws Exception {
        int sum=0;
        for (int i = 0; i <= 100; i++) {
            sum+=i;
        }
        return sum;
    }
}

public class CallableThread {
    public static void main(String[] args) {
        // 3. 创建 Callable 接口实现类的对象
        NumThread numThread = new NumThread();
        // 4. 将此 Callable 实现类的对象作为参数传递 FutureTask 构造器中,创建 FutureTask 对象
        FutureTask futureTask = new FutureTask(numThread);
        // 5. 将FutureTask 对象作为参数传递到 Thread类构造器中,创建 Thread 对象,并调用 start()
        new Thread(futureTask).start();

        try {
            // 6. 可以使用 get() 获取call() 中的返回值。
            // get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写 call()方法的返回值。
            Object r = futureTask.get();
            System.out.println("返回值:"+r);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

第四种:使用线程池(常用)

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建和销毁,实现重复利用。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用池中线程,不用每次都创建)
  • 便于线程管理
    • corePoolSize: 核心池大小
    • maximumPoolSize: 最大线程数
    • keepAliveTime: 线程没有任务时的存活时间

线程池相关 API

JDK5.0起提供了线程池API:ExecutorService 和 Executors

ExexutorService:真正的线程池接口。常见子类 ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
  • void shutdown():关闭连接池

Executors: 工具类,线程池的工厂类,用于创建并返回不同类型的线程池

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
  • Executors.newFixedThreadPool():创建一个可重用固定线程池数的线程池(常用)
  • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
  • Executors.newScheduledThreadPool(n):在给定延迟后创建一个线程池

线程创建步骤

  1. 提供指定线程数量的线程池
  2. 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
  3. 关闭连接池

示例:

package com.acfuu.java;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 创建线程方式四:使用线程池
 * @author chadJ
 * @create 2022-03-06 14:47
 */


class NumThread2 implements Runnable{

    private boolean odd = true;

    public NumThread2(boolean odd) {
        this.odd = odd;
    }

    @Override
    public void run() {
        for (int i = 0; i <= 10; i++) {
            if(odd){
                if(i%2==1) System.out.println(Thread.currentThread().getName()+":"+i);
            } else{
                if(i%2==0) System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        // 1. 提供指定线程数量的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 设置线程池属性
        // System.out.println(executorService.getClass());
        // ThreadPoolExecutor executor = (ThreadPoolExecutor) executorService;
        // executor.setCorePoolSize(1); // 设置为1后,线程池中就不是10个线程,就只有一个线程了


        // 2. 执行指定的线程操作,需要提供实现 Runnable 接口或 Callable 接口实现类的对象
        executorService.execute(new NumThread2(true)); // 适用于 Runnable
        executorService.execute(new NumThread2(false));
        //executorService.submit(xxx); // 适用于 Callable

        // 3. 关闭连接池
        executorService.shutdown();
    }
}


输出:
pool-1-thread-1:1
pool-1-thread-2:0
pool-1-thread-2:2
pool-1-thread-2:4
pool-1-thread-2:6
pool-1-thread-2:8
pool-1-thread-2:10
pool-1-thread-1:3
pool-1-thread-1:5
pool-1-thread-1:7
pool-1-thread-1:9

4.3 线程的生命周期

JDK 中的Thread.State 类定义了线程的状态。一个线程对象只能调用一次 start() 方法启动新线程(所以不能一个Thread对象多次start),并在新线程中执行 run() 方法。一旦 run() 执行完毕,线程就结束了。

Java 线程的状态有:

  • New, 新创建的线程,尚未运行
  • Runnable, 运行中的线程,正在执行 run()方法的代码
  • Blocked, 运行中的线程,因为某些操作被阻塞而挂起
  • Wating, 运行中的线程,因为某些操作在等待
  • Time Waiting, 运行中的线程,因为执行 sleep() 方法正在计时等待
  • Terminated, 线程已终止,run() 执行完毕

线程生命周期:

4.4 线程的同步

线程的同步是为了解决线程的安全问题。

问题:比如卖票出现重票、错票,这就叫出现了线程的安全问题。

解决:当一个线程a在操作的时候,其他线程不能参与进来,直到线程a操作结束其他线程才可以开始操作。即使线程a出现了阻塞,也不能执行其他线程。

线程的安全问题

在多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待。通过加锁和解锁操作,就能保证指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期间被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区,任何时候临界区最多只有一个线程能执行。

在Java中,通过同步机制,解决线程的安全问题。

同步机制解决线程安全问题

使用synchronized (发音 /'sɪŋkrənaɪzd/),有两种方法:

第一种:同步代码块

synchronized(lock) { // lock锁也叫同步监视器
    // 需要被同步的代码。(操作共享数据的代码)
}

任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。

在实现Runnable 接口创建多线程的方式中,可以考虑使用 this 充当 lock。

在继承Thread 类创建多线程的方式中,(慎用this充当lock),考虑使用当前类充当lock。(比如 MyThread.class)

第二种:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

需要同步的方法使用 synchronized 修饰:

1.在实现 Runnable 接口的方式中:

此种方法中,lock 是 this。

public synchronized void test(){ // 在test中,同步监视器/锁就是 this
...
}

public void run(){
  ...
  test();
  ...
}

run() 方法也可以使用 synchronized修饰, sysnchronized void run(),这种要确保 run()中使完整的同步数据。不然包裹的太多,效率会低。

2.在继承 Thread 类的方式中:

为了保证正常执行,需要多加个 static 。

此种方法中,lock 是 当前类。

public static synchronized void test2(){ // 在test2中,同步监视器/锁就是 当前类
...
}

public void run(){
  ...
  test2();
  ...
}

总结:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
  • 非静态的同步方法,同步监视器是 this
  • 静态的同步方法,同步监视器是 当前类本身

线程同步优缺点

  • 同步的方式,解决了线程的安全问题。
  • 操作同步代码时,只能有一个线程参与,其他线程等待。相当于一个单线程的过程,效率低。

只有有共享数据的代码才需要使用线程的同步解决问题。

Lock 锁方式解决线程安全问题

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁来实现同步。同步锁使用Lock对象充当。

1.使用ReentrantLock

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.start();
        t2.start();
        t3.start();
    }
}


class Window implements Runnable{

    private int ticket = 100;

    // 1. 实例化 ReentrantLock
    private final ReentrantLock lock = new ReentrantLock(); // 参数 fair=true/false, 公平或者非公平


    @Override
    public void run() {
        while(true){
            try {

                // 2. 调用lock方法
                lock.lock();

                if(ticket>0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+" 票号:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            } finally {
                // 3. 解锁方法
                lock.unlock();
            }
        }
    }

4.5 线程安全的 懒汉式的单例模式

class Bank{
    private Bank(){}

    private static Bank instance = null;

    // 普通获取实例的方法
    public static Bank getInstance(){
        if(instance==null){
            instance = new Bank();
        }
        return instance;
    }
    
    // 线程安全的获取实例的方法一
    public static synchronized Bank getInstance2(){
        if(instance==null){
            instance = new Bank();
        }
        return instance;
    }

    // 线程安全的获取实例的方法二,其中还有两种写法,后一种效率高些
    public static Bank getInstance3(){
        // 效率不高
        /*
        synchronized (Bank.class) {
            if(instance==null){
                instance = new Bank();
            }
            return instance;
        }
         */

        // 效率高一些
        if(instance==null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;

    }
}

4.6 线程的死锁问题

死锁:

  • 不同的线程分别占用对方需要的同步资源,都在等待对方放弃自己需要的同步资源,形成了线程的死锁。
  • 死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

说明:

  • 出现死锁后,不会异常,不会出现提示,所有线程都处于阻塞状态,无法继续运行
  • 在使用同步时,要避免出现死锁

解决方法:

  • 专门的算法、原则
  • 尽量较少同步资源的定义
  • 尽量避免嵌套同步

示例,下面的代码会出现死锁,程序没输出没结束。在第一个线程运行到sleep的时候,线程二继续运行,结果都需要sb1 sb2,产生了死锁。

    public static void main(String[] args) {
        StringBuffer sb1 = new StringBuffer();
        StringBuffer sb2 = new StringBuffer();

        new Thread(()->{
            synchronized (sb1){
                sb1.append("a");
                sb2.append("1");

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (sb2){
                    sb1.append("b");
                    sb2.append("2");

                    System.out.println(sb1);
                    System.out.println(sb2);
                }
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (sb2){
                    sb1.append("c");
                    sb2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (sb1){
                        sb1.append("d");
                        sb2.append("4");

                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }).start();
    }

4.7 线程的通信

多线程协调(线程通信)问题使用 wait()notify()

示例:两个线程交替打印1-100

/**
 * 线程通信的示例:两个线程交替打印1-100
 * @author chadJ
 * @create 2022-03-05 22:31
 */


class Number implements Runnable{
    private int number = 1;

    @Override
    public void run() {
        while(true){

            synchronized (this) {

                // 唤醒其他线程
                notify();

                if(number<101){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;

                    try {
                        // 使得调用wait()方法的线程进入阻塞状态,并且wait回释放锁
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }
    }
}

public class ThreadCommunication {
    public static void main(String[] args) {
        Number n = new Number();

        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();

    }
}

线程通信涉及到的方法:

  • wait() 一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器(锁)
  • notity() 执行此方法,会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级最高的线程
  • notifyAll() 执行此方法,会唤醒所有被 wait 的线程

说明:

  • 上述三个方法 wait() notify() notifyAll() ,必须使用在同步代码块或同步方法中。Lock想要线程通信使用其他方法
  • wait() notify() notifyAll() 的调用者必须使同步代码块或同步方法中的同步监视器,否则会出现 IllegalMonitorStateException。比如
class Test implements Runnable{
  ...
  public void run() {
    synchronized (this) {
        notify();
        ...
        wait();
    }
}
或

class Test implements Runnable{
  ...
  public void run() {
    synchronized (obj) {
        obj.notify();
        ...
        obj.wait();
    }
}

  • 这三个方法是定义在 java.lang.Object 类中的。(从上一条可知,任何一个对象都要能够调用 wait() notify() notifyAll() 方法)

4.8 线程通信应用:生产者消费者问题

描述:生产者将产品交给店员,消费者从店员处取走商品,店员一次能持有产品数量有容量限制,如果生产者试图生产更多的产品,店员会叫停生产者,如果有空位了再通知生产者继续生产。如果店中没有商品,店员会叫停消费者,如果有产品了再通知消费者来取走产品。

分析:

  • 是否是多线程问题
  • 是否有共享数据
  • 如何解决线程安全问题?同步机制,有三种方法
  • 是否涉及到线程通信
/**
 * 线程通信应用,生产者、消费者问题。 实现Runnable方式
 * @author chadJ
 * @create 2022-03-06 13:13
 */


/**
 * 店员类
 */
class Clerk{
    // 商品数量
    private int goodsNum = 0;

    public synchronized void createProduct() {
        if(goodsNum<20){
            goodsNum++;
            System.out.println(Thread.currentThread().getName()+": 生产商品 "+goodsNum);

            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void useProduce() {
        if(goodsNum>0){

            System.out.println(Thread.currentThread().getName()+": 消费商品 "+goodsNum);
            goodsNum--;

            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Productor implements Runnable{

    private Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":开始生产商品");
        while(true){
            try {
                Thread.sleep(30); // sleep模拟生产过程耗费的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.createProduct();
        }
    }
}

class Customer implements Runnable{
    private Clerk clerk;

    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+":开始消费商品");

        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            clerk.useProduce();
        }
    }
}


public class ProductorCustomerTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Thread p1 = new Thread(new Productor(clerk));
        p1.setName("生产者");
        p1.start();

        Thread c1 = new Thread(new Customer(clerk));
        c1.setName("消费者1");
        c1.start();

        Thread c2 = new Thread(new Customer(clerk));
        c2.setName("消费者2");
        c2.start();

        Thread c3 = new Thread(new Customer(clerk));
        c3.setName("消费者3");
        c3.start();
    }
}

4.9 面试题和总结

synchronized 与 Lock 的异同?

相同:二者都可以解决线程安全的问题

不同:

  • Lock 是显式锁(需要手动启动和释放锁),synchronized 是隐式锁,出了作用域自动释放
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM将花费较少时间来调度线程,性能更好

优先使用顺序:(建议)

Lock → 同步代码块(已经进入了方法体,分配了相应的资源)→ 同步方法(方法体之外,作用整个方法)

如何解决线程安全问题,有几种方式

同步机制说两种三种都可以,要都讲到

两种:synchronized方式(又分同步代码块和同步方法)和Lock方式

三种:synchronized 同步代码块、synchronized 同步方法 和 Lock 方式

sleep() 和 wait() 的异同?

相同点:都可以让线程进入阻塞状态

不同点:

  • 两个方法声明的位置不同:Thread 类中声明的 sleep();Object 类中声明的 wait()
  • 调用要求不同:sleep() 可以在任何需要的场景下调用;wait() 必须使用在同步代码块或同步方法中。
  • 关于是否释放同步监视器:如果两方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。

如何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式更强大?

  • call() 可以有返回值。可以抛出异常,被捕获后获取异常信息
  • Callable 是支持泛型的

创建多线程有几种方式?

四种:继承Thread类,实现Runnable接口,实现 Callable 接口,使用线程池(后两种为JDK5.0 新增)。

释放锁(同步监视器)的操作

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步方法、同步代码块中遇到 break、return,终止了该代码块的继续执行
  • 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束。
  • 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停并释放锁。

不会释放锁的操作

  • 线程执行同步方法、同步代码块时,程序调用 Thread.sleep()Thread.yield()方法暂停当前线程的执行
  • 线程执行同步方法、同步代码块时,其他线程调用了该线程的 suspend()方法将该线程挂起,该线程不会释放锁
posted @ 2022-03-06 15:35  姜小豆  阅读(329)  评论(0编辑  收藏  举报