什么是最大并行数?

这个与CPU有关,比如4核8线程

  • 4核:CPU有4个大脑——可以做四件事情
  • 因特尔发明了——超线程技术(分身),把4个大脑虚拟成8个

一.并发和并行,上下文切换

进程:

  1. 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统会为该进程分配内存空间,当我启动迅雷,就又启动了一个进程,操作系统会为迅雷分配新的内存空间
  2. 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生存在和消亡的过程。

线程:

  1. 线程是操作系统能够进行运算调度的最小单位,线程由进程建立,是进程中的实际运作单位
  2. 简单理解:应用软件中相互独立,可以同时运行的功能
  3. 多线程就比如:一个qq进程,可以同时打开多个聊天窗口;

并发:在同一时刻,有多个指令在单个CPU交替执行(多个任务交替执行,造成貌似同时的假象)

并行:在同一时刻,有多个指令在多个CPU同时执行(多个任务同时执行)

并发和并行可能同时进行

上下文切换:

      CPU通过时间片分配算法来循环执行线程任务,当前任务执行一个时间片后会切换到下一个线程任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换
      这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
      既然线程上下文切换会影响多线程的执行速度,我们在设计多线程并发程序的时候,减少线程之间的上下文切换会是一个优化方向。常用的减少上下文切换的手段包括:

  • 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
  • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
  • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

二.多线程的实现方式

①继承Thread类的方式进行实现

②实现Runnable接口的方式进行实现

③利用Callable接口和Future接口方式实现

多线程的第一种启动方式:

  1.自己定义一个类继承Thread

  2.重写run方法

  3.创建子类的对象,并启动线程

//1.当一个类继承了Thread类,该类就可以当成线程使用
//2.其实Thread类实质上也implements runnable接口
public class MyThread extends Thread{
    @Override
    public void run(){
    //重写的run方法,书写线程要执行的业务代码
        for(int i=0;i<10;i++){
            System.out.println("子线程:"+getName());
        }
    }
}
    public static void main(String[] args) {

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("t1");
        t2.setName("t2");
        //说明: 当main线程启动一个子线程时(start),此时的主线程也不会阻塞,会继续执行
        //这时子线程和主线程相互交替执行
        //此时主线程结束了(eg:main的打印工作线程任务结束,不是main方法终止),子线程如果还在继续,这个时候程序还会继续运行
        //知道子线程也结束了,程序才会结束
        t1.start();
        t2.start();
    }
}

简化写法:

//匿名内部类简化写法
new Thread("Thread A"){//构造方法输入名
 @Override
 public void run(){
  System.out.println("当前线程名字"+Thread.currentThread().getName())
 }
}.start();

疑问: t1.start()启动线程,最终其实还是执行MyThread的run方法,为什么不直接用t1.run()?

答: 因为run方法只是一个普通方法,直接调用只是在main线程运行,并没启动线程。真正实现多          线程的效果是源码里的start0()方法,为本地方法,由JVM调用,而不是run方法

  • 用线程的第一种启动方式虽然可行,但是因为java是单继承的,所以在某些情况下一个类可能已经继承了某个父类,这个时候再继承Thread类方法来创建线程不可能,因此第二个线程启动的方式诞生了

多线程的第二种启动方式:
 1. 自己定义一个类实现Runnable接口
 2. 重写里面的run方法
 3. 创建自己类的对象
 4. 创建一个Thread类的对象,并开启线程

public class MyRun implements Runnable{

    @Override
    public void run() {
        //书写线程要执行的代码
        for(int i=0;i<10;i++){
            //获取当前线程的对象Thread.currentThread()
            System.out.println("子线程:"+Thread.currentThread().getName());
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //创建MyRun对象
        //表示多线程要执行的任务
        MyRun myRun = new MyRun();

        //创建Thread对象
        Thread t1 = new Thread(myRun);
        Thread t2 = new Thread(myRun);

        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");

        //启动线程
        t1.start();
        t2.start();
    }
}

  • 前面两种run方法没有返回值,不能获取到多线程运行的结果,所以为了获取到运行结果,第三种方法诞生了

多线程的第三种实现方式:

  特点: 可以获取到多线程运行的结果

  1.创建一个类MyCallable实现Callable接口

  2.重写call(是有返回值的,表示多线程运行的结果)

  3.创建MyCallable的对象(表示多线程要执行的任务)

  4.创建FutureTask对象(作用管理多线程运行的结果)

  5.创建Thread类的对象,并启动(表示线程)

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //求1-100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}
public class TreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable mc = new MyCallable();

        //创建FutureTask对象(作用管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(mc);

        //创建Thread类的对象,并启动(表示线程)
        Thread t1 = new Thread(ft);
        //启动线程
        t1.start();

        //获取线程运行结果
       Integer ft1 = ft.get();
        System.out.println(ft1);


    }
}
  • 从Java设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,Thread类本身就实现了Runnable接口
  • 实现Runnable接口方式更适合多个线程共享一个资源的情况,并且避免了单继承的限制。(编程中推荐使用Runnable接口实现线程)

三.多线程中的常见成员方法

interrupt         中断线程

yield               线程的礼让,让出cpu,让其他线程执行,但礼让的时间不确定,所以不一定                          礼让成功

join                 线程插队,插队的线程一旦插队成功,则肯定先执行完插入的线程所有任务

工作线程         也叫用户线程,当线程的任务执行完或通知方式结束

守护线程         一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束

常见的守护线程:垃圾回收机制

/*
题目:main线程创建一个子线程,每隔一秒输出hello,输出二十次,主线程每隔一秒输出hi,输出20次。
要求: 两个线程同时执行,当主线程输出5次之后,就让子线程运行完毕,主线程再继续
 */
public class ThreadLearning {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();//创建子线程
        t.start();

        //主线程也同时吃
        for (int i = 0; i < 20; i++) {
            Thread.sleep(1000);
            System.out.println("hi" + Thread.currentThread().getName());

            if (i == 5) {
                System.out.println("子线程插队");
                t.join();//让当前线程等待,直到t线程结束(被插队)
                System.out.println("子线程已结束");
            }
        }
    }


}

class T extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);//此处的try-catch可以解释(interrupt会中断sleep)
            }
            System.out.println("hello" + Thread.currentThread().getName());
        }
    }
}

  • 操作系统基本采用时间分片的形式来分配处理器资源给线程运行
  • 而优先级就是一个可以指定线程应该多分还是少分时间片的属性
  • 针对频繁阻塞(频繁休眠或者IO操作较多)的线程应该 设置高优先级,而计算较多,耗CPU的线程则应该设置更低的优先级,避免处理器被独占。(与常识相反)

setName细节:

  1. 如果我们没有给线程设置名字,线程也是有默认名字的(格式:Thread-X(X序号,从0开始)
  2. 如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置 

sleep细节:

  1. 哪条线程执行到这个方法,那么哪条线程就会在这停留对应的时间
  2. 方法的参数:就是表示睡眠的时间,单位毫秒(1s=1000ms)
  3. 当时间到了之后,线程会自动的醒来,继续执行下面的其它代码

setDaemon细节:

  1. 当其他的非守护线程执行完毕之后,守护线程会陆续结束
  2. 通俗理解:当女神线程结束了,那么备胎线程也没有存在的必要了

细节:

  • 当JVM虚拟机启动之后,会自动的启动多条线程
  • 其中有一条线程就叫做main线程
  • 它的作用就是去调用main方法,并执行里面的代码
  • 在以前,我们写的所有的代码,其实都是运行在main线程当中

3.2 线程中断与终止

线程中断:

  • 中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。
  • 其他线程通过调用该线程的interrupt方法进行中断操作。反过来,线程通过isInterrupt()方法来判断自己是否被执,行了中断操作,并做出响应。

总结:

线程中断不同于阻塞等待这些,它是一个线程的标识,线程可以选择如何去处理这个标识,所以我们是不是一般要在可能会发生中断的代码处加上isInterrupt(),然后如果感受到中断可以做出一些操作。

例如,小A在学习,但是这个时候电话打来了,小A感受到了这个“中断”,他可以选择处理这个“中断”-接电话,也可以挂电话,继续自己的学习。

InterruptedException

  • 如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常。
  • 在抛出InterruptException之前,Java虚拟机会将该线程的中断标记位清除。然后抛出InterruptException,此时调用isInterrunpted()方法将会返回false。
  • Lock 锁可以中断,Synchronized 不可以,但是线程无论持有哪种锁,只要被休眠了再中断都可以抛出异常。(无论锁支不支持中断,线程都会响应中断,线程的中断状态与锁无关)
  • 抛出异常后,线程就不会再阻塞了,会执行后面的逻辑,和正常程序一样看trycatch.
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new TestThread1();
        thread1.start();
        TimeUnit.SECONDS.sleep(2);
        thread1.interrupt();
    }

    private static class TestThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(20000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("catch:" + Thread.currentThread().isInterrupted());
        }
    }
}
//执行结果
//抛出异常
//打印:catch:false

