Java多线程

基础语法,不涉及JUC

Java多线程

多线程和异步

概念

异步:在异步模型中,允许同一时间发生(处理)多个事件。程序调用一个耗时较长的功能(方法)时,它并不会阻塞程序的执行流程,程序会继续往下执行。当功能执行完毕时,程序能够获得执行完毕的消息或能够访问到执行的结果(如果有返回值或需要返回值时)。

简单来说就是,为了不让主线程被阻塞,一般将比较耗时的操作(eg.网络请求、IO、数据库操作)通过异步操作放在后台中执行,执行结束后向程序通知结果。

注意

1.“异步操作放在后台执行”不代表一定要新开一个进程,也可以是利用操作系统提供的异步IO、回调(callback)或者事件(event)机制等方式来实现异步处理。

2.“向程序通知结果”而不是“向主进程通知结果”,因为回调函数或者监听器不一定要在主线程中执行。例如程序如果需要在异步操作完成后更新UI界面(UI线程是主线程),那么回调函数或监听器必须在主线程执行,以免出现线程安全问题;如果不需要更新UI,则回调函数和监听器可以在任意线程中执行。

多线程:多线程是指并发或并行执行多个指令(线程)。

总的来说,可以这么理解:

  • 多线程是关于功能的并发执行,异步编程是关于函数之间的非阻塞执行
  • 异步可以应用到单线程或多线程中
  • 异步是目的,多线程是实现异步的一种方法

单线程异步编程

要更深刻理解多线程和异步编程的概念,可以看下单线程异步编程。

当需要处理大量的I/O操作或网络请求时,传统的多线程方法可能会出现线程上下文切换和资源占用等问题,因此单线程异步编程成为了一种常见的解决方案。单线程异步编程可以通过事件循环、回调函数、协程等方式来实现。

1.事件循环(Event Loop)

事件循环是单线程异步编程中最常见的方式之一。在事件循环中,程序会不断地检查是否有新的事件需要处理,如果有就立即执行相应的事件处理函数。事件循环的核心是一个循环体,该循环体不断地从事件队列中获取事件,并执行相应的事件处理函数。事件循环通常会配合异步IO模型使用,如epoll、select等,以便从系统中获取事件。

下面是一个简单的事件循环示例:

 import javax.swing.*;
 ​
 public class SimplifiedEventLoopExample {
 ​
     public static void main(String[] args) {
         // 将任务添加到事件队列中
         SwingUtilities.invokeLater(() -> task1());
    }
 ​
     private static void task1() {
         System.out.println("Task 1 executed");
 ​
         // 将任务2添加到事件队列中
         SwingUtilities.invokeLater(() -> task2());
    }
 ​
     private static void task2() {
         System.out.println("Task 2 executed");
    }
 }

2.回调函数(Callback)

回调函数是另一种常见的单线程异步编程方式。在回调函数中,程序会将需要异步处理的任务交给另外一个线程或进程,等待任务完成后再执行回调函数来处理任务结果。回调函数通常用于处理事件通知,如网络请求结束、文件读取完成等。

下面是一个简单的回调函数示例:

 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 ​
 public class AsyncCallbackExample {
 ​
     public static void main(String[] args) throws ExecutionException, InterruptedException {
         // 异步执行一个任务,该任务将在1秒后返回"Hello, World!"字符串
         CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
             try {
                 TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }
             return "Hello, World!";
        });
 ​
         // 注册回调函数,当任务完成后,将结果转换为大写并输出
         future.thenAccept(result -> System.out.println("Result: " + result.toUpperCase()));
 ​
         System.out.println("Main thread is not blocked!");
 ​
         // 等待异步任务完成,以便查看回调函数的输出
         future.get();
    }
 }

3.协程(Coroutine)

协程是一种轻量级的线程,可以在单线程中实现并发操作。协程常用于处理I/O操作和CPU密集型任务。

