当前位置: 首页 > JAVA > 正文

什么是多线程

先了解几个基本的概念

  • 进程:进程是操作系统进行资源分配和调度的一个独立单位,每个进程都有自己独立的地址空间,即它们拥有各自独立的内存空间。进程间通信比较复杂,如通过管道、信号、套接字等方式。如果一个进程崩溃,通常情况下不会影响到其他进程,因为它们彼此隔离。(当程序开始执行时,操作系统会为其创建一个进程)
  • 线程:线程是进程中的一个执行流,它是CPU调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存和文件句柄。线程有自己的程序计数器、栈和局部变量,但是它们可以访问相同的全局变量。线程之间的切换成本比进程之间要小得多,因为线程共享了很多资源。
  • 并发:在同一时刻,有多个指令在单个 CPU 上交替运行。
  • 并行:在同一时刻,有多个指令在多个 CPU 上同时执行。

多线程是指在一个程序中同时运行多个线程来执行不同的任务。

多线程的实现方式

  • 继承 Thread 类的方式实现

线程类

public class ThreadExtend extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("继承 Thread 类的方式实现多线程"+i);
        }
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) {
        Thread_extend threadExtend = new Thread_extend();
        threadExtend.start();
    }
}
  • 实现 Runnable 接口的方式实现

线程类

public class ThreadRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("通过实现 Runnable 接口实现多线程"+i);
        }
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) {
        ThreadRunnable threadRunnable = new ThreadRunnable();
        Thread thread = new Thread(threadRunnable);
        thread.start();
    }
}
  • 利用 Callable 接口和 Future 接口实现

实现类

public class ThreadCallable implements Callable<Integer> {
    private Integer sum = 0;
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 100; i++) {
            sum += i;
            System.out.println("利用 Callable 接口和 Future 接口实现多线程"+i);
        }
        return sum;
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadCallable threadCallable = new ThreadCallable();
        FutureTask<Integer> ft = new FutureTask<>(threadCallable);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println("多线程返回的结果="+ft.get());
    }
}

三种实现多线程的方法对比

方法 优点 缺点
继承 Thread 类 实现简单,可以直接使用 Thread 类中的方法 扩展性差,不能再继承其他类
实现 Runnable 接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用 Thread 类中的方法
实现 Callable 接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用 Thread 类中的方法

Thread 类的常用方法

方法名称 说明
String getName() 返回该线程的名称
void setName(String name) 设置线程名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
final int getPriority() 获取线程的优先级,默认5
void setPriority(int newPriority) 设置线程的优先级,范围1-10
final void setDaemon(boolean on) 设置为守护线程
public static void yield() 礼让线程,暂停当前正在执行的线程对象,并执行其他线程
public static void join() 线程插队,等待该线程终止

守护线程

在Java中,线程分为用户线程和守护线程两种类型。用户线程是应用程序中的自定义线程,它们执行程序中的特定任务。而守护线程则是服务其他线程的线程,它们通常用于执行后台任务,如内存管理、垃圾回收等。守护线程的一个显著特点是,当所有用户线程结束时,守护线程会自动结束,如果守护线程还在运行,JVM也会退出。

可以通过调用Thread.setDaemon(true)方法将线程设置为守护线程。需要注意的是,一旦一个线程被设置为守护线程,就不能再将其转换回用户线程。此外,守护线程的优先级通常较低,这意味着在资源分配时,用户线程会优先于守护线程获得CPU时间。

在实际应用中,守护线程常用于那些不是必须完成的任务,或者在主程序运行时需要一直运行的任务,比如内存管理和垃圾回收。这些任务对于程序的正常运行至关重要,但它们不需要特定的完成时间。

线程插队

线程类

public class ThreadExtend extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程执行"+i);
        }
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadExtend threadExtend = new ThreadExtend();
        threadExtend.start();
        //表示把 threadExtend 线程插入到当前线程之前
        //只有当 threadExtend 线程执行完毕,main主线程才会继续执行,而不是同时执行
        threadExtend.join();
        for (int i = 0; i < 100; i++) {
            System.out.println("main主线程"+i);
            Thread.sleep(1000);
        }
    }
}

线程的六种状态

线程状态。线程可以处于下列状态之一:

  • NEW
    至今尚未启动的线程处于这种状态。
  • RUNNABLE
    正在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED
    受阻塞并等待某个监视器锁的线程处于这种状态。
  • WAITING
    无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
  • TIMED_WAITING
    等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
  • TERMINATED
    已退出的线程处于这种状态。

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。

补充

官方文档对 RUNNABLE 状态的详细描述为:可运行线程的线程状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。

以上是官方文档对线程状态的描述,但是如果细分线程的状态可以分为 7 种,RUNNABLE 状态可以再细分为两种状态:就绪和运行。

线程的安全问题

场景引入