分析上述代码为何执行结果是后面的:

3.为什么线程不应该强制停止?

  • 上面介绍了中断,那基于中断的停止模式是,A线程如果想要停止B线程 ,则A 线程需要给B线程发送一个中断信号,B线程通过中断标志位判断自己是否有被中断,最后自行决定如何响应中断(例如:可以停止,可以延后,也可以直接忽略)。为什么要让线程拥有响应中断的自主权,而不可以强制停止呢?(A强制停止B)
  • 可以设想一下,我们很多工作并不是可以贸然停止的,例如A想要停止B时,B线程正在写入一个文件,可能文件正写入一半,如果立即停止那数据就是不完整的。但是对于A来说,它是感知不到B进行到什么阶段的,所以也没办法选择一个最佳时机来停止B,要想安全稳妥的停止B线程,确实只能B线程自行决策。
  • 再看几种停止线程的错误方法。比如 stop(),suspend()和 resume(),这些方法已经被 Java 直接标记为@Deprecated。如果再调用这些方法,IDE 会友好地提示,我们不应该再使用它们了。是因为 stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务夏然而止,会导致出现数据完整性等问题。

四.线程的生命周期和安全问题

线程的生命周期(6种状态)

问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?

答:不会,会先抢夺CPU

问:bolocked和waiting的区别:

  • 阻塞状态BLOCKED是线程阻塞在进入synchronized关键字修饰的方法或代码块时(获取锁)的状态
  • 但如果是阻塞在java.concurrent包中的Lock接口的线程状态却为等待状态WAITING,因为Lock接口的阻塞实现均使用的是LockSupport类中的相关方法。

线程的安全问题:

public class MyThread extends Thread{
    //static表示这个类所有的对象,票数是共享的
    static int ticket=0;//0~99
    @Override
    public void run(){
        while (true){
            if(ticket<100){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(getName()+"在卖第"+ticket+"张票");
            }else{
                break;
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /*
        需求:某电影院目前有100张票,3个窗口同时开卖
         */
        //创建三个线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //设置线程名称
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

因此我们可以知道Java多线程并发不安全是指什么?

  • 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作结果是不一致的

Java多线程并发出现问题的根源

  1. 可见性:"线程本地内存"引起(什么是本地内存见:JMM);可见性即是指一个线程对共享变量的修改,另一个线程能够立即看到

  2. 原子性:分时复用引起,原子性即一个操作或多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

    int i=1;
    
    i+=1;
    
    i+=i;
    
    //这里需要注意的是:i +=1 需要三条CPU 指令
    //1.将变量i从内存(主存)读取到 CPU寄存器,
    //2.在CPU寄存器中执行i+1操作;
    //3.将最后的结果i写入内存(本地内存)
  3. 有序性:指令重排引起;有序性即程序执行的顺序按照代码的先后顺序执行

五.同步代码块/方法synchronized

线程同步机制:

  1. 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多只有一个线程访问,以保证数据的完整性
  2. 也可以这样理解:线程同步,即当有一个线程对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作

  把操作共享数据的代码锁起来

上述问题的解决方案

public class MyThread extends Thread{
    //static表示这个类所有的对象,票数是共享的
    static int ticket=0;//0~99

    //锁对象,一定要是唯一的(static)
    static Object obj=new Object();
    
    @Override
    public void run(){
        while (true){
            //同步代码块
            synchronized (obj){
                if(ticket<100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName()+"在卖第"+ticket+"张票");
                }else{
                    break;
                }
            }
        }
    }
}

需要注意的是synchronized是一个非公平锁

同步方法

    就是把synchonized关键字加到方法上,对于普通同步方法,锁是当前实例对象

    记住快捷键Ctrl+Alt+M 方法生成

class SynchronizedObject implements Runnable {

    @Override
    public void run() {
        method();
    }

    // synchronized 定义普通同步方法,默认锁的是当前实例对象,即 instance
    // 而两个线程 t1 和 t2 使用的是同一个 instance,所以线程会在此处竞争锁,竞争成功的线程运行完成之后下一个线程才会执行
    public synchronized void method() {
        System.out.println("线程" + Thread.currentThread().getName() + "开始");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + Thread.currentThread().getName() + "结束");
    }
}

public static void main(String[] args) {
    SynchronizedObject instance = new SynchronizedObject();
    Thread t1 = new Thread(instance);
    Thread t2 = new Thread(instance);
    t1.start();
    t2.start();
}

分析:

  • synchronized定义普通同步方法,默认锁的是当前实例对象,即instance
  • 而两个线程t1和t2使用的是同一个instance,所以线程会在此处竟争锁,竟争成功的线程运行完成之后下一个线程才会执行。

同步方法(非静态)的锁可以是this,也可以是其他对象(要求是一个对象)---->即锁为当前对象

同步方法(静态)的锁为当前类本身 类.class

释放锁:

以下不会释放锁

六.Synchronized原理分析

加锁和释放锁的原理

      本质是对一个对象的监视器(monitor)进行获取,而这个获取的过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。下图是展示线程,对象Object,监视器Monitor,同步队列SychronizedQueue之间的关系

      图中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器Monitor。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

可重入的实现原理

      synchronized对于一个对象加锁后是可以重入的,就是说同一个线程可以反复给该对象加锁,且并不会因为前一次加的锁还没有释放而阻塞。
      原理:Synchronized加锁的对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会加一,释放锁后就会将monitor计数器减一。所以同一个线程反复对该对象加锁时,只会引起monitor计数器加1,并不会触发Monitor.Enter失败的流程,即线程不会被阻塞。释放锁也是一样,每触发一次释放操作锁monitor计数器会减1,当最终monitor计数器重新减为0之后,才真正表示释放了锁。

public class MethodDemo {

    public static void main(String[] args) {
        MethodDemo demo = new MethodDemo();
        demo.method1();
    }

    private synchronized void method1() {
        System.out.println(Thread.currentThread().getName() + " method1()");
        method2();
    }

    private synchronized void method2() {
        System.out.println(Thread.currentThread().getName() + " method2()");
        method3();
    }

    private synchronized void method3() {
        System.out.println(Thread.currentThread().getName() + " method3()"); 
    }
}

执行monitorenter获取锁

  • (monitor计数器=0,可获取锁)
  • 执行method1()方法,monitor计数器+1->1(获取到锁)
  • 执行method2()方法,monitor计数器+1->2
  • 执行method3(方法,monitor计数器+1->3

执行monitorexit释放锁

  • method3()方法执行完,monitor计数器-1->2
  • method2()方法执行完,monitor计数器-1->1
  • method1()方法执行完,monitor计数器-1->0(释放了锁)
  • (monitor计数器=0,锁被释放了)

保证可见性的原理-锁的内存语义
锁的happens-before关系:
      Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() { // 1
        a++; // 2
    }

    public synchronized void reader() { // 4
        int i = a; // 5
    }
}

该代码的happens-before关系如图所示:

      在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2happens-before5(根据程序顺序2 happens-before3.根据监视器锁规则3happens-before 4,而又根据程序顺序4 happens-before 5,最后根据传递性规则2happens-before 5),通过这个关系我们可以得出什么?
      根据happens-before的定义:如果A happens-beforeB,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量a进行加一,由2happens-before5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。
 

锁的内存语义:

  1. 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  2. 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。设置为无效,意思是线程不能再继续使用本地内存里的无效数据,需要从主内存中去读最新的。

锁的升级与优化看下面的文章:

相关知识点

七.lock锁

有没有可能手动释放锁和加锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread{
    //这个类共享tictek
    static int ticket = 0;

     static  Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true)
        {
            lock.lock();
            try {
                if(ticket<100)
                {

                    Thread.sleep(500);
                    ticket++;
                    System.out.println(getName()+"正在卖第"+ticket+"张票");
                }
                else break;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }


        }
    }
}

