Java并发编程基础-线程间通信

简介: 章节目录volatile 与 synchronized 关键字等待/通知机制等待/通知经典范式管道输入/输出流Thread.join() 的 使用1. volatile 与 synchronized 关键字线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一行一行的执行,直到终止。

章节目录

  • volatile 与 synchronized 关键字
  • 等待/通知机制
  • 等待/通知经典范式
  • 管道输入/输出流
  • Thread.join() 的 使用

1. volatile 与 synchronized 关键字

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一行一行的执行,直到终止。如果每个运行中的线程,仅仅是孤立的运行,那么没有价值,或者说价值很少,如果多个线程能够 相互配合 完成工作,这将带来巨大的价值。

1.1 Java 线程操作的共享变量是对共享内存变量的一份拷贝

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个共享变量的一份拷贝
(虽然对象以及成员变量分配的内存是在共享内存中,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是
加速程序的执行)。这是现代多核处理器的一个显著特性,
所以在程序执行过程中,(未同步的程序代码块),一个线程看到的变量并不一定是最新的。

1.2 volatile 关键字-线程间通信

关键字volatile可以用来修饰字段(成员变量),就是告知任何对该变量的访问均
需要从共享内存中获取,而对它的改变必须同步刷新到共享内存,
它能保证虽有线程对共享变量的可见性。
举个例子,定义一个程序是否运行的成员变量,boolean on = true; 那么另一个
线程可能对它执行关闭动作(on = false),这涉及多个线程对变量的访问,因此
需要将其定义为 volatile boolean on = true,这样其他线程对他进行改变时,可
以让所有线程感知到变化,因为所有对共享变量的访问(load)和修改(store)都需
要以共享内存为准。但是过多的使用volatile是不必要的,因为它会降低程序执行的效率。

1.3 synchronized 关键字-线程间通信

关键字 synchronized 可以修饰方法 或者以同步块的形来进行使用,它主要确
保多个线程在同一时刻,只能有一个线程执行同步方法或同步块,它保证了线
程对变量访问的可见性、排他性。

如下所示,类中使用了同步块和同步方法,通过使用javap 工具查看生成的class文件信息来分析synchronized关键字实现细节,示例如下:

package org.seckill.Thread;

public class Synchronized {
    public static void ls(String[] args) {
        synchronized (Synchronized.class) {

        }//静态同步方法,对Synchronized Class对象进行加锁

        m();
    }

    public static synchronized void m(){

    }
}

执行 javap -v Synchronized.class
输出如下所示:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class org/seckill/Thread/Synchronized
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit
        13: aload_2
        14: athrow
        15: invokestatic  #3                  // Method m:()V
        18: return

public static synchronized void m();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return

对上述汇编指令进行解读

  • 对于同步代码块(临界区)的实现使用了monitorenter 和 monitorexit 指令。
  • 同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED。
  • 另种同步方式的原理是 对一个充当锁的对象的monitor 进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由syntronized 所保护的对象的监视器。
  • 任何一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到对象的监视器才能进入到同步块或者同步方法中,那么没有获取到监视器(执行改方法)的线程将会被阻塞在同步块和同步方法的入口处,进入blocked 状态。

如下是对上述解读过程的图示:


img_4762d144061eedb379700a28e071a142.png
对象、监视器、同步队列、执行线程之间的关系

2.等待/通知机制

等待通知相关方法

方法名称 描述
wait() 调用lock.wait()(lock是充当锁的对象)的线程将进入waiting状态,只有等待另外线程的通知或者线程对象.interrupted()才能返回,wait()调用后,会释放对象的锁
wait(long) 超时一段时间,这里的参数是毫秒,也就是等待n毫秒,如果没有通知就超时返回
wait(long,int) 对于超时间的更细粒度控制,可以达到纳秒级别
notify() 通知一个在锁对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁(其实是线程获取到了该对象的monitor对象的控制权)
notifyAll() 通知所有等待在充当锁的对象上的线程

