Java并发编程艺术之Java并发编程基础

版权声明:本博客为技术原创, 未经允许,谢绝转载 !!! https://blog.csdn.net/yewandemty/article/details/81902110

Java并发编程艺术之Java并发编程基础

一、线程简介

1. 什么是线程
    线程是现代操作系统调度的最小单位,也称为轻量级进程,这些线程拥有各自的计数器、堆栈、局部变量等信息,并且能够访问主内存中的共享变量。
    从宏观的角度来看,使用者可能觉得多个线程在同时进行,但是是处理器在这些线程上高速切换。比如下面的 代码示例

public class TestThread {
    public static void main(String[] args) {
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = mxBean.dumpAllThreads(false, false);
        for(ThreadInfo threadInfo : threadInfos) {
            System.out.println("["+ threadInfo.getThreadId() +"] " + threadInfo.getThreadName());
        }
    }
}

运行上面的程序输出下面的结果:

[ 5 ]Attach Listener            // 线程间通信监听的线程
[ 4 ]Signal Dispatcher          // 分发处理发送给JVM信号的线程
[ 3 ]Finalizer                  // 调用finalizer对象的线程
[ 2 ]Reference Handler          // 清除Reference的线程
[ 1 ]main                       // main线程,程序的主入口

从上面的运行结果可以看出,java程序的运行不仅main线程,还有其它线程。

2. 为什么要使用线程
    正确的使用多线程,主要有下面几点:

  • 更多的处理器数量
    随着处理器数量的增加、处理器性能的提升,如果程序以单线程运行,多余的处理器将会被浪费,如果使用多线程技术,可以最大化的利用处理器,减少程序处理时间,变得更有效率。

  • 更快的响应时间
    对一些业务复杂的代码,在多线程处理的情况下,对一些数据一致性要求不高的操作,可以交给其它线程处理,这样好处是响应用户请求的程序能够尽快的完成,缩短了响应时间,提升用户体验。

  • 更好的编程模型
    Java为多线程编程提供了良好的编程模型,使得开发人员更加专注问题的解决,一旦建立好了合适的模型,稍作修改可以映射到Java提供的多线程模型上。

3. 线程优先级
    线程执行的前提是系统给该线程分配了时间片,分配的时间片越多,获得的处理器资源越多,而线程的优先级决定了需要分配多一点还是少一点处理器资源。
    优先级的范围一般在1-10之间,可以通过setPriority(int)方法来设置,可以针对不同不同的需求设置不同的优先级:

  • 针对频繁阻塞、休眠、I/O操作 的线程需要设置比较高的优先级
  • 针对偏重计算、较多CPU时间的线程需要设置比较低的优先级

注意: 优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java对优先级的设定,比如下面的示例代码