因为篇幅问题,此处就不再过多介绍,其它详情知识请看我的另一篇学习笔记:Java中的锁

八.volatile关键字

看下述代码:

public class VolatileExample {
    private static boolean stop = false;

    public static void main(String[] args) {
        // Thread-A
        new Thread("Thread A") {
            @Override
            public void run() {
                while (!stop) {
                    System.out.println("3:" + Thread.currentThread().getName());
                }
            }
        }.start();

        // Thread-main
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("1:主线程等待一秒...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("2:将stop变量设置为true");
        stop = true;
    }
}

      执行结果:可看出,即便主线程已经将变量stop变为了true,但是线程A依然没有执行第3步;执行完第2步后就停止了,说明线程A还在while循环中,即线程A读取到的stop值依然为false,并没有读取到主线程对stop变量的更改true;

      当尝试给stop变量加volatile关键字:

private static volatile boolean stop = false;

所以我们知道了volatile变量的作用:如果一个字段被声明为volatile,Java内存模型(JMM)确保所有线程看到这个变量的值一样。

volatile一定的有序性实现:

对一个volatile域的写,happens-before于任意后续对这个volatile域的读

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;           // 1 线程A修改共享变量
        flag = true;     // 2 线程A写volatile变量
    }

    public void reader() {
        if (flag) {      // 3 线程B读同一个volatile变量
            int i = a;   // 4 线程B读共享变量
        }
    }
}

根据happens-before规则,上面过程会建立3类happens-before关系。

  • 根据程序次序规则:1 happens-before 2且3 happens-before4。
  • 根据 volatile 规则:2 happens-before 3。
  • 根据 happens-before 的传递性规则:1 happens-before 4.

volatile禁止重排序:

      为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。针对会改变语义的场景,Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序,JMM针对编译器制定的volatile重排序规则表如下(NO就是需要禁止重排序的场景)

第一个操作 第二个操作 → 普通读/写 volatile读 volatile写
普通读/写
volatile读
volatile写
规律 1:只要“第二个操作”是 volatile 写,一律禁止重排序(整列都是 NO)
  • 看表格: 最后一列(volatile 写)里全是 NO

  • 为什么?

    volatile 写 就像是一个分界碑。JMM 的目标是:在 volatile 写 之前发生的所有操作,绝对不能被重排序到 volatile 写 的后面去。

    如果允许重排序,那就意味着“本来应该在写之前完成的代码,跑到了写之后去执行”,这会导致别人读 volatile 变量时,相关的数据还没准备好。

规律 2:只要“第一个操作”是 volatile 读,一律禁止重排序(整行都是 NO)
  • 看表格: 第二行(volatile 读)里全是 NO

  • 为什么?

    volatile 读 也是一个分界碑。JMM 的目标是:在 volatile 读 之后发生的所有操作,绝对不能被重排序到 volatile 读 的前面去。

    如果允许重排序,那就意味着“本来应该在读了最新数据之后才能执行的代码,居然在读之前就执行了”,这会导致程序拿旧数据去干活,完全失去了 volatile 的意义。