下面是一个简单的协程示例:

 import java.util.concurrent.Fiber;
 import java.util.concurrent.FiberScope;
 ​
 public class CoroutineExample {
 ​
     public static void main(String[] args) {
         FiberScope scope = FiberScope.open(); // 创建一个FiberScope
 ​
         // 在协程中异步执行一个任务
         Fiber<String> fiber = scope.schedule(() -> {
             try {
                 Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }
             return "Hello, World!";
        });
 ​
         System.out.println("Main thread is not blocked!");
 ​
         // 获取协程的结果并输出
         String result = fiber.join();
         System.out.println("Result: " + result.toUpperCase());
 ​
         // 关闭FiberScope
         scope.close();
    }
 }

总结:

上述三种单线程异步编程的方式各有优缺点,需要根据具体的应用场景和需求来选择适合的编程方式。在实际编程中,可以结合使用多种方式来实现异步处理,例如利用事件循环机制来实现异步IO操作,利用回调函数来处理异步结果。

Java使用多线程方法

使用线程有三种方法

  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 继承Thread类

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现Runnable接口

 public class RunnableTask implements Runnable {
     public void run() {
         System.out.println("Runnable!");
    }
 ​
     public static void main(String[] args) {
         RunnableTask task = new RunnableTask();
         new Thread(task).start();
    }
 }

优点:

  • 灵活,不占用继承.Java不支持多继承,如果已经继承了某个基类,则实现Runnable接口来生成多线程.
  • 更便于多个线程共享资源A implements Runnable{}B extends Thread{},那么a=new A;a1=new Thread(a);a2=new Thread(a)b1=new B();b2=new B()是不一样的,可以理解为,runnable是多个线程执行同一个runnable任务,Thread是将同一个任务复制为多个线程.(应用场景 eg. 售票)
  • 将线程的任务与线程管理本身分离开
  • 适用于Java线程池,线程池通常接受Runnable任务作为输入,并在其中管理线程的生命周期。这可以提高性能,因为线程池可以复用线程,而无需为每个任务创建新的线程。⭐(简单来说就是线程池new出几个线程之后,任务执行结束只需要切换不同的runnable任务就可以了,不需要再new新的线程)

但是这种方法没有返回值,无法获取线程执行结果.

实现Callable接口

重写call()方法,可以通过FutureTask获取任务执行的返回值.有返回值.

 public class CallerTask implements Callable<String> {
     public String call() throws Exception {
         return "Hello,i am running!";
    }
 ​
     public static void main(String[] args) {
         //创建异步任务
         FutureTask<String> task=new FutureTask<String>(new CallerTask());
         //启动线程
         new Thread(task).start();
         try {
             //等待执行完成,并获取返回结果
             String result=task.get();
             System.out.println(result);
        } catch (InterruptedException e) {
             e.printStackTrace();
        } catch (ExecutionException e) {
             e.printStackTrace();
        }
    }
 }

CallableRunnable的区别:

  1. 返回值Callable接口的call()方法可以返回一个结果值,而Runnable接口的run()方法没有返回值。这使得Callable接口更适合于需要返回计算结果的任务。
  2. 异常处理Callable接口的call()方法可以抛出已检查的异常(checked exception),而Runnable接口的run()方法不允许抛出已检查的异常。这意味着在Runnablerun()方法中,需要捕获和处理所有可能抛出的已检查异常。
  3. Future结合Callable接口通常与Future接口一起使用,以便异步获取任务的结果。当将Callable任务提交给ExecutorService时将获得一个Future对象,该对象可以用于检查任务是否完成、获取任务的结果或取消任务。

继承Thread类

Thread类直接继承Object类,实现了Runnable接口。

实现多线程可以继承Thread类,并重写run方法,调用start启动线程,自动进入run方法.

 class MyThread extends Thread{
     public void run(){...}
 }
 ​
 MyThread mt = new MyThread();
 mt.start();

为什么不直接调用run,要先调用start?

调用start方法先创建子线程,由子线程执行run方法;若直接调用run方法,相当于主线程顺序执行.

