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

Java程序主频上不去 java主线程


文章目录

  • 5. 线程间通信
  • 5.1 synchronized
  • 5.2 Lock +Condition
  • 5.3 BlockingQueue


5. 线程间通信

当线程在运行时,在系统的调度中具有一定的透明性,虽然程序无法精准的控制线程,但是我们可以通过一定的手段,达到协调线程运行。

上一篇我们介绍了线程同步,确保不同的线程操作的数据一致性。同样的 我们也可以用锁的方式,协调线程。下面我们用一道经典的力扣多线程题目多线程交替打印 foobar来简单说下线程间通信。

5.1 synchronized

之前我们讲到 synchronized 代码块和 synchronized 方法,都是获取监视器来控制线程的状态,那么同样的在不同的线程中,如果我们用的监视器是同一个,就可以做到根据条件切换不同线程运行。话不多说先上代码

public class PrintAB{
    private static final Object LOCK = new Object();
    private static boolean printA = true;
  
    public static void main(String[] args){
        new Thread(new PrintA()).start();
        new Thread(new PrintB()).start();
    }
  
    static class PrintA implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) {  
                for (int i = 0; i < 10; i++) {
                    try {
                        if (!printA) {  //注释 1
                            LOCK.wait(); // 注释 2
                        }
                        System.out.println("A");  //注释 3
                        printA = false; // 注释 4
                        LOCK.notifyAll(); // 注释 5
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) { 
                for (int i = 0; i < 10; i++) {
                    try {
                        if (printA) { //注释 6
                            LOCK.wait(); //注释 7
                        }
                        System.out.println("B"); //注释 8
                        printA = true; //注释 9
                        LOCK.notifyAll(); //注释 10
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}

这里我们首先定义两个成员变量,一个是锁 LOCK,一个是 用来标记是否打印 A 的标记printA。接着 main 方法中启动了两个线程,一个线程是用来打印 A,一个线程是用来打印 B,到这里都很好理解。

下面我们来看看如何通过监视器,协调线程的:

  • 当程序运行到 注释 1 处,回去判断是否要阻塞当前线程,由于第一次进来的时候printA = true,所以这里不阻塞,并且运行了输出
  • 解咒走到了注释 4,此处将 printA 赋值 flase,因为要交替输出 AB,A 已经输出,所以到 B 的轮次
  • 然后走到注释 5,这里唤醒了 其他的锁,当再次循环到注释 1 的时候,由于 printA 为 false,所以此时 Lock 是 wait 状态,线程阻塞,从而走到线程 B,注释 6 处获取的 printA 为 false,所以线程 B 不阻塞,输出了 B,然后有将 printA 设置为 true,并且通知其他线程,如此循环 做到了两个输出 交替执行。

5.2 Lock +Condition

上面的方法是通过 synchronized 来保证线程同步,通过控制监视器的 wait,notify 方法协调线程。如果不通过 synchronized 仅使用 Lock 对象确保同步,也是可以的,这就是我们要学习的第二个方式 Lock+Condition

在这种方式下,Lock 代替了synchronized, Condition 代替了监视器。先看代码:

public class Main {
    
    private static boolean printA = true;

    private static final Lock LOCK = new ReentrantLock();

    private static final Condition CONDITION = LOCK.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(new PrintA()).start();

        new Thread(new PrintB()).start();

    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            LOCK.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    if (!printA) {
                        CONDITION.await();
                    }
                    System.out.println("A");
                    printA = false;
                    CONDITION.signalAll();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            LOCK.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    if (printA) {
                        CONDITION.await();
                    }
                    System.out.println("B");
                    printA = true;
                    CONDITION.signalAll();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
        }
    }
}

从代码上看,和方法一的写法起始很像,只不过将 synchronized 代码块通过 lock()方法加锁,而监视器的 wait 方法通过 condition 的 await 方法代替,监视器的 notifyAll方法被 condition 的 signalAll 方法代替。这里需要注意的是 lock 一定要在 finally 中释放锁。同样介绍一下 condition 方法:

  • await(): 和 Object 类的 wait 类似,也是用于当线程等待,直到其他线程通过 signal 或者 signalAll 唤醒
  • signal():唤醒在此Lock 对象上的等待的单个线程,如果有多个线程在等待,则回选择唤醒其中一个。只有当前线程放弃对该 Lock 对象的锁定,才能执行被唤醒的线程。
  • signalAll():唤醒所有等待的线程,只有当前线程放弃对该 Lock 对象的锁定后,才能执行被唤醒的线程

5.3 BlockingQueue

从字面意思可以看出 这就是一个阻塞队列,从源码看 BlockingQueue 也是继承 Queue,所以他也有队列的特性。BlockingQueue 作为阻塞队列,当队列已经满的情况下,下一个通过 put 方法加入队列的会被阻塞,当队列有空闲才会继续添加,同理在使用 take 方法 取队列里面的元素的时候,如果队列为空,那么此时取操作也会阻塞,等到队列内有元素的时候,才会读取。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Main {
    static BlockingQueue<String> queueA = new LinkedBlockingQueue<>();
    static BlockingQueue<String> queueB = new LinkedBlockingQueue<>();

    public static void main(String[] args) {
        queueA.add("A");
        new Thread(new PrintA()).start();
        new Thread(new PrintB()).start();
    }

    static class PrintA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(queueA.take());
                    queueB.add("B");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    static class PrintB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(queueB.take());
                    queueA.add("A");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

这里我们引入了两个队列,一个队列控制输出 A,一个队列控制输出 B,因为在 BlockingQueue 的顺序不一定可靠,所以使用两个队列确保每一个队列只做一件事情,但是在不同线程控制 队列 A,B 的数据,这样就能做到线程同步,交替输出。值得注意的是,在main 方法的一开始,就给 A 队列加入了一个元素,因为如果不加入元素,在 printA 中,走到 queueA.take()的时候,队列为空,此时队列 B 也为空,互相阻塞造成了死锁。

上面通过力扣题目,讲解三种方式的线程通信,起始还有很多并发类可以控制,总结我们可以发现,在多线程通信中,如果想要控制线程,逃不过三板斧 加锁 -> 操作 -> 释放锁



https://www.xamrdz.com/web/2r91964310.html

相关文章: