loading

Java - 多线程和等待唤醒机制

前言

单线程(Single Thread)

定义:单线程是指程序中只有一个执行线程。在任何给定的时刻,程序只执行一个任务。

(1)优点

  1. 简单:单线程程序通常更容易编写和调试,因为不涉及到并发问题。
  2. 逻辑清晰:程序执行的顺序更易于理解。

(2)缺点

  1. 效率低:在某些情况下,单线程可能无法充分利用计算机的多核处理能力,导致性能瓶颈。

多线程(Multithreading)

定义:多线程是指程序中包含多个执行线程,每个线程执行独立的任务。这些线程可以并发运行,从而提高程序的性能。

(1)优点

  1. 并发性:允许多个任务同时执行,提高程序的整体性能。
  2. 响应性:在某些情况下,多线程可以提高程序对外部事件的响应速度。

(2)缺点

  1. 复杂性:多线程编程涉及到共享资源、同步和互斥等问题,可能增加代码的复杂性。
  2. 调试困难:由于并发问题,调试可能变得更加困难,因为程序的执行顺序变得不确定。

基本使用

  1. 第一种:继承 Thread 类,重写 run 方法;
  2. 第二种:实现 Runnable 接口,传递给 Thread 构造函数。
  3. 共同:通过 start 函数启动线程。
file:[多线程基本使用]
public class Main {

    public static void main(String[] args) {
        System.out.println("main thread start");
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("main thread end");
    }

}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("my runnable running...");
        }
    }

}

tip:[start]
直接调用 run() 方法只会在当前线程中执行,并不会启动新线程。
tip:[end]

线程进入,不妨碍主线程执行,如果需要阻塞主线程,等待线程执行完成之后再执行主线程下面的语句,可以通过 join 来实现。以下是执行结果:

main thread start
my runnable running...
my runnable running...
my runnable running...
my runnable running...
my runnable running...
......
......
......
my runnable running...
my runnable running...
my runnable running...
my runnable running...
my runnable running...
main thread end

同步和互斥

同步(Synchronization)和互斥(Mutual Exclusion)确保多个线程之间的正确协同工作,以数据不一致的问题。

  1. 同步:指的是多个线程按照顺序执行,以确保数据的正确性和一致性。在多线程环境中,多个线程可能同时访问共享资源,需要对线程进行同步。
  2. 互斥:指的是同一时刻只允许一个线程访问共享资源,其他线程必须等待。互斥机制确保在同一时间内只有一个线程对关键部分进行访问,从而避免了多个线程同时修改共享数据的问题。

最经典的就是抢票问题,假如有三个线程(A、B、C)抢夺 100 张票,可能会存在同步和互斥问题,遇到共享数据(100张票)可能会因为数据不一致导致最终票超卖的情况。

tip:[start]
Java 多线程是随机执行、交替执行的并发执行,而不是并行执行。
tip:[end]

因此,我们需要在抢票的逻辑加上一个锁,把这一个地方圈起来,避免出现资源竞争导致的数据不安全问题(数据不一致)。

file:[抢票演示]
public class Main {

    public static void main(String[] args) {
        TicketCounter ticketCounter = new TicketCounter();

        new Thread(() -> {
            while (true) {
                if (ticketCounter.buy()) break;
            }
        }, "Thread-A").start();

        new Thread(() -> {
            while (true) {
                if (ticketCounter.buy()) break;
            }
        }, "Thread-B").start();

        new Thread(() -> {
            while (true) {
                if (ticketCounter.buy()) break;
            }
        }, "Thread-C").start();
    }

}

class TicketCounter {

    private static int totalTickets = 100;

    public synchronized boolean buy() {
        if (totalTickets == 0) {
            System.out.println("票已售罄");
            return true;
        } else {
            try {
                Thread.sleep(10); // 模拟抢票过程中的延迟
            } catch (InterruptedException e) {
                e.fillInStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " 抢到一张票,剩余:" + (--totalTickets));
            return false;
        }
    }

}

给一个对象中的方法加上 synchronized,就实现了同步和互斥,在多个线程调用这个对象的方法时,里面的数据总是安全的。假如 A 线程执行完结束之后,锁被释放了,那么线程 A、B、C 都可能获取到锁,如此往复。

以下是执行结果:

Thread-B 抢到一张票,剩余:4
Thread-B 抢到一张票,剩余:3
Thread-C 抢到一张票,剩余:2
Thread-C 抢到一张票,剩余:1
Thread-C 抢到一张票,剩余:0
票已售罄
票已售罄
票已售罄

tip:[start]
异步(Asynchronous)是与同步(Synchronous)相对。在同步编程中,任务按照程序的正常执行顺序逐个执行,一个任务的执行会阻塞后续任务的执行,直到当前任务完成。而在异步编程中,任务不按照固定的顺序执行,而是通过回调函数、事件监听等机制,通过非阻塞的方式进行执行。

异步通常是指通过启动新的线程或使用线程池,让任务在后台执行,而不影响主线程的执行。异步编程的主要目的是提高程序的性能和响应性,允许程序在等待某些操作完成的同时继续执行其他任务。

上面的例子做的是线程同步和互斥,目的是保证数据一致性,异步就不需要去考虑这些。
tip:[end]

等待唤醒机制

等待和唤醒机制允许一个线程在满足特定条件之前等待,然后由另一个线程通知或唤醒它以继续执行。通过 wait()notify()notifyAll() 方法来实现。

  1. wait(): 在对象上调用 wait() 方法会使当前线程等待,直到另一个线程调用相同对象的 notify()notifyAll() 方法。
  2. notify(): 在对象上调用 notify() 方法会唤醒等待该对象的线程中的一个线程。具体唤醒哪个线程不确定,由JVM决定。
  3. notifyAll(): 在对象上调用 notifyAll() 方法会唤醒等待该对象的所有线程,使它们进入就绪状态。
public class Main {

    public static void main(String[] args) {
        Data data = new Data();

        Thread producer = new Thread(new Producer(data));
        Thread consumer = new Thread(new Consumer(data));

        producer.start();
        consumer.start();
    }

}

class Data {

    private int number;
    private boolean produced;

    synchronized void produce(int num) {
        if (produced) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        number = num;
        produced = true;
        System.out.println("Produced: " + num);
        notify();
    }

    synchronized void consume() {
        if (!produced) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        produced = false;
        System.out.println("Consumed: " + number);
        notify();
    }

}

class Producer implements Runnable {
    private final Data data;

    Producer(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            data.produce(i);
        }
    }
}

class Consumer implements Runnable {
    private final Data data;

    Consumer(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            data.consume();
        }
    }
}
  1. 如果本次是 producer 线程,执行 produce() 函数,此时 produced 初始值为 false,因此不会进入到 if 语句中,也就是不会休眠 producer 线程并释放锁,因此,当前线程执行的环境是安全的(数线程安全)。
  2. 当 number 设置为最新 0 之后,打印一条消息到控制台,然后唤醒 consumer 线程(不管有没有都执行)。
  3. 如果本次又是 producer 线程,执行 produce() 函数,由于上一次执行中已经把 produced 设置为 true,所以进入 if 语句中,也就是休眠当前线程并释放锁,由 consumer 线程通过 notify() 来唤醒。
  4. 如果本次是 consumer 线程,由于已经阻塞了 producer 线程,Data 锁已经被释放。因为 produced 是 true,不会让 consumer 线程等待,打印一条消息到控制台,然后唤醒 producer 线程,执行 produce() 函数。

通过这个机制,也要做好多个线程之间的同步和互斥,通过一个变量来控制。例如上面的 produced 变量,然后通过 notify()wait() 来唤醒或等待。

posted @ 2023-11-11 01:55  Himmelbleu  阅读(9)  评论(0编辑  收藏  举报