对等待通知机制的解释

  • 等待通知机制,是指一个线程A调用了充当锁的对象的wait()方法进入等 waiting 状态
  • 另一个线程B调用了对象的O的 notify() 或者 notifyAll() 方法,线程A接收到通知后从充当锁的对象上的wait()方法返回,进而执行后续操作,最近一次操作是线程从等待队列进入到同步阻塞队列。
  • 上述两个线程通过充当锁的对象 lock 来完成交互,而lock对象上的wait()/notify/notifyAll()的关系就如同开关信号一样,用来完成等待方通知方的交互工作

如下代码清单所示,创建两个线程 WaitThread & NotifyThread,前者检查flag是否为false,如果符合要求,进行后续操作,否则在lock上wait,后者在睡眠一段时间后对lock进行通知。

package org.seckill.Thread;

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();//充当锁的对象

    public static void main(String[] args) {
        //新建wait线程
        Thread waitThread = new Thread(new WaitThread(),"waitThread");
        Thread notifyThread = new Thread(new NotifyThread(),"notifyThread");
        waitThread.start();//等待线程开始运行
        Interrupted.SleepUnit.second(5);//主线程sleep 5s
        notifyThread.start();
    }

    //wait线程
    static class WaitThread implements Runnable {
        public void run() {
            synchronized (lock) {
                //判定flag
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取flag 信息" + flag);
                        //判定为true 直接wait
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println(Thread.currentThread().getName() + "获取flag 信息 为" + flag);
            }
        }
    }

    static class NotifyThread implements Runnable {

        public void run() {
            synchronized (lock) {
                while (flag) {
                    System.out.println(Thread.currentThread().getName() + "获取flag 信息 为" + flag+"可以运行");
                    lock.notify();//唤醒wait在lock上的线程,此时wait线程只能能从waiting队列进入阻塞队列,但还没有开始重新进行monitorenter的动作
                    // 因为锁没有释放
                    flag = false;
                    Interrupted.SleepUnit.second(5);
                }
            }

            synchronized (lock){//有可能获取到lock对象monitor,获取到锁
                System.out.println(Thread.currentThread().getName()+" hold lock again");
                Interrupted.SleepUnit.second(5);
            }
        }
    }

}

运行结果如下所示:

img_fcae3495b82a9d18a938947e61a73136.png
运行结果

对如上程序运行流程的解释如下所示:
上图中"hold lock again 与 最后一行输出"的位置可能互换,上述例子说明调用wait()、notify()、notifyAll需要注意的细节

  • 使用wait()、notify() 和 notifyAll() 时需要在同步代码块或同步方法中使用,且需要先对调用的锁对象进行加锁(获取充当锁的对象的monitor对象)
  • 调用wait() 方法后,线程状态由running 变为 waiting,并将当前线程放置到等待队列中
  • notify()、notifyAll() 方法调用后,等待线程依旧不会从wait()方法返回,需要调用notify()、notifyAll()的线程释放锁之后,等待线程才有机会从wait()方法返回
  • notify() 方法将waiting队列中的一个等待线程从waiting队列 移动到同步队列中,而notifyAll() 则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态由waiting status change to blocked状态
  • 从wait() 方法返回的前提是获得了调用对象的锁
    等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改

3.等待/通知经典范式

等待/通知经典范式
该范式分为两部分,分别针对等待方(消费方)、和通知方(生产方)
等待方遵循如下原则:

  • 获取对象的锁
  • 如果条件不满足,则调用对象的wait() 方法,被通知后仍要检查条件
  • 条件满足则执行对应的逻辑
    对应伪代码
syntronized (lock) {
  while( !条件满足 ){
      lock.wait();
  }
  //对应的处理逻辑
}

通知方遵循如下原则:

  • 获取对象锁
  • 改变条件
  • 通知所有等待在锁对象的线程
syntronized(lock) {
       //1.执行逻辑
       //2.更新条件
       lock.notify();
 }

4.管道输入输出流

  • 管道输入 / 输出流和普通的文件输入/输出流 或者网络输入/输出流的不同之处在于它主要用于线程之间的数据传输,而传输的媒介为内存。
  • 管道输入 / 输出流主要包括如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader 、PipedWriter 前两种面向字节,后两种面向字符
    对于Piped类型的流,必须先进行绑定,也就是调用connect()方法,如果没有输入/输出流绑定起来,对于该流的访问将抛出异常。

