目录

前言

1.入门多线程

1.1. 线程、进程、多线程、线程池

1.2.并发、串行、并行

1.3. 线程的实现方式

1.3.1. 继承 Thread 类

1.3.2. 实现 Runnable 接口

1.3.3. 使用 Callable 和 Future

1.3.4. 使用线程池

1.4.线程的状态

1.5. 线程常用方法

1.5.1 sleep()

1.4.2 join()

1.5.3 yield()

1.5.4.wait() 和 notify() 

1.5.5.-interrupt()方法和stop()方法 

1.5.6. setPriority (int newPriority)、getPriority()

1.6.线程调度

2.进阶多线程

2.1.多线程引发问题

2.2.多线程解决方法

2.2.1. 锁机制

2.2.3. 线程池

2.2.4. 并发工具类

3.面试题

3.1.sleep()和wait()有什么区别?

3.2.如何停止一个处在运行态的线程

3.3.interrupt()、interrupted()、isInterrupted()方法的区别

3.4.系统创建的线程过多会有什么影响

4.写在最后

鸣谢


前言

多线程已经成为一种常见的编程模式,广泛应用于各种不同类型的应用程序中。

本篇博客文章中,我们将会探讨多线程编程的相关知识和技巧。通过代码示例和实际应用案例来深入了解多线程的具体实现和应用方法,帮助更好地掌握多线程编程技术,提高程序效率和性能。后期随学习深入还会补充修改。

1.入门多线程

1.1. 线程、进程、多线程、线程池

线程

在计算机科学中,线程是指进程中的一个单独的执行路径。一个进程可以包含多个线程,每个线程都可以并行执行不同的任务。多线程编程是指在同一时间内运行多个线程来完成多个任务。

多线程

多线程是指在同一时间内运行多个线程来完成多个任务。多线程提高程序的性能和响应速度。但是增加了代码的复杂性,同时需要考虑线程安全和死锁等问题。

线程池

线程池是一组预先创建的线程,它们可以被重复使用来执行多个任务。使用线程池可以避免在创建和销毁线程时产生额外的开销,从而提高程序的性能。Java 中提供了 Executor 框架来实现线程池。

进程

进程即一段程序的执行过程,是计算机中的程序关于某数据集合上的一次运行活动,是系统分配资源的最小单位

线程与进程的区别

  • 根本区别:进程是操作系统资源分配的最小单位,而线程是处理任务调度和执行的最小单位
  • 资源开销:每个进程都有单独的代码和数据空间,进程之间的切换会有较大的开销;线程可以开做轻量级的进程,同一类线程共享代码和数据空间,每个线程有自己独立的运行栈和程序计数器,线程之间的切换开销较小
  • 包含关系:一个进程可以包含多个线程,这些线程可以共享同步资源
  • 内存分配:同一个进程中的所有线程共享本进程的地址空间和资源,而进程之间的地址空间和资源相互独立
  • 影响关系:一个进程崩溃后,不会影响其他进程;而一个线程崩溃后其他线程也会收到影响,从而整个进程崩溃
  • 执行过程:每个独立的进程都有程序运行的入口,顺序执行序列和出口,但线程不能独立执行,必须依存于进程

1.2.并发、串行、并行

并发、串行、并行概念

  • 并发:指多个任务在同一时间段同一个CPU上交替执行,看起来好像是同时执行的。例如,多个线程在同一时间内运行。
  • 串行:指多个任务按照顺序依次执行,一个任务完成后才能执行下一个任务。例如,单线程程序就是串行执行的。
  • 并行:多个处理器或多核处理器同时处理多个任务,必须需要有多个处理器或者多核 CPU 才能实现,否则只能是并发。例如,多个线程在不同的处理器或者 CPU 上运行。

并发、串行、并行的区别

  • 执行方式:并发和串行都在单个处理器上执行,但是并发是多个任务交替执行,串行是按照顺序依次执行;并行需要多个处理器或多核 CPU 才能实现。
  • 性能:并发和并行都可以提高程序的性能和响应速度,但是并发需要考虑线程安全和死锁等问题;串行虽然简单稳定,但是无法充分利用多核 CPU 的优势。
  • 实现方式:并发可以使用多线程技术实现;串行只能使用单线程实现;并行需要多个处理器或多核 CPU 才能实现。

