并发编程-JMM

并发编程-JMM

Q&A

什么是多线程并发编程?

多线程编程中,线程个数往往多于CPU核数

为什么要进行多线程并发编程?

多核CPU时代,随着对应用性能和吞吐量要求提高,出现海量数据和请求的要求,高性能应用程序中需要并发编程提升硬件利用率来提高整体处理性能

多线程基本概念

进程与线程

进程是代码在数据集合上的一次运行活动,是系统运行程序的基本单位,是系统资源分配和调度的基本单位。线程是进程中的一个实体(执行单元),一个进程至少有一个线程。

进程拥有独立的内存空间,进程间是独立的,线程共享进程的内存空间

CPU资源是分配到线程的,所以线程是CPU调度的基本单位

线程间堆和方法区是共享的,线程栈和程序计数器是独立的

并发与并行

并发是单位时间内,多个线程任务根据CPU时间片分配依次执行,并不一定是同时执行

并行是单位时间内,多个线程任务同时执行,并行上限取决于CPU核数

线程上下文切换

并发编程中线程数一般大于CPU核数,所以每个CPU同一时刻只能被一个线程使用,为了让用户感受在同时执行,采用抢占式时间片轮转策略,而线程CPU时间片用完、主动让出或者被中断,其他线程使用CPU,期间存在线程执行现场的存储和恢复操作(程序计数器和CPU寄存器),即上下文切换

线程的生命周期

线程生命周期.png

wait与sleep的区别

wait方法释放锁,sleep方法不释放锁

wait方法属于Object类,sleep方法属于Thread类

wait方法可指定时间也可不指定时间,调用notify、notifyAll方法唤醒

sleep方法必须指定时间,自动苏醒,苏醒后处于Ready状态等待CPU时间片执行

线程安全问题

多个线程同时读写一个共享资源并且在没有任何同步措施下,导致出现脏数据和其他不可预见结果的问题

卖票案例

public class TicketDemo {

    public static void main(String[] args) throws InterruptedException {
        // 三个售票员线程共享100张票,模拟卖票
        SellTicketTask sellTicketTask = new SellTicketTask();
        Thread thread1 = new Thread(sellTicketTask);
        Thread thread2 = new Thread(sellTicketTask);
        Thread thread3 = new Thread(sellTicketTask);

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join();
        thread2.join();
        thread3.join();
        stopWatch.stop();
        System.out.println(stopWatch.getTotalTimeMillis());
    }


    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;

        @Override
        public void run() {
            while (ticketNum > 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "正在卖:" + ticketNum--);
            }
        }
    }
}
D:\Java\jdk1.8.0_144\bin\java.exe 
// 存在大量重复卖同一张票的线程,因为全局变量及静态变量共享引起
Thread-2正在卖:50
Thread-0正在卖:50
Thread-1正在卖:49
Thread-1正在卖:48
Thread-2正在卖:48
Thread-0正在卖:48
Thread-0正在卖:47
Thread-2正在卖:46
Thread-1正在卖:46
Thread-1正在卖:45
Thread-0正在卖:44
Thread-2正在卖:43
Thread-2正在卖:42
Thread-0正在卖:41
Thread-1正在卖:42
Thread-1正在卖:40
Thread-2正在卖:40
Thread-0正在卖:40
Thread-0正在卖:39
Thread-2正在卖:38
Thread-1正在卖:39
Thread-1正在卖:37
Thread-2正在卖:36
Thread-0正在卖:35
Thread-1正在卖:34
Thread-0正在卖:33
Thread-2正在卖:32
Thread-1正在卖:31
Thread-2正在卖:31
Thread-0正在卖:30
Thread-0正在卖:29
Thread-1正在卖:27
Thread-2正在卖:28
Thread-1正在卖:26
Thread-2正在卖:25
Thread-0正在卖:24
Thread-0正在卖:23
Thread-1正在卖:21
Thread-2正在卖:22
Thread-1正在卖:20
Thread-0正在卖:20
Thread-2正在卖:20
Thread-0正在卖:19
Thread-1正在卖:18
Thread-2正在卖:18
Thread-0正在卖:17
Thread-2正在卖:17
Thread-1正在卖:16
Thread-2正在卖:15
Thread-0正在卖:15
Thread-1正在卖:15
Thread-2正在卖:14
Thread-1正在卖:14
Thread-0正在卖:14
Thread-1正在卖:13
Thread-0正在卖:13
Thread-2正在卖:13
Thread-0正在卖:12
Thread-1正在卖:10
Thread-2正在卖:11
Thread-1正在卖:8
Thread-2正在卖:7
Thread-0正在卖:9
Thread-2正在卖:6
Thread-1正在卖:6
Thread-0正在卖:5
Thread-2正在卖:4
Thread-1正在卖:3
Thread-0正在卖:2
Thread-1正在卖:1
Thread-2正在卖:0
Thread-0正在卖:1
726

