Java 多线程并发编程(一) 线程基础

1.Java 程序执行过程分析

Java 虚拟机在实际执行 Java 代码的时候会将高级语言编写的代码 .java 编译成 .class 的字节码文件,然后通过读取字节码文件的指令来执行实际的功能。

假设现在有下面一段代码:

public class Demo1 {
    public int x;

    public int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        demo1.x = 3;
        int y = 2;
        int z = demo1.sum(demo1.x, y);
        System.out.println("person age is " + z);
    }
}

通过在命令控制台窗口使用 javac Demo1.java 命令可以编译得到如下所示的 Demo1.class(部分) 文件内容:

由 javac 编译的字节码文件开头都是 CAFEBABE,这也是一种代码执行规范,以 CAFEBABE 开头的字节码文件才会被 Java 虚拟机执行。很显然作为我们来说,这样的字节码文件是无法理解的,因此我们可以使用如下的命令将程序的字节码文件翻译成我们能理解的指令文件 Demo1.txt:

javap -v Demo1.class > Demo1.txt

文件内容如下所示:

Classfile /f:/IDEA WorkSpace/Thread/src/main/java/com/study/thread/Demo1.class
  Last modified 2019年3月5日; size 1001 bytes
  MD5 checksum 89def5bbe9482c48ade34e60b45901f1
  Compiled from "Demo1.java"
public class com.study.thread.Demo1
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // com/study/thread/Demo1
  super_class: #9                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 3
Constant pool:
   #1 = Methodref          #9.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // com/study/thread/Demo1
   #3 = Methodref          #2.#22         // com/study/thread/Demo1."<init>":()V
   #4 = Fieldref           #2.#24         // com/study/thread/Demo1.x:I
   #5 = Methodref          #2.#25         // com/study/thread/Demo1.sum:(II)I
   #6 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = InvokeDynamic      #0:#31         // #0:makeConcatWithConstants:(I)Ljava/lang/String;
   #8 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #9 = Class              #34            // java/lang/Object
  #10 = Utf8               x
  #11 = Utf8               I
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               sum
  #17 = Utf8               (II)I
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               Demo1.java
  #22 = NameAndType        #12:#13        // "<init>":()V
  #23 = Utf8               com/study/thread/Demo1
  #24 = NameAndType        #10:#11        // x:I
  #25 = NameAndType        #16:#17        // sum:(II)I
  #26 = Class              #35            // java/lang/System
  #27 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #28 = Utf8               BootstrapMethods
  #29 = MethodHandle       6:#38          // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #30 = String             #39            // person age is \u0001
  #31 = NameAndType        #40:#41        // makeConcatWithConstants:(I)Ljava/lang/String;
  #32 = Class              #42            // java/io/PrintStream
  #33 = NameAndType        #43:#44        // println:(Ljava/lang/String;)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Methodref          #45.#46        // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #39 = Utf8               person age is \u0001
  #40 = Utf8               makeConcatWithConstants
  #41 = Utf8               (I)Ljava/lang/String;
  #42 = Utf8               java/io/PrintStream
  #43 = Utf8               println
  #44 = Utf8               (Ljava/lang/String;)V
  #45 = Class              #47            // java/lang/invoke/StringConcatFactory
  #46 = NameAndType        #40:#51        // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #47 = Utf8               java/lang/invoke/StringConcatFactory
  #48 = Class              #53            // java/lang/invoke/MethodHandles$Lookup
  #49 = Utf8               Lookup
  #50 = Utf8               InnerClasses
  #51 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
  #52 = Class              #54            // java/lang/invoke/MethodHandles
  #53 = Utf8               java/lang/invoke/MethodHandles$Lookup
  #54 = Utf8               java/lang/invoke/MethodHandles
{
  public int x;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC

  public com.study.thread.Demo1();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public int sum(int, int);
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 11: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class com/study/thread/Demo1
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_3
        10: putfield      #4                  // Field x:I
        13: iconst_2
        14: istore_2
        15: aload_1
        16: aload_1
        17: getfield      #4                  // Field x:I
        20: iload_2
        21: invokevirtual #5                  // Method sum:(II)I
        24: istore_3
        25: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: iload_3
        29: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
        34: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: return
      LineNumberTable:
        line 15: 0
        line 16: 8
        line 17: 13
        line 18: 15
        line 19: 25
        line 20: 37
}
SourceFile: "Demo1.java"
InnerClasses:
  public static final #49= #48 of #52;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #29 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #30 person age is \u0001

这里面包括:版本、访问标志、常量池、当前类、超类、接口、字段、属性、方法等信息。

第一部分是类的基本信息,其中 minor version: 0 表示使用的 jdk 的最低版本,而 major version :55 表示当前使用的 jdk 版本(55 对应的是 jdk 11,54 对应 jdk10 以此类推...), interfaces:0 表示当前类没有实现任何接口,fields:1 表示当前类只有一个字段 x,methods:3 表示当前类有三个方法(包括 sum()、main() 以及默认的构造方法)

ACC_PUBLIC 表示访问修饰符 public,其他的访问标志可以参考下图:

 

接下来是常量池的定义:

具体指令的含义可以参考如下指令码表:

 

然后是默认的构造函数:

 

自定义的 sum 方法:

 

以及最后的 main 方法:

以上大量的操作都可以通过 JVM 指令码 找到对应的操作含义。

 

2.JVM 运行时数据区

JVM 运行时的数据区分为:线程共享部分以及线程独占部分。

线程共享部分包括方法区、堆内存,其中方法区存放了类的加载器信息、运行时常量池、字符串常量等定义,而方法区又分为新生代和永久代,在 JDK 1.8 之前方法区的这些信息都放在了永久代中,JDK 1.8 及其之后这些信息存放在 MetaSpace 元数据空间内;堆内存就是用来存放我们创建的对象。

线程独占部分包括虚拟机栈、本地方法栈以及程序计数器,虚拟机栈是由一个或多个栈帧组成的,每个栈帧表示一个线程的执行方法;本地方法栈是存放 Java 提供的 native 方法的地方;程序计数器顾名思义就是记录程序当前执行的位置,在多线程情景下,多个线程经过操作系统的调度再次得到执行时,通过访问程序计数器来得知接下来要执行的指令。如下是一段程序指令,而计数器中记录的就是序号列的值:

 

3.线程状态

一个线程在经过执行、操作系统的调度、锁的等待和获取等过程中会发生一些列的状态转换,线程的状态有以下几种:

(1)NEW:当线程刚刚创建的时候,线程处于 NEW 状态;

(2)RUNNABLE:当调用线程的 start() 方法之后,线程处于可执行状态 RUNNABLE;

(3)WAITING:当线程调用 wait() 等方法时;

(4)TIMED_WAITING:当线程调用指定了最长等待时间的方法时;

(5)BLOCKED:当线程请求获取锁对象的时候;

(6)TERMINATED:当线程正常执行结束后。

线程之间状态的切换如下图所示:

 

4.线程中止

早期,Java 提供了一种线程中止的方法 stop ,这种方法由于会立马中断线程的执行,所以可能会破坏线程内部操作的原子性从而出现数据不一致的情况,例如如下这段程序:

 

StopThread:自定义实现 Runnable 接口的线程

public class StopThread implements Runnable{

    public static int i,j=0;
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    public void run() {
        synchronized (this){
            ++i;
            try {
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ++j;
        }
    }

    public void print(){
        System.out.println("i="+i+",j="+j);
    }
}

当执行完 ++i 之后线程进入 10s 的休眠时间,如果在此期间调用线程的 stop 方法的话,线程会立即停止执行,即 ++j 的操作无法执行完毕,在实际的业务方法中就会导致线程不安全的问题。

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        StopThread st=new StopThread();
        Thread stopThread=new Thread(new StopThread());
        stopThread.start();
        Thread.sleep(3000L);
        //stopThread.stop(); //使用 thread.stop() 方法是线程不安全的,它会直接中断线程的操作,进而破坏线程的原子性
        System.out.println(stopThread.isInterrupted()); //正常情况下,线程是没有被中断的,所以 isInterrupted 返回为 false

        // 使用 thread.interrupt() 方法就能优雅的中断线程,它会等待线程执行完原子操作后才会中断线程,并抛出一个异常
        // 该方法是通过将线程的 isInterrupted 的状态设置为 true 来使得线程得到中断
        stopThread.interrupt();
        System.out.println(stopThread.isInterrupted()); //在主动执行 interrupt 方法之后,就会将线程的 isInterrupted 状态设置为 true
        while(stopThread.isAlive()){

        }
        System.out.println(stopThread.isInterrupted()); //当线程 isInterrupted 状态被设置成 true 之后一段时间,
        st.print();

    }
}