并发编程的三要素

  • 原子性:指一个或多个操作要么全部执行成功,要么全部执行失败
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到
  • 有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)

多线程和并发

多线程是指在同一时间内运行多个线程来完成多个任务。多线程可以提高程序的性能和响应速度,因为它们可以同时执行多个任务。

并发是指在同一时间内执行多个任务的能力。并发可以通过使用多线程来实现,但也可以通过其他方式实现,例如使用异步编程或事件驱动编程。

因此,多线程是实现并发的一种方式,但并发不一定需要使用多线程。另外,多线程编程中需要考虑的问题,例如线程安全和死锁等,在并发编程中同样需要考虑

1.3. 线程的实现方式

        多线程是提高程序性能和响应速度的重要手段,Java 中有多种实现方式, 

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 使用 Callable 和 Future
  • 使用线程池

1.3.1. 继承 Thread 类

  • 介绍:通过继承 Thread 类来实现多线程。
  • 示例代码:展示如何继承 Thread 类并重写 run() 方法。
  • 优点:实现简单,易于理解。
  • 缺点:无法继承其他类,因为 Java 不支持多重继承。
class MyThread extends Thread {
    public void run() {
        // 执行需要的代码
    }
}

MyThread thread = new MyThread();
thread.start();

1.3.2. 实现 Runnable 接口

  • 介绍:通过实现 Runnable 接口来实现多线程。
  • 示例代码:展示如何实现 Runnable 接口并重写 run() 方法。
  • 优点:可以继承其他类,因为 Java 支持实现多个接口。
  • 缺点:需要创建 Thread 对象来启动线程。
  • 步骤:
     - 自定义线程类实现Runnable接口
     - 实现run()方法,编写线程体
     - 创建线程对象,调用start()方法启动线程(启动后不一定立即执行,抢到CPU资源才能执行)
class MyRunnable implements Runnable {
    public void run() {
        // 执行需要的代码
    }
}

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

1.3.3. 使用 Callable 和 Future

  • 介绍:通过使用 Callable 和 Future 接口来实现多线程。
  • 示例代码:展示如何使用 Callable 和 Future 接口来创建和启动线程。
  • 优点:可以获取线程执行的返回值。
  • 缺点:相比于前两种方式,实现稍微复杂一些。
  •  步骤:
     - 实现Callable接口,先要返回值类型
     - 重写call()方法,需要抛出异常
     - 创建目标对象
     - 创建执行服务:ExecutorService ser = Executor.newFixedThreadPool(1);
     - 提交执行:Future<Boolean> res = ser.submit(t1);
     - 获取结果:boolean r1 = res.get();
     - 关闭服务:ser.shutdownNow();
import java.util.concurrent.*;

// 自定义线程对象,实现Callable接口,重写call()方法
public class MyThread implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException,
        InterruptedException {
        // main线程,主线程

        // 创建线程实现类对象
        MyThread thread = new MyThread();
        MyThread thread2 = new MyThread();

        // 创建执行服务,参数是线程池线程数量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交执行
        Future<Boolean> res = ser.submit(thread);
        Future<Boolean> res2 = ser.submit(thread2);
        // 获取结果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 关闭服务
        ser.shutdownNow();
    }
}

1.3.4. 使用线程池

  • 介绍:通过使用线程池来实现多线程。
  • 示例代码:展示如何使用 Executor 和 ExecutorService 接口来创建和管理线程池。
  • 优点:可以重用线程,避免了频繁创建和销毁线程的开销。
  • 缺点:需要对线程池的大小进行合理配置,否则可能会导致性能问题。
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new Runnable() {
    public void run() {
        // 执行需要的代码
    }
});

1.4.线程的状态

  • 新建(New):当线程对象创建后,线程处于新建状态。
  • 运行(Runnable):当调用线程的 start() 方法后,线程处于就绪状态,等待 CPU 调度执行。
  • 阻塞(Blocked):当线程等待某个条件(如 I/O 操作、锁)时,线程处于阻塞状态。
  • 等待(Waiting):当线程等待某个条件的唤醒(如调用 wait() 方法)时,线程处于等待状态。
  • 超时等待(Timed Waiting):当线程等待某个条件的唤醒,但是等待一定的时间后会自动唤醒(如调用 sleep() 方法或者带超时参数的 wait() 方法)时,线程处于超时等待状态。
  • 终止(Terminated):当线程执行完成或者出现异常时,线程处于终止状态。