Process finished with exit code 0

解决方案

  1. 线程同步synchronized、JUC的锁

  2. volatile保证变量可见性

  3. JUC的原子类

    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;

        @Override
        public void run() {
            synchronized (this) {
                while (ticketNum > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖:" + ticketNum--);
                }
            }
        }
    }
D:\Java\jdk1.8.0_144\bin\java.exe 
// 线程安全但耗时增加
Thread-0正在卖:50
Thread-0正在卖:49
Thread-0正在卖:48
Thread-0正在卖:47
Thread-0正在卖:46
Thread-0正在卖:45
Thread-0正在卖:44
Thread-0正在卖:43
Thread-0正在卖:42
Thread-0正在卖:41
Thread-0正在卖:40
Thread-0正在卖:39
Thread-0正在卖:38
Thread-0正在卖:37
Thread-0正在卖:36
Thread-0正在卖:35
Thread-0正在卖:34
Thread-0正在卖:33
Thread-0正在卖:32
Thread-0正在卖:31
Thread-0正在卖:30
Thread-0正在卖:29
Thread-0正在卖:28
Thread-0正在卖:27
Thread-0正在卖:26
Thread-0正在卖:25
Thread-0正在卖:24
Thread-0正在卖:23
Thread-0正在卖:22
Thread-0正在卖:21
Thread-0正在卖:20
Thread-0正在卖:19
Thread-0正在卖:18
Thread-0正在卖:17
Thread-0正在卖:16
Thread-0正在卖:15
Thread-0正在卖:14
Thread-0正在卖:13
Thread-0正在卖:12
Thread-0正在卖:11
Thread-0正在卖:10
Thread-0正在卖:9
Thread-0正在卖:8
Thread-0正在卖:7
Thread-0正在卖:6
Thread-0正在卖:5
Thread-0正在卖:4
Thread-0正在卖:3
Thread-0正在卖:2
Thread-0正在卖:1
1517

Process finished with exit code 0
    static class SellTicketTask implements Runnable {
        private int ticketNum = 50;
        private Lock lock = new ReentrantLock();

        @Override
        public void run() {
            lock.lock();
            try {
                while (ticketNum > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖:" + ticketNum--);
                }
            } finally {
                lock.unlock();
            }
        }
    }
D:\Java\jdk1.8.0_144\bin\java.exe 
Thread-0正在卖:50
Thread-0正在卖:49
Thread-0正在卖:48
Thread-0正在卖:47
Thread-0正在卖:46
Thread-0正在卖:45
Thread-0正在卖:44
Thread-0正在卖:43
Thread-0正在卖:42
Thread-0正在卖:41
Thread-0正在卖:40
Thread-0正在卖:39
Thread-0正在卖:38
Thread-0正在卖:37
Thread-0正在卖:36
Thread-0正在卖:35
Thread-0正在卖:34
Thread-0正在卖:33
Thread-0正在卖:32
Thread-0正在卖:31
Thread-0正在卖:30
Thread-0正在卖:29
Thread-0正在卖:28
Thread-0正在卖:27
Thread-0正在卖:26
Thread-0正在卖:25
Thread-0正在卖:24
Thread-0正在卖:23
Thread-0正在卖:22
Thread-0正在卖:21
Thread-0正在卖:20
Thread-0正在卖:19
Thread-0正在卖:18
Thread-0正在卖:17
Thread-0正在卖:16
Thread-0正在卖:15
Thread-0正在卖:14
Thread-0正在卖:13
Thread-0正在卖:12
Thread-0正在卖:11
Thread-0正在卖:10
Thread-0正在卖:9
Thread-0正在卖:8
Thread-0正在卖:7
Thread-0正在卖:6
Thread-0正在卖:5
Thread-0正在卖:4
Thread-0正在卖:3
Thread-0正在卖:2
Thread-0正在卖:1
1504

Process finished with exit code 0

同步控制后耗时增加,但从结果看,ReentrantLock耗时与synchronized不相上下

得益于jvm对synchronized一系列锁优化措施

多线程并发的特性

原子性:类似于事务的原子性,要么全部执行,要么全部不执行

有序性:程序代码按顺序执行(存在指令重排)

可见性:任何线程对共享变量的修改其他线程可见(由于Java内存模型JMM存在)

有序性

什么是指令重排序?

编译器和处理器在不影响输出结果前提下,为了提升程序运行效率进行的优化,调整指令运行顺序

因为CPU虽然是多核,但运行进程和线程是远多于核心数的,所以使用CPU时间片调度,指令流水线是间隔一个单位时间并行走的,如果前后两条指令存在关联,第二条指令执行(EX)需等待第一条指令写回寄存器之后才可以,导致浪费一个单位时间,可以通过先执行不相关的一条指令后再执行第二条指令来充分利用资源