构造方法

 public Thread();
 public Thread(Runnable target);
 public Thread(String threadName);
 public Thread(Runnable target,String threadName);

异常处理

run方法不能声明受检异常:

  • 设计原则run方法设计为不声明任何受检异常,来保证实现此方法时必须显式处理可能的受检异常,以避免错误传播到其他线程;
  • 线程独立性:在多线程环境中,每个线程应该独立运行,互不干扰。如果run方法抛出受检异常,那么调用线程需要处理这个异常。影响其他线程不符合独立性原则;
  • 异常处理策略:如果run方法允许抛出受检异常,那么在实际使用时,可能会导致异常处理策略不一致。
 //run不可以throws,run中调用的方法可以throws,但必须在run中try-catch处理,即run不能抛出受检异常
 class Pr extends Thread{
     public void run(){
         try{
             Thread.sleep(10);
        method();
        }catch(Exception e){
             e.printStackTrace();
        }
  }
     public void method() throws Exception{
         System.out.println("method");
    }
 }

Thread类常用方法:

 //启动线程
 public void start();
 //被重写的方法,线程执行的任务
 public void run();
 //用于测试某个线程是否还活着
 public final boolean isAlive();
 //设置线程优先级
 public final void setPriority(int newPriority);
 //获得线程优先级
 public final int getPriority();
 //返回当前正在执行的线程的Thread对象
 public static Thread currentThread();
 //设置是否为后台线程,若当前运行线程全部都是daemon则JVM停止运行。这个方法必须在start()方法前使用.
 public final void setDaemon(Boolean on);
 //判断当前线程是否有权修改调用此方法的线程,无权则抛出SecurityException.
 public final void checkAccess();
 //更改本线程名称
 public void setName(String name);
 ​
 ​
 /**
 通知
 */
 //将对象等待列表中的任一个线程唤醒
 public final void notify();
 //将对象等待列表所有线程唤醒
 public final void notifyAll();
 ​
 ​
 /**
 等待
 */
 //当前线程被中断,【释放隐式锁】,进入一个对象的等待列表,直到其他线程调用同一个对象上的notify()或notifyAll().
 public final void wait(long timeout) throws InterruptedException;
 //A.join()出现在哪个线程,线程A就与哪个线程合并,直到A死亡,当前线程才能继续
 public final void join();
 //sleep可以让任意线程得到执行机会,sleep可以让任意线程得到执行机会,【不释放任何锁】
 public static void sleep(long millis) throws InterruptedException;
 //yield只能让同优先级线程得到执行机会;
 public static void yield();
 ​
 /**
 结束/中断
 */
 //立即强制停止线程,【释放隐式锁,不释放显式锁】,可能造成数据不一致、死锁等问题,被废弃
 public final void stop();
 //通知线程应该中断当前任务,更温和,设置中断标志,完成关键任务或释放资源后中断,【本身不释放任何锁,但可以在try-catch中手动unlock()等释放显式锁,隐式锁会在synchronized块结束后自动释放】
 public void interrupt();
 //interrupt可以抛出InterruptedException将阻塞状态的线程恢复到runnable状态,设置isInterrupted标志位为true,可以让线程在合适的时机检查中断标志进而根据自己的中断策略响应中断请求.
 ​

线程生命周期

2e48d393f22fd1ca2ecac49cfdb6888

多线程同步控制

线程彼此不独立,需要同步

互斥:同一时刻只能有一个线程访问或执行.

协作:多个线程可以在满足条件的情况下同时操作共享数据.

synchronized作为函数修饰符

 public synchronized (static) void aMethod(){...}

锁的生效范围:

  • synchronized非静态锁:
    • 对象锁,锁定调用这个同步方法的对象
    • 当对象P1在不同线程调用这个方法时,它们之间互斥
    • 同Class的对象P2可以任意调用该同步方法,与P1无关
  • 静态同步synchronized方法(static synchronized)
    • Class锁,锁定该Class的所有实例
  • synchronized (class) 代码块
    • Class锁,锁定该Class的所有实例

