Java基础教程(15)-多线程基础

2024-05-06 07:37:19 浏览数 (1)

多线程是Java最基本的一种并发模型;Java语言内置了多线程支持;

进程和线程

进程和线程的关系就是:进程和线程是包含关系;一个进程可以包含一个或多个线程,但至少会有一个线程;

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。 某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

启动多线程

要创建一个新线程非常容易,只需要实例化一个 Thread 实例,然后调用它的 start() 方法;

代码语言:java复制
package com.demo;

public class ThreadDemo {

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread = new Thread(new MyRunnable());
        //优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
        thread.setPriority(10); //设置优先级
        
        thread.start();
    }

}

class  MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("my thread run...");
    }
}

class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("myRunnable is run....");
    }
}

常见的两种方法创建 Thread 实例:

  • 从 Thread 派生一个自定义类,然后覆写 run() 方法
  • 创建 Thread 实例时,传入一个 Runnable 实例
线程状态

在Java程序中,一个线程对象只能调用一次 start() 方法启动新线程,并在新线程中执行 run() 方法。一旦 run() 方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

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

当线程启动后,它可以在 Runnable 、 Blocked 、 Waiting 和 Timed Waiting 这几个状态之间切换,直到最后变成 Terminated 状态,线程终止。

一个线程还可以等待另一个线程直到其运行结束。例如, main 线程在启动 t 线程后,可以通过 t.join() 等待 t 线程结束后再继续运行

操作线程

中断线程两种方式:

对目标线程调用 interrupt() 方法可以请求中断一个线程,目标线程通过检测 isInterrupted() 标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到 InterruptedException ;目标线程检测到 isInterrupted() 为 true 或者捕获了InterruptedException 都应该立刻结束自身线程; 通过标志位判断需要正确使用 volatile 关键字;volatile 关键字解决了共享变量在线程间的可见性问题。为什么要对线程间共享的变量用关键字 volatile 声明? 在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的; volatile 关键字的目的是告诉虚拟机: 每次访问变量时,总是获取主内存的最新值; 每次修改变量后,立刻回写到主内存。

线程同步synchronized

多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用 synchronized 关键字对一个对象进行加锁;

使用 synchronized的步骤 :

  • 找出修改共享变量的线程代码块;
  • 选择一个共享实例作为锁;
  • 使用 synchronized(lockObject) { … } 。在使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized 结束处正确释放锁;

用 synchronized 修饰方法可以把整个方法变为同步代码块, synchronized 方法加锁对象是 this ;

一个类没有特殊说明,默认不是thread-safe;

Java的 synchronized 锁是可重入锁;

死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;

等待和唤醒

wait() 和 notify() 用于多线程协调运行:

  • 在 synchronized 内部可以调用 wait() 使线程进入等待状态;
  • 必须在已获得的锁对象上调用 wait() 方法;
  • 在 synchronized 内部可以调用 notify() 或 notifyAll() 唤醒其他等待线程;
  • 必须在已获得的锁对象上调用 notify() 或 notifyAll() 方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。

使用 notifyAll() 将唤醒所有当前正在 this 锁等待的线程,而 notify() 只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在 getTask() 方法内部的 wait() 中等待,使用 notifyAll() 将一次性全部唤醒。通常来说, notifyAll() 更安全;

线程安全包

使用 java.util.concurrent 包提供的线程安全的并发集合可以大大简化多线程编程:多线程同时读写并发集合是安全的;

使用 java.util.concurrent.atomic 提供的原子操作可以简化多线程编程:原子操作实现了无锁的线程安全;适用于计数器,累加器等

线程池

能接收大量小任务并进行分发处理的就是线程池;

简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理

Java标准库提供了 ExecutorService 接口表示线程池;

ExecutorService 只是接口,Java标准库提供的几个常用实现类有:

  • FixedThreadPool:线程数固定的线程池;
  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。
代码语言:java复制
  ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService = Executors.newCachedThreadPool();
        executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("");
            }
        });

线程池在程序结束的时候要关闭。使用 shutdown() 方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow() 会立刻停止正在执行的任务, awaitTermination() 则会等待指定的时间让线程池关闭。

需要反复执行的任务,可以使用 ScheduledThreadPool 。放入 ScheduledThreadPool 的任务可以定期反复执行。

代码语言:java复制
 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("....");
            }
        },10, TimeUnit.MILLISECONDS);
获取线程返回值

Java标准库还提供了一个 Callable 接口,和 Runnable 接口比,它多了一个返回值:并且 Callable 接口是一个泛型接口,可以返回指定类型的结果。

当我们提交一个 Callable 任务后,我们会同时获得一个 Future 对象,然后,我们在主线程某个时刻调用 Future 对象的 get() 方法,就可以获得异步执行的结果。在调用 get() 时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么 get() 会阻塞,直到任务完成后才返回结果。

一个 Future<V> 接口表示一个未来可能会返回的结果,它定义的方法有:

  • get() :获取结果(可能会等待)
  • get(long timeout, TimeUnit unit) :获取结果,但只等待指定的时间;
  • cancel(boolean mayInterruptIfRunning) :取消当前任务;
  • isDone() :判断任务是否已完成

从Java 8开始引入了 CompletableFuture ,它针对 Future 做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