• 【JAVA】多线程


    ❤️ Author: 老九
    ☕️ 个人博客老九的CSDN博客
    🙏 个人名言:不可控之事 乐观面对
    😍 系列专栏:

    进程和线程

    线程

    • 因为计算机的发展,系统支持多任务了。所以就需要并发编程。通过多进程,是完全可以实现并发编程的,但是也有个问题:如果要频繁的创建/销毁进程,就需要分配内存,打开文件,就需要释放资源,释放内存。执行任务的成本较高,主要是因为资源的 创建 和 释放 不是高效的,所以成本较高。

    实现并发编程中,解决 创建 和 销毁 消耗资源大的问题,有两个方法:

    1.进程池:进程池可以解决问题,提高效率。但是也有问题,池子里的闲置进程,不使用的时候也在消耗系统资源,消耗的系统资源太多了。
    2.使用线程来实现并发编程:线程比进程更轻量,每个进程可以执行一个任务,每个进程也能执行一个任务(执行一段代码),也能够并发编程,创建线程的成本比创建进程要低很多,销毁线程的成本也比销毁进程低很多,调度线程的成本也比调度进程低很多。
    3.但是线程不是越多越好,如果线程多了,这些线程可能要竞争同一个资源,这个时候,整体的速度就受到了限制,因为整体硬件资源是有限的。

    线程和进程的区别和联系

    1.进程包含线程:一个进程里可以有一个线程,也可以有多个线程。
    2.进程和线程都是为了处理并发编程这样的场景。但是进程有问题,频繁的创建和释放的时候效率很低,相比之下,线程更轻量,创建和释放的效率更高。因为线程少了申请和释放的过程
    3.操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位。操作系统创建的线程,是要在 CPU 上调度执行,线程是操作系统调度执行的基本单位。
    4.进程具有独立性,每个进程有各自的虚拟地址空间,一个进程挂了,不会影响到其它进程。同一个进程中的多个线程,公用同一个内存空间,一个线程挂了,可能影响到其他线程,甚至导致程序崩溃。
    5.线程比进程轻量的原因:进程重量重在资源的申请释放。线程是包含在进程当中的,一个进程中的多个线程共用同一份资源。只是创建第一个进程的时候(由于要分配资源),成本是相对高的,后续在这个进程中再创建其他线程,这个时候成本就要更低一些,因为不必再分配资源了。
    6.可以把进程比作一个工厂,线程就是生产线,线程多了之后,生产效率就高了,如果再建一个工厂来生产,效率也可以变高,但是资源花费大,所以通过增加一条生产线(线程)来提高效率的话,资源花费就很小。

    并发编程

    Java 中并发编程主要用多线程,不同于其他语言。go 语言是通过多协程来实现,erlang 是通过 actor 模型实现并发, js 是通过定时器 + 实际回调的方式实现并发。

    最基本的多线程代码

    • 通过 Thread 来创建,不过这里是创建一个自己的 MyTread 并重写 run 方法来看线程的情况。run 方法就描述了线程内部要执行什么代码代码如下:
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("hello thread");
        }
    }
    public class TestDemo {
        public static void main(String[] args) {
            Thread t = new MyThread();
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • run 方法里面描述了线程内部要执行哪些代码,每个线程都是并发执行的(各自执行各自的代码,就是告诉线程,要执行的代码是什么)。不是一定义这个类,一写 run 方法,线程就创建出来,相当于有活了,但是还没干。调用 new 的对象的 start 方法,才是真正的创建了线程。这里可以创建很多个线程,这些线程都是同一个进程内部创建的。运行结果如下:
      在这里插入图片描述

    最简单的并发编程代码

    • 一个进程中,至少会有一个线程,在一个 Java 进程中,至少会有一个调用 main 方法的线程**,自己创建的 t 线程,和自动创建的 main 线程,就是并发执行的关系**(宏观上看起来是同时执行)。这里的代码就是 MyThread 和 main 一起并发执行。代码如下:
    class MyThread2 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.println("Thread!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class TestDemo2 {
        public static void main(String[] args) {
            MyThread2 t = new MyThread2();
            t.start();
            while (true) {
                System.out.println("Main");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    这里的结果就是 Thread!和 Main 交替输出,每次输出和上次输出差不多相隔一秒:
    在这里插入图片描述
    在阻塞一秒之后,先唤醒 Thread 还是 Main,是不确定的。对于操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的**(抢占式执行)**。

    Runnable接口

    Runnable 就是在描述一个任务,然后重写 run 方法,就是要执行的任务内容。然后通过 Runnable 把描述好的任务交给 Thread 实例:

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("hello");
        }
    }
    public class TestDemo3 {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    通过匿名内部类

    匿名继承Thread类

    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("Thread");
            }
        };
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里的匿名内部类是继承自 Thread 类,同时重写了 run 方法,同时再 new 出这个匿名内部类的实例。运行结果如下:

    匿名Runnable

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread");
            }
        });
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里 new 的 Runnable 针对这个创建的匿名内部类,同时 new 出的 Runnable 实例传给 Thread 的构造方法。

    Thread和Runnable的选择

    • 通常认为选择 Runnable 来写更好一些,能够做到让线程和线程执行的任务,更好的解耦。写代码注重 高内聚,低耦合。Runnable 单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程池来执行,还是协程来执行,Runnable 并不关心,Runnable 里面的代码也不关心。

    lambda表达式

    在使用多线程的时候,也可以写成 lambda 表达式,这种表达式方法更简单:

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            System.out.println("Thread");
        });
        t.start();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    多线程对时间的优化

    我们来计算两个变量的自增,从 0 自增到 10亿,然后对比时间。一种是串行执行,一种是并发执行。不过要注意的是,多线程当中的时间戳代码是在 main 线程中,所以要等到 t1 和 t2 都执行玩然后再计时。所以通过 join(); 方法来等待计时结束,代码如下:

    public static void serial() {
        long beg = System.currentTimeMillis();
        long a = 0;
        for (int i = 0; i < 10_0000_0000; i++) {
            a++;
        }
        long b = 0;
        for (int i = 0; i < 10_0000_0000; i++) {
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.print((end-beg)+"ms");
    }
    public static void concurrency() throws InterruptedException {
        long beg = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            long a = 0;
            for (int i = 0; i < 10_0000_0000; i++) {
                a++;
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            long b = 0;
            for (int i = 0; i < 10_0000_0000; i++) {
                b++;
            }
        });
        t2.start();
        t1.join();
        t2.join();
        long end = System.currentTimeMillis();
        System.out.print((end-beg)+"ms");
    }
    public static void main(String[] args) throws InterruptedException {
        serial();
        System.out.println();
        concurrency();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在这里插入图片描述

    • t1.join()在t2.start()之前会导致t1线程先执行完后再执行t2线程,而t2.start()在t1.join()之前会让t1和t2可以并行执行。
    • 可以看出,多线程的运行效率确实比串行要高,不过如果数很小的时候,就不适合用多线程了,因为创建线程也需要时间,如果很小的话,用串行就够了。多线程适用于 CPU 密集型的程序,程序要进行大量的计算,使用多线程就可以更充分的利用 CPU 的多核资源。

    Thread 类的属性和方法

    Thread创建线程对象并命名

    在创建完 Thread 对象之后,可以对其进行命名。命名之后在调试的时候很方便:

    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            while (true) {
                System.out.println("Thread t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread t1");
        t1.start();
    
        Thread t2 = new Thread(()-> {
            while (true) {
                System.out.println("Thread t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"Thread t2");
        t2.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    isAlive

    • 创建出对象之后,在调用 start 之前,系统当中是没有对应线程的,在 run 方法执行完了之后,系统当中的线程就销毁了。但是 t 这个对象可能还存在。通过 isAlive 就能判断当前系统的线程的运行情况。
    public class TestDemo{
        public static void main(String[] args) throws InterruptedException {
           Thread t1 = new Thread(()->{
               int count = 0;
               while (count < 2){
                   System.out.println("thread t1");
                   count++;
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           },"Thread t1");
            t1.start();
            t1.join();
            System.out.println(t1.isAlive());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    start

    **start 决定了系统中是不是真的创建出线程,run 只是一个普通的方法,描述了任务的内容。**代码如下:

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (true) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    那么把 start 换成 run :

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (true) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.run();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    发现 run 也能输出 Thread 。**但关键是 run 并没有创建线程,这里的 run 是输出了任务的内容,而不是创建线程。Thread 则是在操作系统当中创建线程。**下面用一个更简单理解的代码来演示:

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (true) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while (true) {
            System.out.println("Main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    因为 start 是创建线程,所以会和 main 线程并发执行。如果换成 run 的话:

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (true) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.run();
        while (true) {
            System.out.println("Main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    只输出了任务内容,没有创建线程,只是从上往下执行。

    中断线程

    就是让线程停下来,线程停下来的关键,是要让线程对应的 run 方法执行完。(还有一个特殊情况:是 main 这个线程,对于 main 来说,得是 main 方法执行完,线程就完了)

    设置自定义标志位

    通过手动设置一个标志位,来控制线程是否要执行结束。代码如下:

    private static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (!isQuit) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuit = true;
        System.out.println("终止线程");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    因为多个线程共同用一个虚拟地址空间,因此 main 线程修改的 isQuit 和 t 线程判断的 isQuit 是同一个值。

    使用 Thread 内置的标志位

    通过Thread.interrupted()方法终止线程
    通过Thread.currentThread().isInterrupted(),其中 currentThread 能够获取到当前线程的实例。

    public static void main(String[] args) {
        Thread t = new Thread(()-> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("收尾工作");
                    break;
                }
            }
        });
        t.start();
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        t.interrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    这里调用interrupt()方法,可能会出现两种情况;
    1.如果线程处于就绪状态,就是设置线程的标志位为true
    2.如果t线程处在阻塞状态(sleep 休眠了),就会触发一个interruptException异常,使其从阻塞状态被唤醒,允许线程在捕获异常后继续执行或进行清理操作。

    线程等待join

    多个线程之间,调度顺序不确定。线程之间的执行是按照调度器来安排的,这个过程可能是无序,随机的。线程等待,就是其中一种,控制线程执行顺序的手段,主要是控制线程结束的先后顺序。
    哪个线程调用join,就等这个线程执行完毕之后(对应的线程的run方法执行完),再执行别的线程
    但是 join 默认情况下,是死等。所以 join 提供了另外一个版本,可以执行等待时间,最长等多久,等不到就撤了。就是在 join(时间)

    public class JoinExample {
        public static void main(String[] args) {
            // 创建一个子线程
            Thread workerThread = new Thread(new Worker());
            workerThread.start();
    
            // 等待子线程执行,最多等待2秒
            try {
                workerThread.join(2000);
                if (workerThread.isAlive()) {
                    System.out.println("等待超时,主线程继续执行");
                } else {
                    System.out.println("子线程已经完成,主线程继续执行");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        static class Worker implements Runnable {
            @Override
            public void run() {
                System.out.println("子线程开始执行");
                try {
                    Thread.sleep(3000); // 模拟子线程执行3秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行结束");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    获取当前线程的引用

    Thread 自带的 getName实现

    通过 currentThread() 的 getName() 方法 ,不过要注意的是,哪个线程调用这个方法,获取到的就是哪个线程的引用。

    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println(this.getName());
            }
        };
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里是通过 Thread 的方式来创建线程。此时在 run 方法当中,直接通过 this 拿到的就是 Thread 的实例。运行结果如下:
    在这里插入图片描述
    没指定名字,默认是 0。

    Runnable 实现

    如果是 Runnable 的话,就不能用 this.getName 了,因为 Runnable 是一个单纯的任务,没有 name 属性。会直接抛出受查异常。所以只能用 Thread.currentThread().getName() ,如果是 lambda 表达式,也是这样。代码如下:

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    线程休眠 sleep

    进程和线程都是通过PCB描述的,PCB通过 双向链表 组织的,通过双向链表,就可以可以轻松地在PCB之间建立前后关系,如果一个进程包含了多个线程,所对应的就是一组PCB,PCB上有个字段tgroupid,这个id就相当于进程的id,同一个进程当中若干个线程的tgroupId是相同的。
    >

    线程安全

    线程安全是线程当中最重要,最复杂的问题。多进程是最基本的处理并发编程的任务。操作系统调用线程的时候,是随机的(抢占式执行),因为是抢占式的,所以可能出现 bug 如果调度随机性,引入了 bug,那么就认为代码线程是不安全的。

    class Counter {
        public static int count;
        public void increase() {
            count++;
        }
    }
    public class Test2 {
        private static Counter counter = new Counter();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(()-> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(()-> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(counter.count);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在代码中,用 count 作为两个线程自增的变量。运行多次,结果如下:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    运行多次之后发现结果总是不能达到 100000,就说明程序有 bug,两个程序是并发执行的,如果两个线程同时自增,就只加了 1。
    count++ 其实是 三个 CPU指令:
    1.把内存当中的 count 值,加载到 CPU 寄存器当中。
    2.把寄存器当中的值 + 1。
    3.把寄存器的值写回到 内存的 count 当中。

    • 这里加的结果是在 50000 - 100000 之间。因为有一部分是串行的,有一部分是交错的。所以,如果能让 t1 先执行完,然后再让 t2 执行,就可以解决这样的问题了。

    通过加锁来保证线程安全

    像上面这种情况,就可以通过加锁来解决,我们这里使用 synchronized 来对 count++ 加锁,因为问题是出在 count++ 这里,所以我们对 count++ 加锁就好了。也就是在自增之前先加锁,自增之后解锁。解锁之后再执行另外一个线程。加锁之后,并发程度降低,数据更安全了,但是速度慢了。并发性越高,数据越不安全,但速度越快,但是可能会出现一些问题,就像这里的 count++ 。实际开发当中,一个线程要做的事很多。可能只有某一个步骤有线程安全,所以只对有线程安全的加锁就好了。代码如下:

    class Counter {
        public static int count;
        synchronized public void increase() {
            count++;
        }
    }
    public class Test2 {
        private static Counter counter = new Counter();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(()-> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(()-> {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(counter.count);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

    线程不安全的原因

    1.线程是抢占式执行,线程间的调度充满随机性,是线程不安全的万恶之源。
    2.多个线程对同一个变量进行操作。
    3.针对变量的操作不是原子性的(要么全部执行完,要不就不执行)
    4.内存可见性:是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。例如:针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(合适的时候执行一次)。读内存比读寄存器慢很多,循环一直去读的话,消耗就会很多,因此,频繁的读内存的值,就会非常低效,而且修改的线程迟迟不修改,读到的值一直是一样的值。所以,读的时候,就可能不从内存读数据了,而是直接从寄存器里面读。如果此时 把值修改了,那么就读不到这个值了。
    5.指令重排序:也是编译器优化的一种操作。就是调整代码的执行顺序,执行效果不变,但是效率就提高了。调整的前提也是逻辑不变。代码是单线程的话,一般不会出问题,如果是多线程的话,就可能出现问题,避免问题还是通过 synchronized 加锁来操作。

    内存可见性导致线程不安全

    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (isQuit == 0) {
    
            }
            System.out.println("循环结束,t 线程退出");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值:");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    通过图片可以看到,当输入值之后,已经不满足线程执行的条件了,但是线程并没有停止,就是因为内存可见性的原因,导致线程还在运行。如果在主线程中加入sleep的话,引入一个短暂的睡眠来确保写入的值已经在主内存中可见,然后在新线程中进行检查。

    private static volatile int isQuit = 0;
    
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
    
            }
            System.out.println("循环结束,t 线程退出");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值:");
        isQuit = scanner.nextInt();
    
        // 确保isQuit的值在新线程中可见
        try {
            Thread.sleep(100); // 休眠一段时间,确保主线程写入的值可见
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println("main 线程执行完毕");
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    解决内存可见性的办法(volatile)

    1.使用 synchronized 关键字加锁。不光保证原子性,还能保证内存可见性。被 synchronized 包裹起来的代码,编译器就不敢轻易的做出上面优化的那种操作。
    2.使用 volatile 关键字,volatile 和原子性无关,但是能保证内存可见性。就会禁止编译器做出优化,使得编译器每次判断的时候,都会重新从内存当中读取 isQuit 的值。

    private static volatile int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (isQuit == 0) {
    
            }
            System.out.println("循环结束,t 线程退出");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个 isQuit 的值:");
        isQuit = scanner.nextInt();
        System.out.println("main 线程执行完毕");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    指令重排序

    指令重排序也会影响到线程安全问题,也是编译器优化的一种操作。举例:去超市买东西:
    在这里插入图片描述
    如果按照菜单顺序买菜的话,就会绕很多路,浪费很多时间。如果重新排序之后再买的话,就是下面这种情况:
    在这里插入图片描述
    就会节省很多时间,这就是指令重排序带来的优化。不过在有些时候,写的功能很多的情况下,指令重排序也会导致程序,出现 bug。所以通过 synchronized 来解决这种问题。

    synchronized使用

    synchronized 是同步的意思。多线程中,线程安全中,同步 指的是“互斥”,一个进行的时候,另外一个就不能进行了。

    直接修饰使用方法

    使用 synchronized 直接修饰普通方法。本质是对某个对象进行加锁。在 Java 当中,每个类都是继承自 Object 。每个 new 出来的实例,里面一方面包含了自己安排的属性,一方面包含了“对象头”,对象的一些元数据。此时的锁对象就是 this,如下图所示:
    在这里插入图片描述

    当多个线程试图获得锁时,只有当它们竞争相同的锁对象时才会发生竞争。如果多个线程分别尝试获取不同的锁对象,它们之间不会互相竞争。
    举个简单的例子来理解:假设有两个线程A和B,它们都希望访问两个不同的资源,资源X和资源Y。如果线程A尝试获取资源X的锁,而线程B尝试获取资源Y的锁,它们之间没有竞争,因为它们操作的是不同的资源,互不干扰。
    但是,如果线程A和线程B都试图获取相同的资源X的锁,那么它们将会发生竞争,因为它们都想要独占相同的资源。在这种情况下,只有一个线程能够成功获取锁,而另一个线程必须等待或采取其他操作。

    修饰代码块

    使用 synchronized 修饰一个代码块。需要显示指定针对哪个对象加锁(Java 中的任意对象都可以作为锁的对象)。代码如下:

    public void increase() {
        synchronized (this) {
            count++;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    修饰一个静态方法

    使用 synchronized 修饰一个静态方法。相当于针对当前的类对象加锁,也就是反射。把 synchronized 修饰到 static 方法上:
    在这里插入图片描述
    就相当于是下面这种情况:
    在这里插入图片描述

    可重入锁

    就是外层先加了一次锁,然后里层再对对象加一次锁。代码示例:

    synchronized public void increase() {
        synchronized (this) {
            count++;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    外层锁: 进入方法,则开始加锁,这次能够加锁成功,因为当前锁没有人占用。
    里层锁: 进入代码块,开始加锁,这次加锁不能加锁成功,因为这个锁被外层占用了,要等到外层锁释放,里层锁才能加锁。
    外层锁要执行完整个方法,才能释放。但是要想执行完整个方法,就得让里层锁加锁成功继续往下走。所以就变成死锁了。
    为了防止出现这种情况,JVM 就实现了可重入锁,就是发生这种操作的的时候,不会死锁。就是可重入锁内部,会记录当前的锁被哪个线程占用,同时也会记录一个加锁次数。线程 a 针对锁第一次加锁的时候,是可以加锁成功的。锁内部就记录了当前的占用着的是 a,加锁次数是 1。后续再 a 对锁进行加锁,此时就不是真加锁,而是单纯的把计数器自增,加锁次数为 2。然后在解锁的时候,先把计数进行 -1,当锁的计数减到 0 的时候,就真的解锁。可重入锁的意义就是:降低了程序员的负担(降低了使用成本,提高了开发效率),但也带来了代价,程序中需要又更高的开销(维护锁属于哪个线程,并且加减计数,降低了运行效率)。

    哲学家就餐问题

    在这里插入图片描述
    每个哲学家都很固执,在想要吃饭的时候,如果筷子被别人占用,就会死等下去。所以,如果五个人同时拿起左手边的筷子,就陷入死锁了。每个人都能拿起左手的筷子,每个人都拿不起右手的筷子。

    死锁的四个必要条件

    1.互斥使用:一个锁被一个进程或线程占用之后,其他线程占用不了(锁的本质,保证原子性)。
    2.不可抢占:一个锁被一个线程占用之后,其他线程不能把这个锁给抢走(挖墙脚不行)。
    3.请求和保持:进程或线程至少有一个资源,并且在请求其他资源时保持对己有资源的占有(在等待其他资源时不释放自己资源)
    4.环路等待:等待关系,成环了:A 等 B,B 等 C,C 又等 A。避免环路等待:约定好,针对多把锁加锁的时候,有固定的顺序就好。所有的线程都遵守同样的规则顺序,就不会出现环路等待。

    Java标准库当中的类

    线程安全的部分:
    1.ConcurrentHashMap
    2.StringBuffer
    3.String
    线程不安全部分:
    1.ArrayList
    2.LinkedList
    3.HashMap
    4.TreeMap
    5.HashSet
    6.TreeSet
    7.StringBuilder

    volatile 与 synchronized

    volatile 只保证内存可见性,不保证原子性。禁止编译器优化,保证内存可见性。
    如果无脑用 synchronized 的话,其他线程会被阻塞,直到获得锁的线程执行完毕。这会导致线程在争夺锁时发生阻塞,浪费了CPU的时间;一旦线程被阻塞,下次的执行时间是不可控的,线程可能在阻塞队列中等待,导致程序的性能和响应时间都不可预测;从而影响了高性能。
    volatile 就不会引起线程阻塞。

    wait和notify

    wait 和 notify 。就是等待和通知。是处理线程调度随机性的问题的,不喜欢随机性,需要让彼此之间有个固定的顺序。join 也是一种控制顺序的方式,更倾向于控制线程结束。
    调用 wait 方法就会陷入阻塞。阻塞到有其他线程通过 notify 来通知。 wait 内部会做三件事:1、先释放锁 2、等待其他线程的通知 3、收到通知之后,重新获取锁,并继续往下执行。因此,想用 wait/notify 就得搭配 synchronized。代码如下:

    public static void main1(String[] args) throws InterruptedException {
        Object object = new Object();
        //wait 哪个对象,就得针对哪个对象加锁。
        synchronized (object) {
            System.out.println("wait 前");
            object.wait();
            System.out.println("wait 后");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    搭配举例

    在第一个线程 wait 之后,就可以通过第二个线程的 notify 来唤醒第一个线程。代码如下:

    import java.util.Scanner;
    
    public class JoinExample {
        public static Object locker = new Object();
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                synchronized (locker) {
                    System.out.println("wait 之前");
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait 之后");
                }
            },"wait");
            t1.start();
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(3000);
            Thread t2 = new Thread(() -> {
                synchronized (locker) {
                    System.out.println("notify 之前");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    locker.notify();
                    System.out.println("notify 之后");
                }
            },"notify");
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    然后代码当中就是 wait 三秒之后,进入线程二,然后打印出 notify 之后,再等待 3秒,然后使用 notify 唤醒。
    注意:只有将Thread.start方法触发之后,才开始创建执行线程
    运行结果如下:
    在这里插入图片描述
    然后唤醒:
    在这里插入图片描述

    notifyAll

    假如有一个对象 lock 有 10 个线程,都调用了 o.wait 此时 10 个线程都是阻塞状态。如果调用了 o.notify 就会把 10 个当中的一个给唤醒(唤醒哪个不确定),使用 notifyAll 就会把所有的 10 个线程都给唤醒。wait 唤醒之后,就会重新尝试获取到锁(这个过程就会发生竞争),直到把10个线程全部唤醒后执行完再结束。

        class Example1 {
            private final Object lock = new Object();
    
            public void threadA() throws InterruptedException {
                synchronized (lock) {
                    System.out.println("Thread A is doing some work.");
                    lock.wait(); // 线程A等待
                    System.out.println("Thread A has been notified and is continuing.");
                }
            }
    
            public void threadB() throws InterruptedException {
                synchronized (lock) {
                    System.out.println("Thread B is doing some work.");
                    lock.wait(); // 线程B等待
                    System.out.println("Thread B has been notified and is continuing.");
                }
            }
    
            public void notifyThreads() {
                synchronized (lock) {
                    System.out.println("Notifying all waiting threads.");
                    lock.notifyAll(); // 唤醒所有等待的线程
                }
            }
    
        }
    
    public class Example {
        public static void main(String[] args) throws InterruptedException {
            Example1 e = new Example1();
    
            Thread threadA = new Thread(() -> {
                try {
                    e.threadA();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            });
    
            Thread threadB = new Thread(() -> {
                try {
                    e.threadB();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            });
    
            Thread threadC = new Thread(() -> {
                e.notifyThreads();
            });
    
            threadA.start();
            threadB.start();
    
            Thread.sleep(1000);
            threadC.start();
        }
    }
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    ♥♥♥码字不易,大家的支持就是我坚持下去的动力♥♥♥
    版权声明:本文为CSDN博主「亚太地区百大最帅面孔第101名」的原创文章

  • 相关阅读:
    Mapping 设计指南
    基于java+SpringBoot+VUE +mysql学生信息管理系统的设计与实现
    Hyperbolic geometry
    基于Python OpenCV的金铲铲自动进游戏、D牌...
    第68步 时间序列建模实战:ARIMA建模(Matlab)
    【JavaScript复习】【一篇就够】作用域
    章鱼网络进展月报 | 2022.10.1-10.31
    Linux之gdb调试工具
    路径几何图形的各种线段
    编译构建 meson ninja
  • 原文地址:https://blog.csdn.net/partworld/article/details/133911725