public class Priority {
    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;
    public static void main(String[] args) throws Exception {
            List<Job> jobs = new ArrayList<Job>();
            for (int i = 0; i < 10; i++) {
                int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
                Job job = new Job(priority);
                jobs.add(job);
                Thread thread = new Thread(job, "Thread:" + i);
                thread.setPriority(priority);
                thread.start();
            }
            notStart = false;
            TimeUnit.SECONDS.sleep(10);
            notEnd = false;
            for (Job job : jobs) {
                System.out.println("Job Priority : " + job.priority + ",Count : " + job.jobCount);
            }
    }
    static class Job implements Runnable {
        private int priority;
        private long jobCount;
        public Job(int priority) {
            this.priority = priority;
        }
        public void run() {
            while (notStart) {
                Thread.yield();         //所有通过start()启动的线程都会暂停在此处
            }
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

在不同的操作系统环境下,输出的结果不同,

  • MAC系统 (jdk version 1.7.0_71)
Job Priority : 1, Count : 1259592
Job Priority : 1, Count : 1260717
Job Priority : 1, Count : 1264510
Job Priority : 1, Count : 1251897
Job Priority : 1, Count : 1264060
Job Priority : 10, Count : 1256938
Job Priority : 10, Count : 1267663
Job Priority : 10, Count : 1260637
Job Priority : 10, Count : 1261705
Job Priority : 10, Count : 1259967
  • ThinkPad E470 Windows 10 (jdk version 1.8.0_101)
Job Priority : 1,Count : 1272179
Job Priority : 1,Count : 1272751
Job Priority : 1,Count : 1274397
Job Priority : 1,Count : 1273538
Job Priority : 1,Count : 1273410
Job Priority : 10,Count : 4584184
Job Priority : 10,Count : 4596352
Job Priority : 10,Count : 4594366
Job Priority : 10,Count : 4586751
Job Priority : 10,Count : 4574652

4. 线程状态
    线程存在多中状态,可以参见下表:
Java线程状态

图1、java线程状态

关于线程状态的情况可以通过下面的示例代码来查看:

//ThreadState代码
public class ThreadState {
    public static void main(String[] args) {
        new Thread(new TimeWaiting (), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        // 使用两个Blocked线程,一个获取锁成功,另一个被阻塞
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }

    static class TimeWaiting implements Runnable {

        @Override
        public void run() {
            while(true) {
                SleepUtils.second(100L);
            }
        }

    }

    static class Waiting implements Runnable {

        @Override
        public void run() {
            while(true) {
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }

    static class Blocked implements Runnable {

        @Override
        public void run() {
            synchronized (Blocked.class) {
                while(true) {
                    SleepUtils.second(100L);
                }
            }
        }

    }
}
//SleepUtils代码
public class SleepUtils {
    public static final void second(Long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

查看线程状态信息的操作步骤如下:
1. 通过eclipse将工程导出jar文件,并指定main路径(可能jar中包含多个main入口)
2. 通过 java -jar JavaInBase.jar 启动jar程序
3. 通过jps查看运行的线程pid,如下图
通过jps查看线程pid

图2、通过jps查看线程pid

4. 通过 jstack pid 查看线程状态,如下图
jstack查看线程状态
图3、jstack查看线程状态

关于Java线程状态变迁,可以通过下面图示进行查看
Java线程状态变迁

图4、Java线程状态变迁

由上图可以看出,
1) 线程创建之后,调用start()开始运行,
2) 当线程执行wait()方法之后,线程进入等待状态(Waiting),线程进入等待状态的情况下,需要依靠其它线程的通知才能返回到运行状态,
3) 而超时等待状态(TIME_WAITING)在等待的基础上添加了超时的限制,即超时时间到达时会返回到运行状态,
4) 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态
5) 当线程的run方法执行完之后会进入stop状态

注意 : Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法

5.Daemon线程
    线程分类:java中有两类线程: 用户线程(User Thred)、守护线程(Daemon Thread)
    守护线程主要用作程序后台调度以及支持性工作,比如垃圾回收线程;守护线程和用户线程几乎没有什么区别,区别在于线程的离开,当用户线程全部退出时,守护线程没有了被守护者,也就没有了继续运行的必要。

关于守护线程需要注意下面几点:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会报IllegalThreadStateException异常,即不能将一个常规线程设置为守护线程
  • 在Daemon线程中产生的新线程也是Daemon的
  • 守护线程不能访问固有资源,如: 文件、数据库,因为他会在任何时候发生中断。
  • 在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源
    的逻辑

可以通过下面的示例代码进行查看

public class DaemonThrad {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner() , "DaemonRunner");
        thread.setDaemon(true);
        thread.start();
    }

    static class DaemonRunner implements Runnable {

        @Override
        public void run() {
            try {
                SleepUtils.second(10L);
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }

    }
}

二、 启动和终止线程
1. 构造线程
    在运行线程之前需要先构造线程,对线程设置相关信息,比如:线程组(Thread Group)、 线程优先级(Thread Priority) 、是否守护线程(is Deamon)等信息,下面示例代码时java.lang.Thread类中线程初始化的代码

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name.toCharArray();

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

对上面的代码进行分析,