规律 3:当第一个是 volatile 写,第二个是 volatile 读 时,禁止重排序
  • 看表格: 第三行(volatile 写)和第二列(volatile 读)的交叉点是 NO

  • 为什么?

    volatile 写 和紧接着的 volatile 读 之间不能调换顺序。这个很好理解,写在前面、读在后面,这是经典的线程通信模式,绝对不能把“读”提前到“写”的前面。

九.生产者和消费者(等待唤醒机制)

    生产者消费者模式是一个十分经典的多线程协作的模式

生产者和消费者常用方法

public class Desk {
    /*
    作用:控制生产者和消费者的执行
     */

    //是否有面条 0:没有面条 1:有面条
    public  static int foodFlag=0;

    //总个数
    public static int count=10;

    //锁
    public static Object lock=new Object();

}
public class Foodie extends  Thread{
    @Override
    public void run() {
        /*口诀:
        1.循环
        2.同步代码块
        3.判断共享数据是否到了末尾(到了末尾)
        4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */

        while(true){
            synchronized (Desk.lock){
                if(Desk.count==0){
                    break;
                }else{
                    //先判断桌子上是否有面条
                    if(Desk.foodFlag==0){
                        //如果没有,则等待
                        try {
                            Desk.lock.wait();//让当前线程和锁进行绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //把吃的总数减一
                        Desk.count--;
                        //如果桌子上有面条,则吃面条
                        System.out.println("吃面条"+"还能再吃"+Desk.count+"碗");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();
                        //修改桌子状态
                        Desk.foodFlag=0;
                    }
                }
            }
        }

    }
}
public class Cook extends  Thread{
    @Override
    public void run() {
        /*口诀:
        1.循环
        2.同步代码块
        3.判断共享数据是否到了末尾(到了末尾)
        4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */

         while (true){
             synchronized (Desk.lock){
                 if(Desk.count==0){
                     break;
                 }else{
                     //判断桌子上是否有食物
                     if(Desk.foodFlag==1){
                         //如果有,则等待
                         try {
                             Desk.lock.wait();
                         } catch (InterruptedException e) {
                             throw new RuntimeException(e);
                         }
                     }else{
                         //如果没有,则制作事物
                         System.out.println("开始制作食物");
                         //修改桌子上的食物状态
                         Desk.foodFlag=1;
                         //叫醒等待的消费者开吃
                         Desk.lock.notifyAll();
                     }
                 }
             }
         }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        /**
         * 完成生产者消费者(等待唤醒机制的代码)
         * 实现线程轮流交替执行的效果
         */

        //创建线程的对象
        Cook cook = new Cook();
        Foodie foodie = new Foodie();

        //给线程起个名字
        cook.setName("厨师");
        foodie.setName("吃货");

        //启动线程
        cook.start();
        foodie.start();
    }
}

十.阻塞队列方式实现等待唤醒机制

阻塞队列的继承结构

public class Cook extends  Thread{

    ArrayBlockingQueue<String> queue;

    // 构造器
    public Cook( ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断的把面条放到阻塞队列中
            try {
                queue.put("面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("厨师正在做面条");
        }
    }
}
public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;

    // 构造器
    public Foodie( ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true){
            try {
                // 从阻塞队列中获取面条
                String food = queue.take();
                System.out.println("吃货正在吃:"+food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo {
    /**
     * 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
     * 细节:
            生产者和消费者必须使用同一个阻塞队列
     */

    public static void main(String[] args) {
        //1.创建阻塞队列
        ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10);

        //2.创建线程的对象,并把阻塞队列传递过去
        Cook cook = new Cook(queue);
        Foodie foodie = new Foodie(queue);
        
        //3.启动线程
        cook.start();
        foodie.start();
    }
}

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