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

简介: 【4月更文挑战第15天】Java内置多线程支持,通过Thread类或Runnable接口实现。线程状态包括New、Runnable、Blocked、Waiting、Timed Waiting和Terminated。启动线程调用start(),中断线程用interrupt(),同步用synchronized关键字。线程安全包如java.util.concurrent提供并发集合和原子操作。线程池如ExecutorService简化任务管理,Callable接口允许返回值,Future配合获取异步结果。Java 8引入CompletableFuture支持回调。

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

进程和线程

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

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

启动多线程

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

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:仅单线程执行的线程池。
  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 的任务可以定期反复执行。

 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 接口表示一个未来可能会返回的结果,它定义的方法有:

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

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

相关文章
|
3天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
1天前
|
Java 测试技术 Python
《手把手教你》系列基础篇(八十一)-java+ selenium自动化测试-框架设计基础-TestNG如何暂停执行一些case(详解教程)
【6月更文挑战第22天】本文介绍了如何在TestNG中不执行特定测试用例。当部分模块未准备好时,可以通过以下方式暂停测试:③使用`@Test(enabled=false)`注解来禁用测试用例。作者提供了一个Java Selenium自动化测试的示例,展示如何通过修改`enabled`参数控制测试方法的执行。代码中,`testSearch2()`方法被禁用,因此在测试运行时不执行。文章还包含了测试报告和执行过程的截图。
30 7
|
1天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十二)-java+ selenium自动化测试-框架设计基础-TestNG测试报告-上篇(详解教程)
【6月更文挑战第23天】TestNG 是一个用于自动化测试的 Java 框架,它自动生成测试报告,包括 HTML 和 XML 格式。报告可在 `test-output` 文件夹中找到。要创建测试用例,可创建一个实现了 `@Test` 注解的方法。通过 `testng.xml` 配置文件来组织和执行测试。默认报告包括测试结果、失败点和原因。用户还能实现 `ITestListener` 和 `IReporter` 接口来自定义报告和记录器。
17 2
|
1天前
|
Java
Java中,有两种主要的方式来创建和管理线程:`Thread`类和`Runnable`接口。
【6月更文挑战第24天】Java创建线程有两种方式:`Thread`类和`Runnable`接口。`Thread`直接继承受限于单继承,适合简单情况;`Runnable`实现接口可多继承,利于资源共享和任务复用。推荐使用`Runnable`以提高灵活性。启动线程需调用`start()`,`Thread`直接启动,`Runnable`需通过`Thread`实例启动。根据项目需求选择适当方式。
9 2
|
22小时前
|
Java
Java多线程中notifyAll()方法用法总结
Java多线程中notifyAll()方法用法总结
|
1天前
|
Java
在Java中,死锁是指两个或多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。
【6月更文挑战第24天】在Java并发中,死锁是多线程互相等待资源导致的僵局。避免死锁的关键策略包括:防止锁嵌套,设定固定的加锁顺序,使用`tryLock`带超时,避免无限等待,减少锁的持有时间,利用高级同步工具如`java.util.concurrent`,以及实施死锁检测和恢复机制。通过这些方法,可以提升程序的并发安全性。
6 1
|
3天前
|
安全 Java 程序员
Java多线程详解
Java多线程详解
|
21小时前
|
Java
java使用多线程编写服务端与客户端文件上传程序
java使用多线程编写服务端与客户端文件上传程序
4 0
|
20小时前
|
Java
java使用匿名内部类实现多线程
java使用匿名内部类实现多线程
3 0
|
3天前
|
Java API