5.Thread.join() 的 使用

如果使用了一个线程A执行了thread.join ,其含义是线程A等待thread线程终止之后才从thread.join()返回。
如下笔试题:
有A、B、C、D四个线程,在main线程中运行,要求 执行顺序是A->B->C->D->mian
变种->main等待A、B、C、D四个线程顺序执行,且进行sum,之后main线程打印sum
解法1-join()
其实就是插队

package org.seckill.Thread;

public class InOrderThread {
    static  int num = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread previous = null;
        for (int i = 0; i < 4; i++) {
            char threadName = (char) (i + 65);
            Thread thread = new Thread(new RunnerThread(previous), String.valueOf(threadName));
            previous = thread;
            thread.start();
        }
        previous.join();
        System.out.println("total num=" + num);
        System.out.println(Thread.currentThread().getName() + "terminal");
    }

    static class RunnerThread implements Runnable {
        Thread previous;//持有前一个线程引用

        public RunnerThread(Thread previous) {
            this.previous = previous;
        }

        public void run() {
            if (this.previous == null) {
//                num += 25;
                System.out.println(Thread.currentThread().getName() + " terminate ");
            } else {
                try {
                    previous.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
//                num += 25;
                System.out.println(Thread.currentThread().getName() + " terminate ");
            }

        }
    }
}

解法2-wait/notify

package org.seckill.Thread;

//wait/notify
public class InOrderThread2 {
//    static int state = 0;//运行标志
//    static Object lock = new Object();

    public static void main(String[] args) {
//        RunnerThread runnerThreadA = new RunnerThread();
//        RunnerThread runnerThreadB = new RunnerThread();
//        RunnerThread runnerThreadC = new RunnerThread();
//        RunnerThread runnerThreadD = new RunnerThread();
//        Thread threadA = new Thread(runnerThreadA, "A");
//        Thread threadB = new Thread(runnerThreadB, "B");
//        Thread threadC = new Thread(runnerThreadC, "C");
//        Thread threadD = new Thread(runnerThreadD, "D");
        RunnerThread runnerThread = new RunnerThread();
        Thread threadA = new Thread(runnerThread, "A");
        Thread threadB = new Thread(runnerThread, "B");
        Thread threadC = new Thread(runnerThread, "C");
        Thread threadD = new Thread(runnerThread, "D");

        threadD.start();
        threadA.start();
        threadB.start();
        threadC.start();
    }

    static class RunnerThread implements Runnable {
//        private  boolean flag = true;
        static int state = 0;//运行标志
        static Object lock = new Object();

        public void run() {
            String threadName = Thread.currentThread().getName();
//            while (flag) {
//                synchronized (lock) {
//                    if (state % 4 == threadName.charAt(0) - 65) {
//                        state++;
//                        flag = false;
//                        System.out.println(threadName + " run over");
//                    }
//                }
//            }

            synchronized (lock) {
                while (state % 4 != threadName.charAt(0) - 65) {
                    try {
                        lock.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
                state++;
                System.out.println(threadName+" run over ");
                lock.notifyAll();
            }
        }
    }
}

等待/通知范式做线程同步 是非常方便的。

解法3-循环获取锁

package org.seckill.Thread;

//wait/notify
public class InOrderThread2 {
    static int state = 0;//运行标志
    static Object lock = new Object();

    public static void main(String[] args) {
        RunnerThread runnerThreadA = new RunnerThread();
        RunnerThread runnerThreadB = new RunnerThread();
        RunnerThread runnerThreadC = new RunnerThread();
        RunnerThread runnerThreadD = new RunnerThread();
        Thread threadA = new Thread(runnerThreadA, "A");
        Thread threadB = new Thread(runnerThreadB, "B");
        Thread threadC = new Thread(runnerThreadC, "C");
        Thread threadD = new Thread(runnerThreadD, "D");
//        RunnerThread runnerThread = new RunnerThread();
//        Thread threadA = new Thread(runnerThread, "A");
//        Thread threadB = new Thread(runnerThread, "B");
//        Thread threadC = new Thread(runnerThread, "C");
//        Thread threadD = new Thread(runnerThread, "D");

        threadD.start();
        threadA.start();
        threadB.start();
        threadC.start();
    }

