• 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() 方法

    示例:

    Copy
    // 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: 子线程

    如果要创建多个线程

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

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

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

    start() 方法的作用

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

    创建Thread 的匿名子类

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

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

    使用Java8引入的 lambda

    Copy
    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() 方法

    示例:

    Copy
    /** * 多线程的创建,方式二,实现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(); } }

    如果要创建多个线程

    Copy
    // 如果创建多个子线程的话 // 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() 方法

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

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

    Copy
    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()方法的返回值。)
    Copy
    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. 关闭连接池

    示例:

    Copy
    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-11 pool-1-thread-20 pool-1-thread-22 pool-1-thread-24 pool-1-thread-26 pool-1-thread-28 pool-1-thread-210 pool-1-thread-13 pool-1-thread-15 pool-1-thread-17 pool-1-thread-19

    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/),有两种方法:

    第一种:同步代码块

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

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

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

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

    第二种:同步方法

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

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

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

    此种方法中,lock 是 this。

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

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

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

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

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

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

    总结:

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

    线程同步优缺点

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

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

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

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

    1.使用ReentrantLock

    Copy
    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 线程安全的 懒汉式的单例模式#

    Copy
    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,产生了死锁。

    Copy
    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

    Copy
    /** * 线程通信的示例:两个线程交替打印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。比如
    Copy
    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 线程通信应用:生产者消费者问题#

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

    分析:

    • 是否是多线程问题
    • 是否有共享数据
    • 如何解决线程安全问题?同步机制,有三种方法
    • 是否涉及到线程通信
    Copy
    /** * 线程通信应用,生产者、消费者问题。 实现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()方法将该线程挂起,该线程不会释放锁
  • 相关阅读:
    脉冲神经网络:MATLAB实现脉冲神经网络(Spiking Neural Network,SNN) 用于图像分类(提供MATLAB代码)
    数据库索引面试的相关问题
    git 如何删除本地分支且并没有完全合并到目标分支中
    API 集成测试工具Hitchhiker 0.1.1 正式发布
    update-alternatives的使用
    基于STM32的OLED多级菜单GUI实现(简化版智能手表)
    三次握手、四次挥手的详细过程
    蓝牙资讯|三星Galaxy Wearable有望在国内发售,智能戒指成为新宠儿
    对mysql的联合索引的深刻理解
    NginxWebUI网页配置工具
  • 原文地址:https://www.cnblogs.com/warcraft/p/15972287.html