1.5. 线程常用方法

  • start():启动线程。
  • run():线程执行的代码。
  • join():等待线程执行完毕。
  • sleep():使线程休眠一段时间。
  • interrupt():中断线程的执行。
  • wait() 使当前线程等待另一个线程发出通知。
  • notify() 通知等待的线程继续执行。
  • setPriority (int newPriority)、getPriority() 改变、获取线程的优先级。

1.5.1 sleep()

sleep() 方法可以使当前线程暂停指定的时间。例如:

try {
    Thread.sleep(1000); // 暂停 1 秒钟
} catch (InterruptedException e) {
    e.printStackTrace();
}

1.4.2 join()

join() 方法可以等待指定的线程执行完毕。例如:

Thread thread1 = new Thread(() -> {
    // 执行需要的代码
});

Thread thread2 = new Thread(() -> {
    // 执行需要的代码
});

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

try {
    thread1.join(); // 等待 thread1 执行完毕
    thread2.join(); // 等待 thread2 执行完毕
} catch (InterruptedException e) {
    e.printStackTrace();
}

1.5.3 yield()

暂停当前正在执行的线程对象,并执行其他线程,yield() 方法可以让出 CPU 时间片,使其他线程有机会运行。

例如:

Thread.yield();

yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

1.5.4.wait() 和 notify() 

在多线程程序中,可以使用 wait()notify() 方法来协调多个线程之间的操作。wait() 方法可以使当前线程等待另一个线程发出通知,而 notify() 方法可以通知等待的线程继续执行。

public class Message {
    private String content;
    private boolean available = false;

    public synchronized String getContent() {
        while (!available) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        available = false;
        notifyAll();
        return content;
    }

    public synchronized void setContent(String content) {
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.content = content;
        available = true;
        notifyAll();
    }
}

public class Main {
    public static void main(String[] args) {
        Message message = new Message();
        // 创建两个线程来发送和接收消息
        new Thread(() -> {
            message.setContent("Hello");
        }).start();
        new Thread(() -> {
            System.out.println(message.getContent()); // 输出 Hello
        }).start();
    }
}

1.5.5.-interrupt()方法和stop()方法 

- JDK提供的stop方法已废弃,不推荐使用。

- 推荐线程自动停止下来,建议使用一个标识位变量进行终止,当flag=false时,则终止线程运行。

public class DemoInterrupt {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
        t.interrupt();
        //        t.stop(); //强行终止线程
        //缺点:容易损坏数据  线程没有保存的数据容易丢失
    }
}
class MyRunnable2 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            // 睡眠1天
            Thread.sleep(1000 * 60 * 60 * 24);
        } catch (InterruptedException e) {
        //            e.printStackTrace();
        }
        //1天之后才会执行这里
        System.out.println(Thread.currentThread().getName() + "---> end");
 
    }
}

1.5.6. setPriority (int newPriority)、getPriority()

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。 - 线程的优先级用数据表示,范围1~10。 - 线程的优先级高只是表示他的权重大,获取CPU执行权的几率大。 - 先设置线程的优先级,在执行start()方法

public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程优先级:"
            + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        Thread thread2 = new Thread(myThread,"b");
        Thread thread3= new Thread(myThread,"c");
        Thread thread4= new Thread(myThread,"d");
        thread3.setPriority(Thread.MAX_PRIORITY);
        thread.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        thread4.setPriority(8);
        thread.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

结果如下:

c线程优先级:10
b线程优先级:5
a线程优先级:1
d线程优先级:8

1.6.线程调度

  • 线程调度模型

    • 均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。

    • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。

    Java中JVM使用的就是抢占式调度模型

  • getPriority():获取线程优先级

  • setPriority:设置线程优先级

2.进阶多线程

2.1.多线程引发问题