    static class RunnerThread implements Runnable {
        private  boolean flag = true;//每个线程的私有变量
//        static int state = 0;//运行标志
//        static Object lock = new Object();

        public void run() {
            String threadName = Thread.currentThread().getName();
            while (flag) {//主动循环加锁
                synchronized (lock) {
                    if (state % 4 == threadName.charAt(0) - 65) {
                        state++;
                        flag = false;
                        System.out.println(threadName + " run over");
                    }
                }
            }
//
//            synchronized (lock) {
//                while (state % 4 != threadName.charAt(0) - 65) {
//                    try {
//                        lock.wait();
//                    }catch (InterruptedException e){
//                        e.printStackTrace();
//                    }
//                }
//                state++;
//                System.out.println(threadName+" run over ");
//                lock.notifyAll();
//            }
        }
    }
}

开销是极大的、难以确保及时性

解法4-CountDownLatch

package org.seckill.Thread;


import java.util.concurrent.CountDownLatch;


public class InOrderThread3 {
//    static int state = 0;//运行标志
//    static Object lock = new Object();

    public static void main(String[] args) throws  InterruptedException{
        CountDownLatch countDownLatchA = new CountDownLatch(1);
        CountDownLatch countDownLatchB = new CountDownLatch(1);
        CountDownLatch countDownLatchC = new CountDownLatch(1);
        CountDownLatch countDownLatchD = new CountDownLatch(1);
        RunnerThread runnerThreadA = new RunnerThread(countDownLatchA);
        RunnerThread runnerThreadB = new RunnerThread(countDownLatchB);
        RunnerThread runnerThreadC = new RunnerThread(countDownLatchC);
        RunnerThread runnerThreadD = new RunnerThread(countDownLatchD);

        Thread threadA = new Thread(runnerThreadA, "A");
        Thread threadB = new Thread(runnerThreadB, "B");
        Thread threadC = new Thread(runnerThreadC, "C");
        Thread threadD = new Thread(runnerThreadD, "D");


//        RunnerThread runnerThread = new RunnerThread();
//        Thread threadA = new Thread(runnerThread, "A");
//        Thread threadB = new Thread(runnerThread, "B");
//        Thread threadC = new Thread(runnerThread, "C");
//        Thread threadD = new Thread(runnerThread, "D");

        threadA.start();
        countDownLatchA.await();//主线程阻塞,待countDownLatch 减为0即可继续向下运行
        threadB.start();
        countDownLatchB.await();
        threadC.start();
        countDownLatchC.await();
        threadD.start();
        countDownLatchD.await();

        System.out.println(Thread.currentThread().getName()+" run over ");

    }

    static class RunnerThread implements Runnable {
//        private  boolean flag = true;
//        static int state = 0;//运行标志
//        static Object lock = new Object();
        CountDownLatch countDownLatch;