int a = 2;//1
int b = 1 +a;//2
int c = 1;//3

由于3与12没有关联,1必须在2之前,所以可能会出现312/132/123多种执行顺序

指令流水线.png

多线程版本

由于num与ready赋值没有关联,所以可能出现0和4两种输出情况

public class ReOrderInstruction {
    private static int num = 0;
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
        ReadThread readThread = new ReadThread();
        WriteThread writeThread = new WriteThread();

        readThread.start();
        writeThread.start();
        Thread.sleep(5);
        readThread.interrupt();
        // 可能会出现输出0
        System.out.println("main done");
    }

    static class ReadThread extends Thread{
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                if (ready) {
                    System.out.println("read:" +(num + num));
                }
                System.out.println("read is done");
            }
        }
    }

    static class WriteThread extends Thread{
        @Override
        public void run() {
            num = 2;
            ready = true;
            System.out.println("write is done");
        }
    }
}

可见性

JMM内存模型

JMM决定了共享变量何时写入,何时对其它线程可见

线程之间的共享变量存储在主内存

每个线程有一个私有本地内存,本地内存存储共享变量副本

本地内存是抽象的

线程操作共享变量必须读取到本地内存中,不能直接操作主内存

线程间无法直接访问其他线程的本地内存,需要通过主内存进行传递

JMM.png

volatile保证了修改后的共享变量新值立即同步到主内存,对于其他线程可见,使用该共享变量前立即从主内存刷新共享变量新值到自己的本地内存,保证了多线程操作共享变量可见性

JMM内存天然的现行发生原则(Happens-before)
  • 程序顺序原则:一个线程内,书写在前的操作先行发生于书写在后面的操作

  • 管程锁定规则:一个unlock操作先行发生于后面同一个锁的lock操作

  • Volatile变量规则:一个volatile变量的写操作先行发生于该变量的读操作

  • 传递性原则:A先行发生于B,B先行发生于C,那么A先行发生于C

  • 线程启动规则:线程启动先于线程所有操作

  • 线程终止规则:线程所有操作先于线程终止

  • 线程中断规则:线程中断的调用先于线程代码中断检测

  • 对象终结规则:对象初始化先于对象终结finalize();

synchronized

保证方法和代码块在多线程环境运行,同一时刻只有一个线程执行代码

JDK1.6之前,synchronized底层实现依赖OS级别互斥锁MuteLock,存在严重性能问题

JDK1.6之后,synchronized实现改为管程(Monitor),并进行一系列优化,性能与JUC的lock不相上下,只是API能力无法满足场景,例如线程间通信lock的condition

synchronized保证了原子性。可见性、有序性,保证了线程安全

修饰不同方法和代码块锁定范围?

  1. 修饰代码块:锁给定对象

  2. 修饰非静态方法:锁当前对象

  3. 修饰静态方法:锁当前类对象(字节码对象/class对象)

如何解决可见性?

  • Happens-before规则

  • JMM中线程对共享变量操作规定

如何实现同步?

通过monitorenter与monitorexit jvm指令实现,即管程(Monitor)

public class SynchronizedDemo {
    public static void main(String[] args) {

    }

    public void sync1(){
        synchronized (this) {
            int a = 1;
        }
    }

    public synchronized void sync2() {
        int a = 1;
    }

    public static synchronized void sync3() {
        int a = 1;
    }
}
D:\Java\jdk1.8.0_144\bin\javap.exe -c com.zhaoccf.study.juc.SynchronizedDemo
Compiled from "SynchronizedDemo.java"
public class com.zhaoccf.study.juc.SynchronizedDemo {
  public com.zhaoccf.study.juc.SynchronizedDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: return

  public void sync1();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter //对应Monitor的lock
       4: iconst_1
       5: istore_2
       6: aload_1
       7: monitorexit //对应Monitor的unlock
       8: goto          16
      11: astore_3
      12: aload_1
      13: monitorexit //编译器会为同步块添加一个隐式的try-finally,在finally中会调用monitorexit命令释放锁
      14: aload_3
      15: athrow
      16: return
    Exception table:
       from    to  target type
           4     8    11   any
          11    14    11   any

  public synchronized void sync2();
    Code:
       0: iconst_1
       1: istore_1
       2: return

  public static synchronized void sync3();
    Code:
       0: iconst_1
       1: istore_0
       2: return
}

Process finished with exit code 0

何为管程(Monitor)?

管理共享变量即线程对共享变量操作的过程

Java所有对象都可以作为锁传入,因每个对象都有一管程与之关联