在多线程售票系统中,线程安全问题主要表现为多个线程可能同时访问和修改共享资源(如票的数量),导致数据不一致或出现错误。

具体来说,当多个线程同时执行售票操作时,它们可能会同时读取和修改票的数量,这可能导致以下问题:

  • 数据不一致性:如果多个线程同时读取票的数量,然后基于这个数量进行售票操作,可能会导致售出的票数超过实际库存。
  • 数据丢失:如果一个线程在修改票的数量时被另一个线程中断,那么它所做的修改可能会丢失,导致数据错误。
  • 顺序问题:在没有同步的情况下,线程执行的顺序是不确定的,这可能导致某些线程提前获得资源,而其他线程无法正常执行。

代码复现

线程类

public class ThreadExtend extends Thread {
    static int ticket = 100;

    @Override
    public void run() {
        for (;;) {
            if (ticket > 0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(currentThread().getName()+"还剩" + ticket-- + "张票");
            }else {
                break;
            }
        }
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadExtend threadExtend = new ThreadExtend();
        ThreadExtend threadExtend1 = new ThreadExtend();
        ThreadExtend threadExtend2 = new ThreadExtend();
        threadExtend.start();
        threadExtend1.start();
        threadExtend2.start();
    }
}

解决方案

  • 同步代码块
public class ThreadExtend extends Thread {
    static int ticket = 100;

    @Override
    public void run() {
        for (;;) {
            synchronized (ThreadExtend.class){
                if (ticket > 0){
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(currentThread().getName()+"还剩" + ticket-- + "张票");
                }else {
                    break;
                }
            }

        }
    }
}
  • 同步方法
public class ThreadExtend extends Thread {
    static int ticket = 100;

    @Override
    public void run() {
        for (; ; ) {
            if (extracted()) break;
        }
    }

    private synchronized static boolean extracted() {
        if (ticket > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(currentThread().getName() + "还剩" + ticket-- + "张票");
        } else {
            return true;
        }
        return false;
    }
}
  • lock 锁
public class ThreadExtend extends Thread {
    static int ticket = 100;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        for (; ; ) {
            lock.lock();
            try {
                if (ticket > 0) {
                    Thread.sleep(50);
                    System.out.println(currentThread().getName() + "还剩" + ticket-- + "张票");
                } else {
                    break;
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
}

死锁

死锁是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁的产生有四个必要条件,包括互斥条件、请求与保持条件、不剥夺条件和循环等待条件。
具体如下:

  • 互斥条件:一个资源每次只能被一个进程使用。如果另一进程请求该资源,那么请求进程必须等到该资源被释放为止。
  • 请求与保持条件:一个进程至少已经持有一个资源,且又提出了新的资源请求,而该资源已被其他进程占有。
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能被其他进程强行夺走,只能由获得该资源的进程自己释放。
  • 循环等待条件:存在一种进程资源的循环等待关系。

解决死锁的方法通常有两种:一种是预防,即破坏死锁的四个必要条件之一;另一种是避免,即使用算法来确保系统不会进入死锁状态。预防策略包括有序资源分配法和银行家算法等。避免策略则涉及到更复杂的算法,如死锁检测和恢复算法。

线程类

public class ThreadExtend extends Thread {
    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A 拿到 objA 锁,准备继续拿 objB 锁");
                    synchronized (objB) {
                        System.out.println("线程A 拿到 objB 锁,业务执行完毕");
                    }
                }
            } else if ("线程B".equals(getName())) {
                synchronized (objB) {
                    System.out.println("线程B 拿到 objB 锁,准备继续拿 objA 锁");
                    synchronized (objA) {
                        System.out.println("线程B 拿到 objA 锁,业务执行完毕");
                    }
                }
            }
        }
    }
}

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadExtend A =  new ThreadExtend();
        ThreadExtend B = new ThreadExtend();
        A.setName("线程A");
        B.setName("线程B");
        A.start();
        B.start();
    }
}

等待唤醒机制

等待唤醒机制是多线程编程中一种重要的同步机制,它允许一个或多个线程等待某个条件的发生,而其他线程可以在该条件满足时唤醒等待的线程。

在Java中,等待唤醒机制通常与Object类的wait()方法和notify()方法(以及它们的变体wait(long timeout)和notifyAll())一起使用。以下是这些机制的关键点:

  • 等待(Wait):当线程调用共享资源的wait()方法时,它会释放该对象的锁并进入等待状态。这意味着其他线程可以获取该对象的锁并修改资源的状态。
  • 唤醒(Notify):当一个线程改变了共享资源的状态并希望通知其他正在等待该资源的线程时,它可以调用notify()或notifyAll()方法。notify()只会随机唤醒一个正在等待的线程,而notifyAll()会唤醒所有等待的线程。
  • 线程间通信:这种机制常用于线程间的协作,例如生产者-消费者问题中,消费者线程可能在缓冲区为空时进入等待状态,而生产者线程在生产了新的数据后唤醒等待的消费者线程。
  • 与锁的关系:等待和唤醒操作通常在同步代码块或同步方法中使用,确保了在检查条件和改变条件之间不会有其他线程干扰。
    需要注意的是,Thread.sleep()方法虽然会让线程暂停执行,但它不会释放锁,这与wait()方法有本质的不同。