        RunnerThread(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName+" run over");
            countDownLatch.countDown();

//            while (flag) {
//                synchronized (lock) {
//                    if (state % 4 == threadName.charAt(0) - 65) {
//                        state++;
//                        flag = false;
//                        System.out.println(threadName + " run over");
//                    }
//                }
//            }
//
//            synchronized (lock) {
//                while (state % 4 != threadName.charAt(0) - 65) {
//                    try {
//                        lock.wait();
//                    }catch (InterruptedException e){
//                        e.printStackTrace();
//                    }
//                }
//                state++;
//                System.out.println(threadName+" run over ");
//                lock.notifyAll();
//            }
        }
    }
}

countDownLatch 的使用场景 :比如系统完全开启需要等待系统软件全部运行之后才能开启。最终的结果一定是发生在子(部分)结果完成之后的。也可作为线程同步的一种方式
Thread join() 源码

public final synchronized void join() throws InterruptedException {
   while (isAlive) {
       wait(0);
   }
}

当被调用thread.join() 的线程(thread)终止运行时,会调用自身的notifyAll()方法,会通知所有等待该线程对象上完成运行的线程,可以看到join方法的逻辑结构与等待/通知经典范式一致,即加锁、循环、处理逻辑3个步骤。

目录
相关文章
|
13小时前
|
安全 Java 容器
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第18天】随着多核处理器的普及,并发编程变得越来越重要。Java提供了丰富的并发编程工具,如synchronized关键字、显式锁Lock、原子类、并发容器等。本文将深入探讨Java并发编程的核心概念,包括线程安全、死锁、资源竞争等,并分享一些性能优化的技巧。
|
18小时前
|
安全 Java
Java一分钟之-并发编程:原子类(AtomicInteger, AtomicReference)
【5月更文挑战第18天】Java并发编程中的原子类如`AtomicInteger`和`AtomicReference`提供无锁原子操作,适用于高性能并发场景。`AtomicInteger`支持原子整数操作,而`AtomicReference`允许原子更新对象引用。常见问题包括误解原子性、过度依赖原子类以及忽略对象内部状态的并发控制。要避免这些问题,需明确原子操作边界,合理选择同步策略,并精确控制原子更新。示例代码展示了如何使用这两个类。正确理解和使用原子类是构建高效并发程序的关键。
9 1
|
18小时前
|
安全 Java 容器
Java一分钟之-并发编程:并发容器(ConcurrentHashMap, CopyOnWriteArrayList)
【5月更文挑战第18天】本文探讨了Java并发编程中的`ConcurrentHashMap`和`CopyOnWriteArrayList`,两者为多线程数据共享提供高效、线程安全的解决方案。`ConcurrentHashMap`采用分段锁策略,而`CopyOnWriteArrayList`适合读多写少的场景。注意,`ConcurrentHashMap`的`forEach`需避免手动同步,且并发修改时可能导致`ConcurrentModificationException`。`CopyOnWriteArrayList`在写操作时会复制数组。理解和正确使用这些特性是优化并发性能的关键。
6 1
|
18小时前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第18天】在Java并发编程中,锁是一种常用的同步机制,用于保护共享资源的访问。然而,不当的锁使用可能导致性能问题和死锁风险。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁分离和读写锁等技术,以提高并发程序的性能和可靠性。
|
1天前
|
存储 安全 Java
Java多线程基础知识总结,36岁老码农现身说法
Java多线程基础知识总结,36岁老码农现身说法
|
1天前
|
Java 编译器
Java 并发编程中的锁优化策略
【5月更文挑战第17天】在 Java 并发编程中,锁是一种常见的同步机制,用于保护共享资源的访问。然而,不当使用锁可能导致性能问题和死锁风险。本文将探讨 Java 中的锁优化策略,包括锁粗化、锁消除、锁降级以及读写锁等技术,以提高并发程序的性能和可靠性。
|
1天前
|
Java 测试技术
Java多线程的一些基本例子
【5月更文挑战第17天】Java多线程允许并发执行任务。示例1展示创建并启动两个`MyThread`对象,各自独立打印&quot;Hello World&quot;。示例2的`CounterExample`中,两个线程(IncrementThread和DecrementThread)同步地增加和减少共享计数器,确保最终计数为零。这些例子展示了Java线程的基本用法,包括线程同步,还有如Executor框架和线程池等更复杂的用例。
9 0
|
1天前
|
Java 编译器
Java并发编程中的锁优化策略
【5月更文挑战第17天】在Java并发编程中,锁是一种常见的同步机制,用于保护共享资源。然而,使用不当的锁可能导致性能下降和死锁等问题。本文将探讨Java中锁的优化策略,包括锁粗化、锁消除、锁排序等方法,以提高程序的性能和可靠性。
|
2天前
|
存储 关系型数据库 MySQL
《MySQL 入门教程》第 05 篇 账户和权限,Java高并发编程详解深入理解pdf
《MySQL 入门教程》第 05 篇 账户和权限,Java高并发编程详解深入理解pdf
|
3天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。