【多线程及高并发 二】线程基础及线程中断同步

发布时间:2023年12月25日

👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列MySQL系列Redis系列Leetcode算法系列GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦??
?时间是条环形跑道,万物终将归零,亦得以圆全完美


多线程及高并发系列

线程(Thread):线程是进程内的执行单元,它是操作系统调度的最小单位。一个进程可以包含多个线程,它们共享进程的资源。线程之间可以并发执行,共享内存空间,因此可以更高效地完成多个任务。Java 中的线程由 Java 虚拟机(JVM)进行管理和调度

多线程的作用:

  1. 提高程序性能:通过利用多核处理器或多处理器系统的并行性,多线程可以同时执行多个任务,从而提高程序的处理能力和执行效率
  2. 提升用户体验:在需要进行耗时操作的情况下,使用多线程可以避免长时间的阻塞,保持用户界面的响应性
  3. 实现异步编程:多线程可以实现异步编程模型,通过在后台执行任务,提供更好的用户体验和系统响应能力
  4. 支持并发处理:在服务器端应用程序中,多线程可以帮助同时处理多个客户端请求,提高系统的并发性和吞吐量
  5. 充分利用资源:多线程可以充分利用计算机的硬件资源,例如 CPU、内存和磁盘等

多线程编程也带来了一些挑战,例如线程安全性数据同步共享资源管理等问题。Java 提供了丰富的并发编程工具和库,如线程类(Thread)、线程池(ThreadPoolExecutor)、锁(Lock)、原子类(Atomic)等,帮助开发人员更方便地进行多线程编程。

进程/线程/虚拟线程

  • 进程(Process):进程是计算机系统中运行的程序的实例。它是资源分配的最小单位,包括内存空间、文件句柄、打开的网络连接等
  • 操作系统线程(OS Thread):线程由操作系统管理,是进程内的执行单元,它是操作系统调度的最小单位
  • 平台线程(Platform Thread):Java.Lang.Thread类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射
  • 虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例java.lang.VirtualThread这个类
  • 载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程

虚拟线程(Virtual Thread)它不与特定的操作系统线程相绑定。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。**这意味着许多虚拟线程可以在同一个平台线程上运行他们的 Java 代码,共享同一个平台线程。详见虚拟线程原理及性能分析

线程状态

线程状态转换

  • 新建状态(New):当线程对象被创建时,它处于新建状态。此时线程尚未启动,还未调用start()方法。
  • 运行状态(Runnable):在线程启动后,它进入运行状态。线程可以在多个线程中竞争处理器资源,但具体的执行顺序由调度器决定。
  • 阻塞状态(Blocked):线程在某些条件下暂停执行,进入阻塞状态。常见的情况包括等待获取锁、等待I/O操作完成、等待其他线程的通知等。当条件满足时,线程会重新进入就绪状态。
  • 等待状态(Waiting):线程进入等待状态是出于某些条件的需要,线程会主动停止执行,直到满足特定的条件才会被唤醒。线程可以通过调用Object.wait()、Thread.join()或LockSupport.park()等方法进入等待状态。
  • 超时等待状态(Timed Waiting):线程在特定的时间范围内等待,如果在指定的时间内未满足条件,线程会自动唤醒。线程可以通过调用Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)或LockSupport.parkNanos()等方法进入超时等待状态。
  • 终止状态(Terminated):线程执行完任务或者由于异常等原因终止执行,进入终止状态。一旦线程进入终止状态,它将不再执行

线程使用方式

Java中有三种常见的线程使用方式:

  1. 继承 Thread 类:适合简单的线程任务,不需要额外的线程控制
  2. 实现 Runnable 接口:适合于需要执行的任务不需要返回结果的情况
  3. 实现 Callable 接口:适合需要执行任务并且获取返回结果的情况

实现RunnableCallable接口的类实际上是任务,最后还需要通过Thread来执行

继承 Thread

创建一个继承自Thread类的子类,重写其run()方法。然后可以创建该子类的实例,并调用start()方法来启动线程

这种方式适合简单的线程任务,不需要额外的线程控制

public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

实现 Runnable 接口

创建一个实现了Runnable接口的类,重写其run()方法。然后可以创建一个Thread对象,将该实现类的实例作为参数传递给Thread构造函数。最后调用Thread对象的start()方法来启动线程