  • 一个新构造的线程对象是由其 parent 线程来进行空间分配的,
  • child线程继承了 parent 线程 是否守护线程、优先级、加载资源的contextClassLoader、ThreadLocal 等信息
  • 分配唯一的线程 tid 来标志这个child线程
  • 启动线程前,最好为该线程设置线程的名称,在通过jstack分析程序或进行问题排查时,可以给开发人员一个提示。

线程初始化好后,会在堆内存中等待运行

2. 启动线程
    线程通过thread.start()来启动线程,start()语义是当前线程告诉Java虚拟机,只要处理器等资源空闲应立即调用 startI() 启动的线程。

3. 线程中断
    线程中断可以理解为线程的一个标志位(flag),它标志一个运行中的线程是否被其它线程进行了中断操作,可以通过interrupt()方法对其进行中断操作。可以通过isInterrupted()方法校验线程是否已经被中断,也可以通过Thread.isInterrupted()来堆线程进行复位。

4. 线程的suspend()、resume()和stop()
    关于线程的暂停、复位、停止可以参考下面的示例代码

public class DeprecatedThread {
    public static void main(String[] args) throws InterruptedException {
        DateFormat format = new SimpleDateFormat("HH:mm:ss");

        Thread printThread = new Thread(new Runner(), "PrinterThread");
        printThread.setDaemon(true);
        printThread.start();
        //main 线程停止运行3s钟
        TimeUnit.SECONDS.sleep(3);
        //printThread线程暂停
        printThread.suspend();
        System.out.println("Main suspend printThread at " + format.format(new Date()));

        //main 线程停止运行3s钟
        TimeUnit.SECONDS.sleep(3);
        //对printThread进行复位操作
        printThread.resume();
        System.out.println("Main resume printThread at " + format.format(new Date()));

        //main 线程停止运行3s
        TimeUnit.SECONDS.sleep(3);
        //对printThread进行停止
        printThread.stop();
        System.out.println("Main stop printThread at " + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
    }

    static class Runner implements Runnable {

        @Override
        public void run() {
            DateFormat format = new SimpleDateFormat("HH:mm:ss");
            while(true) {
                System.out.println(Thread.currentThread().getName() + " Run at " + format.format(new Date()));
                SleepUtils.second(1L); // 此行代码必须存在, 不然无法从run中退出
            }
        }

    }
}

输出结果如下:

PrinterThread Run at 22:00:31
PrinterThread Run at 22:00:32
PrinterThread Run at 22:00:33
Main suspend printThread at 22:00:34
Main resume printThread at 22:00:37
PrinterThread Run at 22:00:37
PrinterThread Run at 22:00:38
PrinterThread Run at 22:00:39
Main stop printThread at 22:00:40

注意:上面的suspend()、resume()、stop()方法会带来副作用,是不被建议使用的,比如:
1) suspend(): 执行该方法时,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题
2)stop(): 执行该方法时,不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下

5. 安全的终止线程
    线程的运行如果不能正确的终止,可能会造成严重的后果,因此需要保证程序正确的执行结束, 第4节内容suspend(), stop() 是不被推荐的,可以采用interrupted(), cancel()方法对线程进行正确的终止操作, 可以参见下面的代码

public class ShutDownThread {
    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();

        // main 线程睡眠 1s, 让countThread线程运行
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();

        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();

        //main线程睡眠 1s, 让countThread运行
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }


    static class Runner implements Runnable {
        private long i ;
        private volatile boolean isOn= true ;

        @Override
        public void run() {
            while(isOn && !Thread.currentThread().isInterrupted()){
                //如果标志位为true,并且当前线程的终止状态为false的情况下, 执行下面的代码
                i++ ;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            isOn = false ;
        }

    }
}

代码运行结果如下:

Count i = 679027409
Count i = 684359115

针对上面的代码可以通过下面的方式正确的终止线程:
1) 调用interupted()方法
2) 线程内部设置中断标志位flag,比如上面的isOn, 通过设置isOn = false 来终止线程的运行。

三、线程建通信
    孤立运行的线程没有价值或者价值很小, 现实情况中线程之间都是相互配合完成工作
1. volatile和synchronized关键字

  • volatile , 关键字volatile可以用来修饰成员变量,当该成员变量修改之后会刷新到主内存中,并告诉其它需要对该变量进行访问的线程,该变量已经发生变更,需要从新从主内存中从新加载,以保持数据的可见性。
  • synchronized,关键字synchronized可以修饰方法或者同步块,确保多个线程在同一个时刻只有一个线程处于方法或代码块中,以去报线程对变量访问的可见性和排它性
    可以通过下面的示例代码,使用javap命令来熟悉同步代码块
public class SynchronizedExample {
    public static void main(String[] args) {
        //1. 对SynchronizedExample 对象进行加锁
        synchronized (SynchronizedExample.class) {

        }
        //2. 静态同步方法, SynchronizedExample Class对象进行加锁
        m();
    }

    public static synchronized void m() {

    } 
}

通过javap查看信息的具体操作步骤如下:
1) 书写上面的示例代码,保证代码的可以编译通过
2) 打开cmd窗口,进入上面代码编译后的class文件所在目录
3) 执行命令 javap -v SynchronizedExample.class (如果有修改文件类名,请使用修改后的文件名), 本机输出的信息如下:

C:\ityongman\workspace\svncode\JavaInBase\bin\com\ityongman\thread>javap -v SynchronizedExample.class
Classfile /C:/ityongman/workspace/svncode/JavaInBase/bin/com/ityongman/thread/SynchronizedExample.class
  Last modified 2018-8-22; size 519 bytes
  MD5 checksum 4a8966093f66babccd21068023ebc63c
  Compiled from "SynchronizedExample.java"
public class com.ityongman.thread.SynchronizedExample
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/ityongman/thread/SynchronizedExample
   #2 = Utf8               com/ityongman/thread/SynchronizedExample
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/ityongman/thread/SynchronizedExample;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Methodref          #1.#17         // com/ityongman/thread/SynchronizedExample.m:()V
  #17 = NameAndType        #18:#6         // m:()V
  #18 = Utf8               m
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               SourceFile
  #22 = Utf8               SynchronizedExample.java
{
  public com.ityongman.thread.SynchronizedExample();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ityongman/thread/SynchronizedExample;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #1                  // class com/ityongman/thread/SynchronizedExample
         2: dup
         3: monitorenter
         4: monitorexit
         5: invokestatic  #16                 // Method m:()V
         8: return
      LineNumberTable:
        line 6: 0
        line 10: 5
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

  public static synchronized void m();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}
SourceFile: "SynchronizedExample.java"

针对输出信息,有下面的结论:

  • 对同步代码块,使用了monitorenter和monitorexit指令实现同步
  • 对同步方法,使用方法修饰符上的ACC_SYNCHRONIZED来实现同步

注,任意对象都拥有自己的监视器,无论使用monitor相关指令还是在方法上使用ACC_SYNCHRONIZED修饰,其本质都是获取对象的监视器,执行方法必须获取到该对象的监视器才可以进入同步快或同步方法,而没有获取到监视器的线程将会进入BLOCKED状态。

    为了更进一步了解对象、对象监视器、同步队列、执行线程之间的关系,可以参考下面截图:
对象、监视器、同步队列和执行线程之间的关系

图5、对象、监视器、同步队列和执行线程之间的关系

针对上面图5,可以看出

  • 如果要访问Object对象,必须先获取该Object对象的锁
  • 如果获取对象监视器锁失败,将会进入同步队列,线程状态变为BLOCKED状态
  • 如果之前访问Object的对象释放了监视器锁,将会释放信号唤醒同步队列中的线程,使其重新尝试获取监视器锁

2. 等待/通知机制
    等待/通知方法在任意Java对象中都具备,因为这些信息定义在java.lang.Object上,如下面的类信息图 和 等待/通知的相关方法相关说明图:
Object类信息

图6、Object类信息

Java线程的状态

图7、Java线程的状态

    等待/通知机制在功能层面上实现了解耦,体系结构上具有良好的伸缩性。在模型上和生产者/消费者很像,当一个线程修改了对象的值,另一个线程感知到这个对象变化,会进行相应的操作。

可以参见下面的伪代码:

while (value != desire) {
    Thread.sleep(1000);
}
doSomething();

但是针对上面的代码,存在一些问题:
1) 难以确保及时性。虽然睡眠期间不怎么消耗系统资源,但是睡眠时间过久,不能及时发现条件的变化
2) 难以降低开销。如果休眠的时间过短,可以比较及时的发现资源的变化,但是会消耗比较多的系统资源。

可以通过下面的示例代码对wait()和notify()/notifyall()做进一步了解

public class WaitNotifyThread {
    static boolean flag = true ;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();