之后 Java 提供了另外一种更为优雅的中断线程执行的方法:interrupt(),如果线程由于调用 wait(),sleep(),join() 方法而阻塞的生活,该方法会将线程的中断状态位置为 true,然后等待线程执行结束后,由 JVM 将线程中止掉,并使得程序抛出 InterruptedException 异常,之后 JVM 将清除掉该状态位标记,即再次设置成 false。如果线程是由于被IO操作或者被 NIO 的 Channel 所阻塞时,IO操作会被中断或者返回特殊异常值,其他情况下就会设置线程的中断状态标记。

 

5.CPU缓存和内存屏障

众所周知,数据的访问速度跟所在的位置有关系:CPU缓存>内存>硬盘,所以设计 CPU 缓存是用来加快程序执行速度。

CPU缓存等级可以分为三级:

L1 Cache:CPU 的第一级高速缓存,分为数据缓存指令缓存,一般服务器的 L1 级缓存容量为 32-409KB。

L2 Cache:由于 L1 缓存的容量限制,为了再次提高 CPU 的执行速度,在 L1 的外层又放置了一层高速存储器,即二级缓存。

L3 Cache:现在的 L3 缓存都是内置的。而它的实际作用是,L3 缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能,具有较大 L3 缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。一般是多核共享一个 L3 缓存。

 

那么多 CPU 读取同样的数据进行缓存,进行不同的计算后,最终以哪个 CPU 的运算结果写入主内存呢?

在这种高速缓存会写的情况下,有一个所谓的缓存一致性协议 MESI 协议,它规定每条缓存有个状态位,同时定义了下面 4 个状态:

修改态(Modified):此 cache 行内容已被修改过,内容不同于主存,为此cache专有

专有态(Exclusive):此 cache 行内容同于主存,但是不出现在其他缓存中

共享态(Shared):此 cache 行内容同于主存,但也出现于其他缓存中

无效态(Invalid):此 cache 行无效

多处理器时,单个 CPU 对缓存中的数据进行修改后要通知其他的 CPU ,也就是说CPU除了要处理自己的读写操作外,还需要监听来自于其他 CPU 的通知,从而保证数据一致性。

 

CPU性能的优化手段:运行时的指令重排

例如下面这段代码:当 CPU 写缓存时发现缓存区块正在被其他 CPU 占用时,为了提高 CPU 性能,可能将后面的读缓存命令优先执行,但是并非随便重排,需要遵守 as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行速度),单线程程序的执行结果不能改变。也就是说,编译器和处理器不会对存在数据依赖关系的操作做重排序。

 

CPU 高速缓存同样存在问题:

(1)缓存中的数据与主内存的数据并不是实时同步的,各 CPU 之间缓存的数据也不是实时同步的,因此在同一个时间点,各 CPU 所看到同一内存地址的数据的值可能不一致

(2)CPU执行指令重排序时,它只能保证在单CPU自己执行的情况下结果正确,多核多线程中,指令逻辑无法分辨因果关联,导致程序运行结果错误。

针对以上的问题,处理器提供了两个内存屏障指令:

写内存屏障:在指令后强行插入 Store barrier,能让写入缓存中的最新数据写入到主内存,让其他线程可见。强制写入主内存,这种显示调用,CPU 就不会因为性能考虑而对指令进行重排序

读内存屏障:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从主内存中加载数据。

 

6.线程间通信

线程间通信指的是不同线程之间的数据交换、通知等。

线程间通信的方式有如下几种:

(1)文件共享:一个线程写入某个文件,另外一个线程再去读取这个文件的内容

(2)网络共享

(3)共享变量:处在堆内存中的数据,可以被多个线程访问

(4)jdk 提供的协作API:suspend/resume(已废弃),wait/notify&notifyAll,park/unpark

以下是使用 suspend/resume,wait/notify&notifyAll,park/unpark 的代码示例:

/**
 * 总结:线程间的通信可以有以下的方法:
 *
 * suspend/resume:此方法已经被 jdk 废弃,使用该方法进行线程间的等待和唤醒容易出现死锁的情况,出现死锁可能是因为 suspend 在挂起的时候不释放
 *                已有的锁资源,而导致其他线程在同步代码块中因无法获取到锁而不执行 resume;也有可能是因为 resume 和 suspend 的执行顺序导致
 *                死锁,即 resume 先执行而 suspend 后执行
 *
 * wait 和 notify/notifyAll:此方法是通过在对象锁上面添加监视器来实现的,在等待的时候自动释放当前持有的锁资源,并且将线程加入到锁资源的等待集
 *                合中,因此可以避免因为线程等待而继续持有锁的资源进而导致因为同步代码块无法获取锁而无法执行 notify/notifyAll 的方法,
 *                不过此方法也没有解决 wait 和 notify/notifyAll 执行顺序的问题,即执行顺序也会导致线程死锁
 *
 * park/unpark:此方法是模拟请求许可(park)和颁发许可(unpark)的思想来设计的,它们可以避免执行顺序而带来的线程死锁的情况,但是 park 在进入等待
 *              的时候也不会释放当前持有的锁资源,所以其他线程在同步代码块中也可能因为需要锁资源但是得不到所以无法执行 unpark 进而导致线程死锁
 *              的情况
 */
public class Demo6 {

    private static Object o=null;

    //suspend 和 resume 方法可以用来将线程挂起和通知挂起的线程继续执行,但是容易发生线程死锁
    public  void suspendResumeLock() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                System.out.println("进入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        thread.resume();
        System.out.println("通知消费者");
    }

    //使用 suspend 和 resume 线程死锁示例
    public void suspendResumeDeadLockTest() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                System.out.println("进入等待");
                synchronized (this) { //在 suspend 之前获取到当前对象的锁,然后挂起
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        //主线程需要获得当前对象的锁才能通知挂起的线程继续执行,此种情况下就会发生线程死锁的情况,主线程无法执行下去,子线程也没有办法继续执行
        synchronized (this) {
            thread.resume();
        }
        System.out.println("通知消费者");
    }

    //suspend 和 resume 死锁示例2,如果 resume 在 suspend 之前执行的话,那么子线程也不会被唤醒,而是一直保持挂起状态
    public void suspendResumeDeadLockTest2() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                System.out.println("进入等待");
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Thread.currentThread().suspend();
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();

        synchronized (this) {
            thread.resume();
        }
        System.out.println("通知消费者");
    }

    //wait 和 notify/notifyAll 方法可以避免 suspend 和 resume 出现死锁的第一种情况,就是在等待的时候仍旧持有锁
    //wait 和 notify/notifyAll 方法是基于监视器的实现,必须要放在同步代码块中,否则会出现 illegalMonitorException 的异常
    //wait 方法在执行的时候会主动的释放对象上的锁,并且将当前线程加入到对象锁的等待集合中去
    public  void waitNotifyTest() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                synchronized (this){
                    System.out.println("进入等待");
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        synchronized (this){
            //等到对象上的锁调用 notify/notifyAll 方法唤醒所有等待在对象锁上面的线程时,原来监视在对象锁上的线程得以执行
            this.notifyAll();
            System.out.println("通知消费者");
        }

    }

    //wait 和 notify/notifyALl 方法也不可避免的出现 suspend 和 resume 第二种死锁情况的发生,即 notify/notifyAll 方法提前执行了,但是
    //此时子线程还没有进入等待状态,这样的话,子线程就会一直等待下去
    public  void waitNotifyDeadLock() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (this){
                    System.out.println("进入等待");
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        synchronized (this){
            this.notifyAll();
            System.out.println("通知消费者");
        }

    }

    public  void parkTest() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("进入等待");
                LockSupport.park();
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        LockSupport.unpark(thread);
        System.out.println("通知消费者");


    }

    //park 是等待一个许可,而 unpark 是颁发一个许可,因此如果是 unpark 先执行的话,再执行 park,此时许可已经存在了,所以不会出现死锁的情况
    //但是 park 在进行等待的时候,也不会释放对象的锁,因此在同步代码块中还是有可能出现线程死锁的情况
    //多次调用 unpark 方法效果不会叠加,即多次执行 unpark 后,第一次执行 park 方法可以继续执行,后面继续执行 park 的话还是会进行等待
    public  void parkDeadLock() throws InterruptedException {
        Thread thread = new Thread(() -> {
            while(o==null){ //if 判断可能出现的伪唤醒,所以使用官方推荐的 while 循环来进行等待条件的判断
                System.out.println("进入等待");
                synchronized (this) {
                    LockSupport.park();
                }
            }
            System.out.println("子线程执行完成");
        });
        thread.start();
        Thread.sleep(3000L);

        o=new Object();
        synchronized (this) {
            LockSupport.unpark(thread);
            System.out.println("通知消费者");
        }


    }

    //使用 if 条件判断来让线程进入等待状态是不安全的
    //因为线程可能会被来自更底层的变化而被唤醒,也就是所谓的伪唤醒,这种情况下,唤醒条件并没有满足,但是线程的执行已经完成,就会出现执行异常的问题
    //官方推荐使用 while 循环来进行等待的判断,即如果判断条件满足了才继续往后执行代码
    public static void main(String[] args) throws InterruptedException {
        new Demo6().parkDeadLock();
    }

}

 