这种方式适合于需要执行的任务不需要返回结果的情况

public class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

实现 Callable 接口

创建一个实现了Callable接口的类,重写其call()方法。然后可以创建一个FutureTask对象,将该实现类的实例作为参数传递给FutureTask构造函数,最后通过FutureTask对象可以获取任务执行的结果

这种方式适合需要执行任务并且获取返回结果的情况

public class MyCallable implements Callable<String> {
    public String call() {
        // 线程执行的代码
        return "Hello, World!";
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

线程机制

在 Java 中,Thread 类提供了方法及工具来控制线程的行为

  • 基础方法:setDaemon(boolean on)yield()sleep(long millis)
  • 线程中断:interrupted()
  • 线程同步: synchronizedReentranLock
  • 线程协助:join()wait()notify()notifyAll()

基础方法

setDaemon

作用:将线程设置为守护线程(daemon thread)或用户线程(user thread)

  • 守护线程是在后台提供服务的线程。当所有的用户线程结束时,守护线程也会自动结束。典型的守护线程包括垃圾回收线程(Garbage Collector)
  • 用户线程是在前台执行的线程,不会影响 JVM 的关闭。守护线程的存在并不会阻止 JVM 退出
public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon Thread is running");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        daemonThread.setDaemon(true); // 设置为守护线程
        daemonThread.start();

        System.out.println("Main Thread is exiting");
    }
}

守护线程daemonThread,它会不停地输出一条消息。直到主线程(即 main 方法)退出时,守护线程也会随之自动结束

yield

作用:提示调度器当前线程愿意放弃当前的 CPU 时间片,让其他具有相同优先级的线程执行。

调用yield()方法不会导致线程进入阻塞状态,而是将线程从运行状态转换为就绪状态

yield()方法是对线程调度器的一个建议,它在一定程度上提高了线程之间的公平性

public class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1: " + i);
                Thread.yield();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2: " + i);
                Thread.yield();
            }
        });

        thread1.start();
        thread2.start();
    }
}

尽可能交替执行输出,但不保证,因为yield()的线程也可能又被CPU调度

sleep

作用:使当前线程暂停执行指定的时间,进入阻塞状态。
参数:millis指定线程休眠的时间(以毫秒为单位)

sleep()方法会暂时释放 CPU,使得其他线程有机会执行。在指定的时间过去后,线程会重新进入就绪状态,等待重新获得 CPU 执行

public class SleepExample {
    public static void main(String[] args) {
        System.out.println("Before sleep");
        
        try {
            Thread.sleep(3000); // 休眠 3 秒钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("After sleep");
    }
}

线程中断

interrupt

Java 提供了 Thread 类的interrupt()方法来中断线程的执行。

调用interrupt()方法会将线程的中断标志位设置为true

  • 如果线程处于阻塞状态(如调用了sleep()wait()join()方法),会抛出InterruptedException异常并清除中断标志位
  • 如果线程处于非阻塞状态,在适当的时机需要检查线程的中断标志位,并采取相应的处理逻辑
interrupted

作用:静态方法,检查当前线程的中断状态,并重置中断标志位。通常在当前线程需要处理中断状态时使用

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread2 = new MyThread2();
        thread2.start();
        thread2.interrupt();
    }
}

如果一个线程的run()方法执行一个无限循环,并且没有执行sleep()等会抛出 InterruptedException 的操作,那么调用线程的interrupt()方法就无法使线程提前结束。此时需要interrupted()方法来判断线程是否处于中断状态,从而提前结束线程

线程同步

synchronized

Java 中的synchronized关键字用于实现线程同步,确保在同一时间只有一个线程可以进入被 synchronized 修饰的方法或代码块,从而防止多个线程同时访问共享资源,避免出现数据竞争和并发访问的问题

synchronized原理分析、锁位置、锁状态及锁升级详解见【多线程及高并发 二】volatile & synchorized 详解

  1. 修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁
  2. 修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

synchronized关键字在JDK 1.5 前本质上是一把悲观锁。JDK 1.5 之后进行了优化,引入了锁升级的概念。在多线程竞争不激烈的情况下,锁会从无锁状态逐渐升级为偏向锁、轻量级锁,最后升级为重量级锁,以提高性能