总的来说,等待唤醒机制是多线程编程中的一个基础概念,它允许线程在特定条件下挂起自己的执行,并在条件满足时恢复执行。掌握这一机制对于编写高效且正确的多线程程序至关重要。

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Consumer consumer = new Consumer();
        Producer producer = new Producer();
        consumer.start();
        producer.start();
    }
}

条件类

public class Desk {
    //是否可以消费
    public static boolean isExist;
    //可消费的总数
    public static int count = 10;
    //锁对象
    public static Object lock = new Object();
}

生产者线程

public class Consumer extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock){
                if (Desk.count == 0) {
                    break;
                } else {
                    if (!Desk.isExist) { //不可以消费,等待生产者生产
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {//唤醒生产者
                        System.out.println(Desk.count);
                        Desk.count--;
                        Desk.isExist = false;
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

消费者线程

public class Producer extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.isExist) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        System.out.println("生产");
                        Desk.isExist = true;
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

阻塞队列实现等待唤醒机制

测试类

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        Consumer consumer = new Consumer(queue);
        Producer producer = new Producer(queue);
        consumer.start();
        producer.start();
    }
}

生产者线程

public class Producer extends Thread {
    ArrayBlockingQueue<String> queue;

    public Producer(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put("test");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("放入");
        }
    }
}

消费者线程

public class Consumer extends Thread {
    ArrayBlockingQueue<String> queue;

    public Consumer(ArrayBlockingQueue<String> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            String take = null;
            try {
                take = queue.take();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(take);
        }
    }
}

线程池

Java线程池是一种管理线程资源的机制,它可以提高程序的响应速度、降低资源消耗,并增强线程的可管理性。

Java线程池的主要优势在于它能够减少在执行大量异步任务时创建和销毁线程的开销。线程池通过复用已经存在的线程来执行新的任务,从而避免了频繁创建新线程所带来的性能损耗。此外,线程池还能够限制同时运行的线程数量,防止系统因过载而崩溃。

主要流程

  • 创建一个空的线程池
  • 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归化给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程
  • 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
  1. 通过 Executors 工厂类提供的静态方法来快速创建线程池
方法 说明
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
public static ExecutorService newCachedThreadPool() 创建一个没有上限的 线程池

代码实现

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个没有上限的线程池
        //ExecutorService executorService = Executors.newCachedThreadPool();
        //创建一个有上限的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //提交任务
        executorService.submit(new ThreadRunnable());
        executorService.submit(new ThreadRunnable());
        executorService.submit(new ThreadRunnable());
        executorService.submit(new ThreadRunnable());
        executorService.submit(new ThreadRunnable());
        //关闭线程池
        executorService.shutdown();
    }
}
  1. 通过直接实例化ThreadPoolExecutor来自定义线程池的参数

拒绝策略

策略名称 描述
ThreadPollExecutor.AbortPolicy 默认的拒绝策略,当线程池和队列都满了时,直接抛出RejectedExecutionException异常。
ThreadPollExecutor.CallerRunsPolicy 仅用调用者运行一些任务,当线程池和队列都满了时,将任务在调用者的线程中执行,包括这个任务的后续任务。
ThreadPollExecutor.DiscardOldestPolicy 抛弃最老的任务请求,当线程池和队列都满了时,抛弃队列中最老的任务请求,然后再次尝试执行任务(重复此过程直到能够执行或者抛出异常)。
ThreadPollExecutor.DiscardPolicy 不处理,当线程池和队列都满了时,直接丢弃任务,也不提供任何通知或抛出异常。

代码实现

public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int corePoolSize = 5; // 核心线程数
        int maximumPoolSize = 10; // 最大线程数
        long keepAliveTime = 60L; // 空闲线程存活时间(单位:秒)
        TimeUnit unit = TimeUnit.SECONDS; // 存活时间的单位
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10); // 任务队列
        ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        // 提交任务到线程池
        threadPool.submit(new ThreadRunnable());
        threadPool.submit(new ThreadRunnable());
        threadPool.submit(new ThreadRunnable());
        threadPool.submit(new ThreadRunnable());
        threadPool.submit(new ThreadRunnable());

        // 关闭线程池
        threadPool.shutdown();
    }
}

一篇文章学会Java多线程技术:等您坐沙发呢!

发表评论

gravatar

快捷键:Ctrl+Enter