线程同步

1.同步方法

 //根据上述synchronized范围,这几个方法的效果不一样
 class Method{
     public static synchronized void m_methodStatic(int id){...} //s1和s2同步
     public synchronized void m_methodSynchronized(int id){...} //s1和s2不同步
     public void m_method(int id){...} //完全不同步
 }
 ​
 public class SynchronizationStatic extends Thread{
     public int m_ID;
     public Method m_data;
     SynchronizationStatic(int id){m_ID=id;}
     public void run(){
         m_data.m_methodSynchronized(m_ID);
         m_data.m_methodStatic(m_ID);
         m_data.m_method(m_ID);
    }
     public static void main(String args[]){
         Method m1 = new Method();
         SynchronizationStatic s1= new SynchronizationStatic(1);
         s1.m_data=m1;
         s1.start();
         Method m2 = new Method();
         SynchronizationStatic s2= new SynchronizationStatic(2);
         s2.m_data=m2;
         s2.start();
    }
 }

2.同步代码块

 //当一个线程进入synchronized代码块时会获取so对象的锁,此时其他线程试图进入此代码块会被阻塞,直到当前线程执行完并释放so锁.
 //但是锁的粒度仅限于so,若使用不同的so实例则不会影响.
 //还可以使用this、类对象或自定义对象专门作为锁
 public void method(SomeObject so){
     synchronized(so){...}
 }
 public void method(){
     synchronized(this){...}
 }
 ​
 //锁的范围和static synchronized一致
 public void method(){
     synchronized (XX.class){...}
 }
 public void method(){
     synchronized (Class.forName("****Class")){...}
 }

总的来说就是,对synchronized(对象){代码段}来说,synchronized会先判断对象锁是否空闲,若空则拿走并执行代码,若已经被其他线程拿走则等待.

线程通信

wait、notify、notifyAll

  • 一般在synchronized代码块里使用wait()notify()notifyAll()方法
    • 因为wait()方法执行前必须要已经获得锁.
  • wait()需要被try-catch包围
  • notify()要在wait()之后执行
  • notifyAll()最先被唤醒的线程取决于操作系统

多线程中测试条件变化使用if还是while

 public void consume() throws InterruptedException {
     while (true) {
         synchronized (this) {
             // 使用 while 循环测试条件
             while (queue.isEmpty()) {
                 wait();
            }
             System.out.println("Consumed: " + queue.poll());
             notify();
             Thread.sleep(500);
        }
    }
 }

死锁

死锁产生的四个必要条件:

  • 互斥使用
    • 资源被一个线程占用时其他线程不能使用
  • 不可抢占
    • 资源只能由占用者主动释放
  • 请求和保持
    • 线程请求其他资源时对已获取的资源保持占用
  • 循环等待
    • 等待队列

生产者-消费者模型

  • 有一个有一定容量的仓库——临界资源
  • 生产者生产产品放入仓库
  • 消费者消费仓库产品
 class Account{
     private String name;
     private double balance;
     public Account(String name,double money){
         this.name = name;
         this.balance=money;
    }
     public synchronized void deposit(double money){
         if(money>0){
             balance+=money;
             notify();
        }
    }
     public synchronized void withdraw(double money){
         if(money>0){
             while(money>balance){
                 try{wait();}
                 catch(InterruptedException e){}
            }
             balance-=money;
        }
    }
 }

守护线程

概念:

也叫后台线程,通常是为了辅助其他线程而运行的线程,它不妨碍程序终止.e.g.JVM的垃圾回收线程

  • Java线程分为用户线程(前台运行,不提供公共服务)和守护线程(后台提供通用服务的线程)
  • 一个进程只要有一个前台进程在运行,该进程就不会结束
  • 若进程中所有前台进程都结束,则进程结束;所有用户线程退出,则虚拟机退出
  • 后台线程创建的子线程也是后台线程
  • setDaemon(true)可以将线程设置为后台线程,且必须在start()之前,否则会抛出IllegalThreadException异常.

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注