ReentranLock

ReentrantLock是 Java 提供的可重入锁(Reentrant Lock)实现,它是在java.util.concurrent.locks包中的一个类。与传统的synchronized关键字相比,ReentrantLock提供了更多可编程的灵活性和功能,例如可重入性、公平性、条件变量和更精细的线程控制

  1. 可重入性:ReentrantLock 是可重入锁,意味着同一个线程可以多次获取同一个锁而不会造成死锁
  2. 加锁和解锁:使用 ReentrantLock,可以使用lock()方法进行加锁,使用unlock()方法进行解锁
  3. 公平性和非公平性:ReentrantLock 可以构造为公平锁或非公平锁。公平锁会按照线程请求锁的顺序分配锁,而非公平锁则允许插队,可能会导致某些线程长时间等待
  4. Condition条件变量:ReentrantLock 提供了与 CONDITION 相关联的条件变量,用于实现更复杂的线程通信和同步
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int count = 0;

    public void increment() {
        lock.lock(); // 加锁
        try {
            count++;
            System.out.println("Incremented: " + count);
            condition.signalAll(); // 唤醒等待的线程
        } finally {
            lock.unlock(); // 解锁
        }
    }

    public void decrement() throws InterruptedException {
        lock.lock(); // 加锁
        try {
            while (count == 0) {
                condition.await(); // 等待条件满足
            }
            count--;
            System.out.println("Decremented: " + count);
        } finally {
            lock.unlock(); // 解锁
        }
    }
    
    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        
        Thread incrementThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.increment();
            }
        });

        Thread decrementThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    example.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        incrementThread.start();
        decrementThread.start();
    }
}

在上述示例中创建了一个ReentrantLock对象lock和一个与之关联的Condition对象condition

  • increment()方法使用lock加锁,递增count的值,并唤醒等待的线程
  • decrement()方法使用lock加锁,当count为 0 时,调用condition.await()方法等待条件满足,否则递减count的值

线程协作

在Java中,Thread类提供了几个用于线程间协作的方法,包括join()wait()notify()notifyAll()。这些方法用于实现线程的等待、唤醒和协调操作

Thread 类的wait()notify()notifyAll()方法是与对象的监视器(monitor)相关联的,而不是直接与 Thread 类相关联。这些方法是基于对象的锁机制实现的,用于线程间的协调和通信

join()方法用于等待调用线程完成其执行,然后再继续执行当前线程

  • 调用某个线程的join()方法会使当前线程进入阻塞状态,直到被调用线程执行完毕
  • 如果在join()方法中指定了超时时间,当前线程最多会等待指定的时间,然后继续执行
  • join()方法通常与多线程的任务分割和结果合并中使用

wait()notify()notifyAll()这三个方法是用于线程间的等待和唤醒机制,需要在同步代码块或同步方法中使用。被唤醒的线程会重新竞争对象的锁,一旦获得锁,就可以继续执行

  • wait()方法使当前线程进入等待状态,放弃对象的锁,并等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它
  • notify()方法唤醒在相同对象上调用wait()方法并进入等待状态的单个线程。
  • notifyAll()方法唤醒在相同对象上调用wait()方法并进入等待状态的所有线程

wait()方法在调用前需要先获得锁,否则会抛出IllegalMonitorStateException异常

public class ThreadCooperationExample {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 1 starts");
                    Thread.sleep(2000);
                    System.out.println("Thread 1 notifies");
                    lock.notify(); // 唤醒等待的线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Thread 2 starts");
                    lock.wait(); // 等待被唤醒
                    System.out.println("Thread 2 continues");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start();
        thread2.start();

        thread1.join(); // 等待 thread1 执行完毕
        thread2.join(); // 等待 thread2 执行完毕

        System.out.println("Main thread finishes");
    }
}

在上述示例中创建了两个线程 thread1 和 thread2。thread1 在执行过程中调用lock.notify()方法唤醒等待的线程,而 thread2 在执行过程中调用lock.wait()方法等待被唤醒。main 线程使用join()方法等待 thread1 和 thread2 执行完毕后再继续执行


参考资料:

  1. Java 并发编程实战
  2. 虚拟线程原理及性能分析
文章来源:https://blog.csdn.net/why_still_confused/article/details/135175519
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。