多线程编程能够提高程序的性能和响应能力,但同时也会带来一些问题。主要包括以下几个方面:

  1.竞态条件(Race Condition):当多个线程同时访问共享资源时,由于线程执行顺序的不确定性,可能会导致程序的输出结果出现错误。例如,多个线程同时对一个计数器进行自增操作,如果没有进行同步,可能会导致计数器的值不正确。

        可以使用 synchronized 关键字来解决竞态条件问题。

  2.死锁(Deadlock):当多个线程相互等待对方释放所占用的资源时,可能会陷入死锁状态,无法继续执行。例如,线程 A 占用了资源 1,等待资源 2,而线程 B 占用了资源 2,等待资源 1,两个线程都无法继续执行。

        可以使用锁和条件变量来解决死锁问题。

  3.饥饿(Starvation):当某些线程由于竞争共享资源失败而无法继续执行时,可能会出现饥饿问题。例如,如果一个线程在一个高负载的系统中请求资源,它可能会等待很长时间才能获得所需的资源。

       4.上下文切换:当多个线程同时运行时,操作系统需要进行上下文切换,这会消耗一定的系统资源。如果线程数量过多,上下文切换的开销可能会超过程序本身的开销

        5.线程安全:在多线程程序中,需要确保共享资源的安全性。可以使用锁和原子变量来实现线程安全。

        6.性能优化:在多线程程序中,需要考虑性能优化问题。可以使用线程池和并发集合来提高程序性能。

2.2.多线程解决方法

2.2.1. 锁机制

  • 定义:锁机制是解决多线程之间互斥访问共享资源的一种方式。
  • 实现方式:使用 synchronized 关键字、ReentrantLock 类或 ReadWriteLock 接口等实现锁机制。
  • 常用工具:Lock、Condition、Semaphore、ReadWriteLock 等。

2.2.3. 线程池

  • 定义:线程池是管理和调度多个线程的一种机制,可以避免频繁创建和销毁线程带来的性能开销。
  • 实现方式:使用 Executors 类或 ThreadPoolExecutor 类创建和管理线程池。
  • 常用参数:核心线程数、最大线程数、任务队列、拒绝策略等。

2.2.4. 并发工具类

  • 定义:并发工具类是解决并发编程中常见问题的一种工具,例如阻塞队列、计数器、信号量等。
  • 实现方式:使用 Java.util.concurrent 包中提供的工具类实现并发编程。
  • 常用工具:ArrayBlockingQueue、CyclicBarrier、Semaphore、CountDownLatch 等。

3.面试题

3.1.sleep()和wait()有什么区别?

两者都可暂停当前线程

  • 所在类不同:sleep()时Thread类的静态方法,wait()是Object类的方法
  • 释放锁:sleep()不会释放锁,wait()会释放锁
  • 用途不同:wait()通常用于线程间通信,sleep()通常用于暂停线程
  • 结果不同:sleep()方法执行完成后,线程会再次进入就绪态;wait()方法被notify()唤醒后,线程会进入同步队列重新抢占锁

3.2.如何停止一个处在运行态的线程

  1. 该线程的run()方法执行结束后线程自动终止
  2. 使用stop()方法强制终止,但一般很少这么用
  3. 使用interrupt()方法中断线程(其流程为,设置一个中断标志位,调用interrupt()方法通知系统请求关闭线程,待系统在适合的时间自行中断)

3.3.interrupt()、interrupted()、isInterrupted()方法的区别

  • interrupt()方法和isInterrupted()方法都是实例方法,通过Thread对象调用;interrupted()则是静态方法,通过Thread类调用,三个方法都是与线程中断有关的
  • interrupt()方法:用来设置中断标志位,通知系统请求结束线程,由系统决定具体何时中断

此时如果线程在阻塞状态:
那么就会抛出InterruptedException异常,并重置中断标志位

如果线程不在阻塞状态:
使用Thread.interrupted()判断当前中断标志位是否被设置,并重置中断标志位
使用Thread.currentThread.isInterrupted()判断当前中断标志位是否被设置,不重置中断标志位

3.4.系统创建的线程过多会有什么影响

  • 线程的生命周期开销非常高
  • 消耗过多的CPU
  • 降低JVM的效率

4.写在最后

以上就是我对多线程的个人简介,后续会不断完善更新,与大家共勉

鸣谢

[1] https://blog.csdn.net/zdl66/article/details/126297036

[2] https://blog.csdn.net/m0_46233999/article/details/118702235

[3] https://blog.csdn.net/qq_29141913/article/details/125964815#3_16

[4] https://blog.csdn.net/YQQAGH178/article/details/119828128

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