7.线程封闭

所谓的线程封闭就是多个线程之间对同一个变量的操作互不影响,也或者是线程内部的局部变量不会被外部线程修改。

JDK 为线程封闭提供了两种方法:

(1)ThreadLocal<T> 变量:ThreadLocal<T> 变量是 JDK 提供的一种特殊的变量,该变量会为每个线程创建一个副本,因此多个线程之间操作该变量的时候不会互相影响

(2)线程范围内的局部变量:其他线程无法修改线程范围内的局部变量

以下是代码示例:


/**
 * ThreadLocal 提供了线程本地变量的实现,虽然定义在外部(非局部变量),但是 jvm 在处理 ThreadLocal 变量的时候,会为每个
 * 线程创建这个变量的一个副本,所以线程之间的操作不会相互影响,从而实现了线程封闭的功能
 *
 * 还有一种线程封闭的功能就是线程内部定义的局部变量
 */
public class Demo7 {

    private static ThreadLocal<String> value=new ThreadLocal<>();

    public static void main(String[] args){
        value.set("这是主线程设置的值 123");
        System.out.println("主线程获取 value 的值:"+value.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程获取 value 的值"+value.get());
                value.set("这是子线程设置的值 456");
                System.out.println("子线程获取 value 的值"+value.get());
            }
        }).start();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程获取 value 的值:"+value.get());
    }
}

 

8.线程池的应用

线程数目越多越好吗?其实并不是这样:

(1)线程资源需要创建和销毁,如果线程的创建+销毁的时间>任务执行的时间,那么越多线程就会出现越多的资源浪费

(2)线程也是对象,需要占用 JVM 提供的堆内存,另外线程内部的虚拟机栈、本地方法栈、程序计数器等空间是直接从系统内存分配而来的,默认的线程大小为 1M,越多的线程表示需要消耗的系统内存越多,当然占用的 JVM 的堆内存也越大

(3)线程在执行的时候是受操作系统调度的,而操作系统调度的方式就是给线程分配时间片并且需要切换线程上下文,过多的线程就会让操作系统忙于切换上下文而导致效率低下

那么如何解决上面的问题呢?这就是线程池的由来,线程池,顾名思义就是可以容纳多个线程的容器,在线程池对象创建的时候就会创建指定的线程,这样在任务到来的时候可以直接使用线程池中的空闲线程来完成执行任务。

那么线程池中的线程数到底多少合适呢?针对不同类型的任务,所需要分配的线程数目也不一定。如果任务类型是计算型任务(计算型任务的特点就是执行速度快,频率高),那么分配 CPU 数量的1-2倍即可;IO 型任务的话,由于可能出现 IO 阻塞的情况,所以这类任务需要更多的线程,需要根据具体的IO阻塞时长来决定。

以下是通过代码的方式对线程池的一些说明:

/**
 * 线程池的组成:
 *  线程池管理器:创建并管理线程池,创建、销毁线程,添加新任务
 *  工作线程:线程池中的线程,在没有任务的时候将处于等待状态,可以循环的执行任务
 *  任务接口:每个任务必须实现的接口,以供工作线程执行
 *  任务队列:存放没有处理的任务
 *
 * 线程池的继承关系:
 *  Executor 顶级接口,只包含一个 execute 方法
 *  ExecutorService 继承自 Executor 的接口,又添加了一些其他的方法,例如 shutdown,awaitTermination,submit,invokeAll,invokeAny 等
 *  ScheduledExecutorService 接口继承了 ExecutorService ,另外添加了一些关于定时任务相关的方法例如schedule,scheduleAtFixedRate,
 *  scheduleWithFixedDelay 等方法
 *  ThreadPoolExecutor 基础、标准的线程池实现
 *  ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor 并且实现了 ScheduledExecutorService 接口
 *
 * 线程池的实现
 */
public class Demo9 {