使用synchronized,JVM会自动加入两个指令monitorenter和monitorexit,对应Monitor的就是lock和unlock操作

Monitor.png

JDK1.6对synchronized的锁优化

同步锁状态:无锁、偏向锁、轻量级锁、重量级锁

锁优化技术:适应性自旋、锁消除、锁膨胀、轻量级锁、偏向锁

锁信息存储在对象头的标记字段(MarkWord)

JVM会分析逐步升级锁,加锁后获取偏向锁(消除同一线程的后续的同步措施),失败后获取轻量级锁,失败后循环自旋加锁,失败后膨胀为重量级锁,失败后循环自旋加锁,失败后OS层面挂起

!
JVM对象头MarkWord.png
偏向锁、轻量级锁、重量级锁状态转化即对象MarkWord关系.png

Volatile

Java内存语义保证线程可见性,禁止指令重排序

写入变量时,把写入本地内存的变量同步到内存,读取变量时,清空本地内存,从主内存刷新新值

无法保证原子性

实现内存可见性原理?

内存屏障,Java编译器会根据内存屏障规则禁止重排序

Volatile写变量时:在写操作之后添加一条store屏障指令,让本地内存变量值刷新到主内存

  • 在每个Volatile写前,插入StoreStore屏障

  • 在每个Volatile写后,插入StoreLoad屏障

Volatile读变量时:在读操作之后添加一条load屏障指令,读取变量主内存的值

  • 在每个Volatile读前,插入LoadLoad屏障

  • 在每个Volatile读后,插入LoadStore屏障

可见性验证

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        Task1 task1 = new Task1();
        new Thread(task1).start();
        Task2 task2 = new Task2();
        new Thread(task2).start();

        Thread.sleep(1000);
        task1.flag = false;
        System.out.println("Task1修改为false");
        task2.flag = false;
        System.out.println("Task2修改为false");
    }

    static class Task1 implements Runnable{
        public boolean flag = true;

        @Override
        public void run() {
            System.out.println("Task1开始");
            while (flag) {
            }
            System.out.println("Task1结束");
        }
    }

    static class Task2 implements Runnable{
        public volatile boolean flag = true;

        @Override
        public void run() {
            System.out.println("Task2开始");
            while (flag) {
            }
            System.out.println("Task2结束");
        }
    }
}

此时程序未结束,修改对Task1线程不可见

D:\Java\jdk1.8.0_144\bin\java.exe 
Task1开始
Task2开始
Task1修改为false
Task2修改为false
Task2结束

字节码指令

其中可以看到Volatile修饰的变量,有关键字ACC_VOLATILE

public volatile int num1;
descriptor: I
flags: ACC_PUBLIC, ACC_VOLATILE

public class Demo {

    public int num;
    public volatile int num1;

    public static void main(String[] args) {

    }
}
D:\Java\jdk1.8.0_144\bin\javap.exe -v com.zhaoccf.study.juc.Demo
Classfile /D:/Program/study/thinking-in-java/target/classes/com/zhaoccf/study/juc/Demo.class
  Last modified 2022-9-18; size 462 bytes
  MD5 checksum 68a69b4141743c22628d8c02296cf702
  Compiled from "Demo.java"
public class com.zhaoccf.study.juc.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#21         // java/lang/Object."":()V
   #2 = Class              #22            // com/zhaoccf/study/juc/Demo
   #3 = Class              #23            // java/lang/Object
   #4 = Utf8               num
   #5 = Utf8               I
   #6 = Utf8               num1
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhaoccf/study/juc/Demo;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               MethodParameters
  #19 = Utf8               SourceFile
  #20 = Utf8               Demo.java
  #21 = NameAndType        #7:#8          // "":()V
  #22 = Utf8               com/zhaoccf/study/juc/Demo
  #23 = Utf8               java/lang/Object
{
  public int num;
    descriptor: I
    flags: ACC_PUBLIC

  public volatile int num1;
    descriptor: I
    flags: ACC_PUBLIC, ACC_VOLATILE

  public com.zhaoccf.study.juc.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhaoccf/study/juc/Demo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Demo.java"

Process finished with exit code 0

原子性验证

不论加与不加volatile,结果都不为20000,无法保证原子性

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {

        Task3 task3 = new Task3();
        new Thread(task3).start();
        new Thread(task3).start();
        Task4 task4 = new Task4();
        new Thread(task4).start();
        new Thread(task4).start();

        Thread.sleep(1000);
        System.out.println(task3.num);
        System.out.println(task4.num);
    }

    static class Task3 implements Runnable{
        public int num;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }

    static class Task4 implements Runnable{
        public volatile int num;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }
}
D:\Java\jdk1.8.0_144\bin\java.exe 
15119
13841

Thread.start()JVM实现源码解析

TODO

你可能感兴趣的:(并发编程-JMM)