• Java EE——线程(4)


    synchronized的特性

    互斥

    当一个线程访问到一个synchronized的对象,那么就会对这个对象进行加锁,这个时候如果有其他的线程访问,那么就会阻塞等待,只有前一个对象访问完毕后,对该对象进行解锁操作后,其他线程才能访问
    注意:
    阻塞等待的线程们并不遵守先来后到的原则,而是重新竞争这个对象,上一个线程对对象解锁后,下一个线程并不能立刻获取到这个对象,而是需要操作系统进行唤醒

    刷新内存

    这个说法存在争议!!!!
    我们线程对synchronized的工作流程:

    1. 获得互斥锁
    2. 将变量从主内存拷贝到工作内存
    3. 执行代码逻辑
    4. 将变量拷回到主内存
    5. 释放锁
      因此synchronized也可以保证内存可见性

    可重入

    一个线程可以对同一个对象加锁两次。
    如果代码是不可重入的,如果我们的线程第一次访问这个对象,会对其进行加锁,但是第二次访问这个对象时,由于对象已经上锁,因此会拒绝线程的访问,而这个线程没法完成接下来的代码,也就无法释放锁,因此会出现死锁情况
    可重入锁的底层实现原理
    记录是哪个线程持有这个锁,并记录加锁次数,当加锁次数是0时,就可以释放这个锁了

    volatile

    这个关键字对标我们上一篇博客讲到的内存可见性问题的解决。当我们的变量由volatile修饰时,就保证了内存可见性

    回顾内存可见性

    由于计算机认为我们的代码写的并不完美,因此会自动进行优化。这种优化在单线程情况下不会出现问题,但是电脑无法预测多线程操作对代码的影响,因此可能会出现一些因电脑优化带来的bug

    demo

    import java.util.Scanner;
    
    public class demo8 {
        public static class Counter{
            public int count = 0;
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
               while(counter.count == 0){
    
               }
                System.out.println("t1执行结束");
            });
            Thread t2 = new Thread(() -> {
                System.out.println("请输入数字:");
                Scanner scanner = new Scanner(System.in);
                counter.count = scanner.nextInt();
            });
    
            t1.start();
            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

    上面的这个代码块我们本来预期的结果是,当我们输入一个不为0的数后,系统会显示t1执行结束。但是实际结果并非如此。

    这是因为当我们在执行counter.count == 0时,实际上计算机做了两个操作,分别是将数据从内存读取数据的LOAD,和进行和0比较的CMP。然而LOAD的开销比CMP大很多个数量级,因此计算机为了效率的提升,对代码进行优化。

    由于在t1这个线程中,并没有其他的值对count进行修改,因此编译器的循环过程就省略了LOAD过程,只进行CMP,因此不管t2进程有没有修改count的值,t1线程读的都是老版本的值,因此t1线程不会执行结束

    因此我们要使用volatile关键字来避免编译器对count变量作出的优化

    使用方法

    用volatile来修饰一个变量,使得编译器在读取这个变量时只从内存中读取,不从寄存器中读取

    volatile public int count = 0;
    
    • 1

    事实上,加volatile关键字并不是解决这个问题的唯一办法,我们可以让线程休息一段时间,也可以得到类似的结果

    import java.util.Scanner;
    
    public class demo9 {
        public static class Counter{
            public int count = 0;
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                while(counter.count == 0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t1执行结束");
            });
            Thread t2 = new Thread(() -> {
                System.out.println("请输入数字:");
                Scanner scanner = new Scanner(System.in);
                counter.count = scanner.nextInt();
            });
    
            t1.start();
            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

    因为当我们让线程休息一段时间,编译器就认为读取count值所用的时间和休息的这一秒来比无关紧要,因此就不会进行优化了

    我们可以发现,编译器的优化是很复杂的,和代码,运行环境等都有关系,因此很多时候我们并没法预期代码的执行结果

    需要注意的是,我们的volatile并不保证原子性,因此还是使用synchronized最为保险

    JMM

    其全称是Java Memory Model,也就是Java内存模型

    由于外界对外存叫内存,对运行内存叫内存,Java为了统一术语,使其更加严谨通用,能够跨平台,使大家感知不到硬件的差异,因此设计了这个模型

    在JMM中将CPU寄存器,缓存叫做工作内存,而我们常说的内存叫做主内存

    因此我们的volatile就是防止编译器偷懒,每次只从工作内存中读取数据,而是使编译器去主内存中读取数据

    变量捕获

    当我们在Thread类外访问一个变量时,往往会报错,这是因为我们不能直接访问类外面的局部变量。但是有些情况下我们需要访问,因此涉及到变量捕获概念
    解决的方法就是让这个变量被final修饰

    Java标准库中的线程安全类

    下面这些都是线程不安全的

    1. ArrayList
    2. LinkedList
    3. HashMap
    4. TreeMap
    5. HashSet
    6. TreeSet
    7. StringBuilder

    下面的是线程安全的

    1. Vector
    2. HashTable
    3. ConcurrentHashMap
    4. StringBuffer

    wait 和 notify

    这两个关键字是通过使一个线程阻塞等待和唤醒来调整线程的执行顺序的

    wait

    wait是Object的方法,因此任何类的实例都可以调用wait方法

    Object object = new Object();
    try {
        object.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    异常就是被interrupt方法唤醒

    当我们的线程执行到wait时,会先释放锁,让这个线程持有的对象可以被其他线程访问到,然后就会阻塞等待,直到另一个线程调用notify才能唤醒,唤醒后会重新竞争锁

    我们的wait和notify都要在synchronized中

    notify

    如果有多个线程在等待这把锁,notify会随机唤醒一个线程,并没有先来后到这个说法

    demo

    public class demo2 {
        static class WaitTask implements Runnable{
            private Object locker;
            public WaitTask(Object locker){
                this.locker = locker;
            }
    
            @Override
            public void run() {
                synchronized (locker){
                    while(true){
                        System.out.println("wait 开始:");
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("wait 结束:");
                    }
                }
            }
        }
    
        static class NotifyTask implements Runnable{
            private Object locker;
            public NotifyTask(Object locker){
                this.locker = locker;
            }
            @Override
            public void run() {
                synchronized (locker){
                    System.out.println("notify 开始:");
                    locker.notify();
                    System.out.println("notify 结束:");
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Object locker = new Object();
            Thread t1 = new Thread(new WaitTask(locker));
            Thread t2 = new Thread(new NotifyTask(locker));
            t1.start();
            Thread.sleep(1000);
            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
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    我们可以看到代码的执行过程是先wait开始,然后notify开始,结束,最后wait结束

  • 相关阅读:
    windows批处理 将当前路径添加到Windows的`PATH`环境变量中 %~dp0
    用C#通过sql语句操作Sqlserver数据库教程
    windows11 如何关闭 vbs
    Kubernetes(k8s)的Namespace和Pod实战入门
    基于SSM的开心农家乐系统设计与实现
    Fabric二进制建链
    Python 基础记录 - 第4课
    flex:1详解,以及flex:1和flex:auto的区别
    1470. Shuffle the Array
    【JavaEE进阶系列 | 从小白到工程师】正则表达式的语法&使用
  • 原文地址:https://blog.csdn.net/m0_60867520/article/details/126841058