    //这是一个核心线程数为 5,最大线程数为 10,无界队列的线程池
    //线程池中线程的处理方式为:当有任务提交过来的时候,如果核心线程还有空闲的,那么就直接分配核心线程去处理;如果核心线程没有空闲的,就看队列中
    //是否还可以放入任务,如果可以放入任务,就直接放入,如果队列已满,就判断当前线程池中的线程数是否达到了最大线程数,如果没有达到的话那么就创建
    //线程来处理到来的任务,如果达到了最大值的话,那么就拒绝提供线程服务。
    //线程的活跃时间 5 表示如果超过核心线程数目的线程在 5 秒内没有再接到线程运行的任务,那么就销毁掉多余的线程,最后线程池中的线程数目再次变成
    //核心线程的 5 个。
    public void ThreadPoolExecutorTest1() throws InterruptedException {
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(5,10,5, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
        testCommon(tpe);
    }

    //该方法是创建了一个核心线程数为 5,最大线程数为 10,任务队列长度为 3 的线程池,也就是说这个线程池可以同时接受  10+3 个线程的提交请求,超过这个数字的
    //提交请求将被拒绝
    public void ThreadPoolExecutorTest2() throws InterruptedException {
        /*ThreadPoolExecutor tpe=new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("队列已满,线程池拒绝接受服务");
            }
        });
        testCommon(tpe);*/
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("有任务被拒绝执行了");
            }
        });
        testCommon(threadPoolExecutor);
    }

    //该方法是创建了一个核心线程是 5 ,最大线程数是 5 的固定长度的线程池,但是由于任务队列仍然是无界的,所以执行效果和第一个中的效果一致
    public void ThreadPoolExecutorTest3() throws InterruptedException {
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(5,5,5,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
        testCommon(tpe);
    }



    //SynchronousQueue
    //
    //SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;
    // 可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,
    // remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

    //声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:如果采用公平模式:SynchronousQueue会
    // 采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;但如果是非公平模式(SynchronousQueue默认):
    // SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,
    // 则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。

    //LinkedBlockingQueue
    //
    //LinkedBlockingQueue是无界的,是一个无界缓存的等待队列。
    //基于链表的阻塞队列,内部维持着一个数据缓冲队列(该队列由链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,
    // 而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中
    // 消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。

    //LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下
    // 生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

    //ArrayListBlockingQueue
    //
    //ArrayListBlockingQueue是有界的,是一个有界缓存的等待队列。
    //基于数组的阻塞队列,同LinkedBlockingQueue类似,内部维持着一个定长数据缓冲队列(该队列由数组构成)。ArrayBlockingQueue内部还保存着两个整形变量,
    // 分别标识着队列的头部和尾部在数组中的位置。
    //ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;
    // 按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为
    // ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。
    // ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成
    // 一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

    //ArrayBlockingQueue和LinkedBlockingQueue是两个最普通、最常用的阻塞队列,一般情况下,处理多线程间的生产者消费者问题,使用这两个类足以。
    public void ThreadPoolExecutorTest4() throws InterruptedException {
        //Executors.newCachedThreadPool() 就是使用同步阻塞队列 SynchronousQueue 实现的
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
        testCommon(tpe);

        Thread.sleep(60000L);
        System.out.println("60s 后再看线程池中的线程数量:"+tpe.getPoolSize());
    }

    //在指定延迟后执行任务,但是仅执行一次
    public void ThreadPoolExecutorTest5(){
        ScheduledThreadPoolExecutor stpe=new ScheduledThreadPoolExecutor(5);
        stpe.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务被执行,现在时间为:"+System.currentTimeMillis());
            }
        },3000,TimeUnit.MILLISECONDS);
        System.out.println("定时任务,提交成功,时间为:"+System.currentTimeMillis()+",当前线程池中的线程数为:"+stpe.getPoolSize());
    }

    public void ThreadPoolExecutorTest6(){
        ScheduledThreadPoolExecutor stpe=new ScheduledThreadPoolExecutor(5);
        //scheduleAtFixedRate 会周期性的执行任务,下面的代码效果为:2秒后开始执行第一次任务,间隔1秒重复执行任务
        //如果任务的执行时间超过了间隔时间的话,那么不会并行再次执行另外的任务,而是等到本次任务执行结束后,立刻执行下一个任务
        stpe.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行时间为:"+System.currentTimeMillis());
            }
        },2000,1000,TimeUnit.MILLISECONDS);

        //scheduleWithFixedDelay 会以固定的延迟执行任务,下面代码的效果是延迟2秒执行第一次任务,以后每次任务执行完成后间隔一秒再执行下一次任务
        //这个方法与 scheduleAtFixedRate 方法的区别在于如果任务的执行时间超过了指定的时间,scheduleAtFixedRate 会立刻执行下一次任务,而scheduleWithFixedDelay
        //会再次等待指定时间执行下一次任务
        stpe.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程执行时间为:"+System.currentTimeMillis());
            }
        },2000,1000,TimeUnit.MILLISECONDS);
        System.out.println("当前系统时间为:"+System.currentTimeMillis());
    }

    //关闭线程池的优雅方法:shutdown 等待线程池中正在执行的任务都执行结束后才真正的关闭线程池,不接受再次提交的任务
    //下面代码的实现效果:
    //1.核心线程为 5,最大线程为 10,任务队列长度为 3,即可以接受 13 个线程的提交执行,剩下的两个线程将被拒绝服务
    //2.调用shutdown 方法后,线程池等待正在执行的线程执行结束,然后关闭线程池
    //3.再次追加任务的时候,由于线程池已经关闭了,所以也被拒绝服务了
    public void ThreadPoolExecutorTest7() throws InterruptedException {
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3)
                , new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("任务被拒绝了");
            }
        });

        for(int i=0;i<15;i++){
            int n=i;
            tpe.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务开始执行:"+n);
                    try {
                        Thread.sleep(3000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("任务执行结束:"+n);
                }
            });
            System.out.println("任务"+n+"提交成功");
        }

        Thread.sleep(1000);
        tpe.shutdown();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("追加一个任务");
            }
        });
    }

    //shutdownNow:立刻关闭线程池
    //下面代码的实现效果:
    //1.核心线程为 5,最大线程为 10,任务队列长度为 3,即可以接受 13 个线程的提交执行,但是由于每个线程执行时间都至少需要 3 秒,而线程池在
    // 主线程休眠 1 秒后就被调用 shutdownNow 方法而关闭,因此队列中的 3 个任务将不会被执行,并且已经在执行的 10 个任务也会被立刻中断执行,
    // 另外剩下的 2 个线程将被拒绝服务
    //2.再次追加任务的时候,由于线程池已经关闭了,所以也被拒绝服务了
    public void ThreadPoolExecutorTest8() throws InterruptedException {
        ThreadPoolExecutor tpe=new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3)
                , new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.err.println("任务被拒绝了");
            }
        });

        for(int i=0;i<15;i++){
            int n=i;
            tpe.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务开始执行:"+n);
                    try {
                        Thread.sleep(3000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("任务执行结束:"+n);
                }
            });
            System.out.println("任务"+n+"提交成功");
        }

        Thread.sleep(1000);
        //shutdownNow 将会立刻停止所有的线程执行,所以正在执行的线程将会由于被中断而抛出 InterruptedException,这个方法返回未被执行的线程
        List<Runnable> shutdownNow =  tpe.shutdownNow();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("追加一个任务");
            }
        });
        System.out.println("等待队列中未执行的线程数为:"+shutdownNow.size());
    }



    public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws InterruptedException {
        for(int i=0;i<15;i++){
            int n=i;
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("线程"+n+"开始执行");
                        Thread.sleep(3000L);
                        System.out.println("线程"+n+"结束执行...");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            System.out.println("任务提交成功:"+i);
        }

        Thread.sleep(500L);
        System.out.println("当前线程池线程数量为:"+threadPoolExecutor.getPoolSize());
        System.out.println("当前线程池队列中等待的线程数量为:"+threadPoolExecutor.getQueue().size());
        Thread.sleep(15000L);
        System.out.println("当前线程池线程数量为:"+threadPoolExecutor.getPoolSize());
        System.out.println("当前线程池队列中等待的线程数量为:"+threadPoolExecutor.getQueue().size());
    }

    public static void main(String[] args) throws InterruptedException {
        Demo9 demo =new Demo9();
        //demo.ThreadPoolExecutorTest1();
        //demo.ThreadPoolExecutorTest2();
        //demo.ThreadPoolExecutorTest4();
        //demo.ThreadPoolExecutorTest5();
        //demo.ThreadPoolExecutorTest6();
        //demo.ThreadPoolExecutorTest7();
        demo.ThreadPoolExecutorTest8();
    }
}

以上如有不足和理解错误之处,还请各位大神指教,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值