        TimeUnit.SECONDS.sleep(1);  // main线程让出资源, 让其它线程执行

        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }


    static class Wait implements Runnable {

        @Override
        public void run() {
            synchronized (lock) {
                while(flag) {
                    //条件不满足, 进入等待状态
                    try {
                        System.out.println(Thread.currentThread() + " flag is true , wait @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait(); //Wati线程进入BLOCKED状态, 并且释放持有的锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //条件满足, 继续进行下面的处理
                System.out.println(Thread.currentThread()+ " flag is false , runing @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }

    }

    static class Notify implements Runnable {

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread() + " hold lock , notify @ " +
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll(); //Notify 发送信号量通知Wait重新尝试获取锁信息
                flag = false ;    // 设置flag = false ,避免仍然进入while中
                SleepUtils.second(5L);  // Notify线程睡眠5s, 但是线程依然持有者锁, 虽然notifyAll已经执行, 但是无法获取到锁  
            }

            synchronized (lock) {
                System.out.println(Thread.currentThread() + " hold lock again , notify @ " +
                        new SimpleDateFormat("HH:mm:ss").format(new Date()));
                SleepUtils.second(5L);  // 让Notify线程睡眠5s, 避免进行资源的竞争
            }
        }

    }
}

针对上面的示例代码需要注意下面的几点:

  • 使用wait()、notify()、notifyall()时需要先对调用对象加锁
  • 调用wait()后,线程的状态由RUNNING转为WAITING, 并将当前线程放到对象的等待队列中
  • notify()和nofityall()方法执行后,等待线程不一定会从wait状态返回,需要等调用notify()/notifyAll()的线程释放锁之后,等待线程才会有机会从wait状态返回。
  • notify()是将一个等待线程从等待队列中移动到同步队列,notifyAll()是将所有等待线程从等待队列中移动到同步队列,被移动的线程从BLOCKED状态变为WAITING
  • 从wait状态返回的前提条件是获取到了对象的锁

对上面示例代码和5点内容可以通过下面的图示来了解,
WaitNotifyThread运行过程

图8、WaitNotifyThread运行过程

3. 等待/通知的经典范式
    针对经典的等待/通知方式,可以分为等待方和通知方,它们的分别满足下列条件,

1) 等待方需要满足下面的规则;

  • 获取对象的锁
  • 如果条件不满足, 调用对象的wait()方法,被通知后仍然需要检测条件
  • 条件满足执行对应的逻辑

伪代码:

synchronized(lock) {
    while(条件不满足) {
        lock.wait();
    }
    对象的处理逻辑。
}

2) 通知方需要满足下列条件

  • 获取对象的锁
  • 改变条件
  • 通知所有等待在对象上的锁

伪代码

synchronized(lock) {
    改变条件
    lock.notifyAll()
}

4. 经典用例
1. 管道输入/输出流
     管道输入/输出和普通文件的输入/输出或网络输入/输出是不同的,管道输入/输出主要是线程之间的数据传输(媒介为内存), 涉及的类有PipeOutputStream、PipeInputStream、PipeReader、PipeWriter, 可以参考下面的示例代码:

public class PipeThread {
    public static void main(String[] args) throws IOException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        //需要将输入流和输出流连接起来, 否则使用时会抛出IoException
        writer.connect(reader);
        Thread printThread = new Thread(new Print(reader), "PrintReader");
        printThread.start();

        int receive = 0 ;
        try {
            while ((receive = System.in.read()) != -1) {
                writer.write(receive);
            } 
        } finally {
            writer.close();
        }
    }

    static class Print implements Runnable {
        PipedReader reader ;
        public Print(PipedReader reader) {
            this.reader = reader ;
        }

        @Override
        public void run() {
            int receive = 0 ;

            try {
                while((receive = reader.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

运行结果如下:

Hello World
Hello World

2.Thread.join()的使用
如果一个线程A执行了thread.join(), 其含义是线程A等待thread线程终止之后才从thread.join()返回,对join(long millis)表示线程thread在给定的时间内没有终止,将从该超时方法返回。可以参考下面的示例代码:

public class JoinThread {

    public static void main(String[] args) {
        Thread previous = Thread.currentThread();
        for(int i = 0; i < 10 ; i++) {
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread ;
        }
        System.out.println(Thread.currentThread().getName() + " terminated.");
    }

    static class Domino implements Runnable {
        private Thread thread ;

        public Domino(Thread thread) {
            this.thread = thread ;
        }

        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminated.");
        }

    }
}

运行结果如下:

main terminated.
0 terminated.
1 terminated.
2 terminated.
3 terminated.
4 terminated.
5 terminated.
6 terminated.
7 terminated.
8 terminated.
9 terminated.

3.ThreadLocal的使用
ThreadLocal是一个以ThreadLocal对象为键,任意对象为值的存储结构,通过set(T)方法设置值,get()方法获取原先的值, 可以参考下面的示例代码,

public class ProfileThread {
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ProfileThread.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost: " + ProfileThread.end() + " mills");
    }

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    } 

    public static final long end() {
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }
}

输出结果:

Cost: 1003 mills

声明:本文中借鉴,总结自下面的文章,有兴趣的读者,可以购买正版图书或进入官网进一步阅读相关信息。
1. Java并发编程艺术
2. http://ifeve.com/

猜你喜欢

转载自blog.csdn.net/yewandemty/article/details/81902110