• 多线程Synchronized锁的使用与线程之间的通讯


    一、什么是线程安全问题

    多线程同时对同一个全局变量做写操作,可能会受到其他线程的干扰,就会发生线程安全问题。

    Java中的全局变量是存放在堆内存中的,而堆内容对于所有线程来说是共享的。

    比如下面一个简单的代码案例:

    
    public class ThreadCount implements Runnable{
        private int count = 10;
    
        @Override
        public void run() {
            while (true) {
                if (count > 1) {
                    try {
                    	// 模拟两个线程的阻塞状态
                        Thread.sleep(30);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + " == " + count);
                }
            }
        }
    
        public static void main(String[] args) {
            ThreadCount threadCount = new ThreadCount();
            // 启动两个线程执行任务 
            new Thread(threadCount).start();
            new Thread(threadCount).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

    代码比较简单,我们看下面控制台的打印:

    Thread-1 == 9
    Thread-0 == 9
    Thread-0 == 7
    Thread-1 == 7
    Thread-1 == 6
    Thread-0 == 6
    Thread-1 == 5
    Thread-0 == 5
    Thread-1 == 4
    Thread-0 == 4
    Thread-0 == 2
    Thread-1 == 2
    Thread-1 == 1
    Thread-0 == 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到两个线程之间产生了冲突,产生了线程安全问题。

    二、如何解决线程安全问题

    如何解决线程安全问题呢?或者说如何实现线程的同步呢?
    核心思想:加锁

    在同一个JVM中,多个线程需要竞争锁的资源。

    那么哪些代码需要加锁呢?
    可能会发生线程安全性问题的代码需要加锁。

    还是上面的例子,我们在哪里加锁合适呢?
    (1)锁加在run()方法上

    @Override
        public synchronized void run() {
            while (true) {
                if (count > 1) {
                    try {
                    	// 模拟两个线程的阻塞状态
                        Thread.sleep(30);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + " == " + count);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样可不可以呢?是可以的,但是我们来思考一个问题,如果synchronized加在了run()方法上,那么该执行过程是单线程还是多线程呢?
    答案是单线程。我们可以看到以下控制台打印:

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这是为什么呢?
    原因是因为synchronized加在了run()方法上,获取到锁的线程不会释放锁,会一直持有锁,所以方法的执行就变成了单线程的;没有获取锁的线程,如果一直没有获取锁,中间需要经历一个锁的升级过程,最后会一直阻塞等待锁的释放。

    (2)锁加在操作共享资源的代码上

    @Override
        public void run() {
            while (true) {
                if (count > 1) {
                    try {
                        Thread.sleep(30);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (this) {
                        count--;
                        System.out.println(Thread.currentThread().getName() + " == " + count);
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    直接看控制台的输出结果:

    Thread-1 == 9
    Thread-0 == 8
    Thread-0 == 7
    Thread-1 == 6
    Thread-1 == 5
    Thread-0 == 4
    Thread-0 == 3
    Thread-1 == 2
    Thread-1 == 1
    Thread-0 == 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这就解决了线程安全问题。
    过程就是第一个线程和第二个线程同时去竞争this锁,假设第一个线程获取到锁,那么第二个线程就会阻塞等待,等第一个线程执行完操作资源后,释放锁之后才会获取到锁,执行操作。

    三、synchronized锁的基本用法

    1.修饰代码块,指定加锁对象,对指定对象加锁,进入同步代码块前要获取 给定对象 的锁。
    2.修饰实例方法,作用于当前实例加锁,进入同步代码块前要获取 当前实例 的锁。
    3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码块前要获得 当前类对象 的锁。

    1、修饰代码块(this锁)

    public class ThreadCount implements Runnable {
        private int count = 10;
    
        @Override
        public void run() {
            while (true) {
                sub();
            }
        }
        public void sub() {
            if (count > 1) {
                try {
                    Thread.sleep(30);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                // 修饰代码块,即this锁,进入同步代码块之前需要获取对象锁
                synchronized (this) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + " == " + count);
                }
            }
        }
    
        public static void main(String[] args) {
        	// 同一个实例,执行不同线程,是线程安全的,不会出现问题
            //ThreadCount threadCount = new ThreadCount();
            //new Thread(threadCount).start();
            //new Thread(threadCount).start();
            // 不同实例,执行不同线程,会出现线程安全问题,这就是对象锁
            // 代码比较简单,可自行执行测试
            ThreadCount threadCount1 = new ThreadCount();
            ThreadCount threadCount2 = new ThreadCount();
            new Thread(threadCount1).start();
            new Thread(threadCount2).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

    2、修饰实例方法(this锁)

    	@Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(30);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                sub();
            }
        }
        // 将synchronized加在实例方法上,则使用的还是this锁
        public synchronized void sub() {
            if (count > 1) {
                count--;
                System.out.println(Thread.currentThread().getName() + " == " + count);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3、修饰静态方法

    	private static int count = 10;
    	
    	...
    	// 使用当前的  类名.class 锁
    	public static synchronized void sub() {
            if (count > 1) {
                count--;
                System.out.println(Thread.currentThread().getName() + " == " + count);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    相当于

    	public static  void sub() {
            synchronized(ThreadCount.class) {
                if (count > 1) {
                    count--;
                    System.out.println(Thread.currentThread().getName() + " == " + count);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    四、死锁问题

    (1)死锁

    所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

    在这里插入图片描述

    (2)死锁产生的必要条件

    产生死锁的必要条件:

    • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
    • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
    • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

    (3)诊断synchronized死锁

    以下这段代码会产生死锁问题,我们当作测试案例:

    public class DeadlockThread implements Runnable {
        private int count = 1;
        private final String lock = "lock";
    
        @Override
        public void run() {
            while (true) {
                count++;
                if (count % 2 == 0) {
                    synchronized (lock) {
                        a();
                    }
                } else {
                    synchronized (this) {
                        b();
                    }
                }
            }
        }
    
        public synchronized void a() {
            System.out.println(Thread.currentThread().getName() + ",a方法...");
        }
    
        public void b() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ",b方法...");
            }
        }
    
        public static void main(String[] args) {
            DeadlockThread deadlockThread = new DeadlockThread();
            Thread thread1 = new Thread(deadlockThread);
            Thread thread2 = new Thread(deadlockThread);
            thread1.start();
            thread2.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

    诊断死锁我们可以使用jdk8自带的诊断工具jconsole.exe

    在这里插入图片描述
    如图,双击打开,选择对应的进程。
    在这里插入图片描述

    这里我们本地,不需要登录,直接选择不安全方式连接。
    在这里插入图片描述
    连接成功之后,点击线程,点击检测死锁,该工具就可以帮我们自动检测到产生死锁的线程,如图:
    在这里插入图片描述
    还能够显示出死锁线程的具体信息,锁的拥有者,以及对应的代码行数:
    在这里插入图片描述

    五、线程如何实现同步

    线程如何实现同步?
    或者说线程如何保证线程安全性问题?

    • 使用synchronized锁,JDK1.6开始,锁的升级过程
    偏向锁 --> 轻量级锁 --> 重量级锁
    
    • 1
    • 使用Lock锁(JUC),需要自己实现锁的升级过程,底层是基于AQS+CAS实现
    • 使用ThreadLocal,但是需要注意内存泄漏的问题
    • 原子类CAS非阻塞式

    六、多线程之间的通信

    1、等待/通知机制

    等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object 上,方法如下:
    1.notify():通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁;
    2.notifyAll():通知所有等待在该对象的线程;
    3.wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会主动释放对象的锁。

    2、生产者和消费者模型

    下面我们通过一个案例来展示生产者消费者模型,模拟过程为两个线程,一个输入线程一个输出线程,输入线程输入内容,输出线程立马打印:

    public class ThreadTest {
    	// 共享变量
        class Res {
            public String userName;
            public char sex;
        }
    
        /**
         *  输入线程
         */
        class InputThread extends Thread {
            private Res res;
            public InputThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    if (count == 0) {
                        res.userName = "zal";
                        res.sex = '男';
                    } else {
                        res.userName = "zzal";
                        res.sex = '女';
                    }
                    count = (count + 1) % 2;
                }
            }
        }
    
        /**
         * 输出线程
         */
        class OutPutThread extends Thread {
            private Res res;
            public OutPutThread(Res res) {
                this.res = res;
            }
            @Override
            public void run() {
                while (true) {
                    System.out.println(res.userName + ", " + res.sex);
                }
            }
        }
    
        public static void main(String[] args) {
            new ThreadTest().print();
        }
    
        public void print() {
            // 全局对象
            Res res = new Res();
            // 输入线程
            InputThread inputThread = new InputThread(res);
            OutPutThread outPutThread = new OutPutThread(res);
            inputThread.start();
            outPutThread.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

    然后我们看控制台输出打印,发现了问题。
    在这里插入图片描述
    这就意味着该代码出现了线程安全性问题,那么为了解决线程安全性问题,我们就需要对线程进行加锁,那么锁哪些代码块呢?

    肯定是锁Res对象。

    代码改进如下:

    public class ThreadTest {
        class Res {
            public String userName;
            public char sex;
        }
    
        /**
         *  输入线程
         */
        class InputThread extends Thread {
            private Res res;
    
            public InputThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    synchronized (res) {
                        if (count == 0) {
                            res.userName = "zal";
                            res.sex = '男';
                        } else {
                            res.userName = "zzal";
                            res.sex = '女';
                        }
                    }
                    count = (count + 1) % 2;
                }
            }
        }
    
        /**
         * 输出线程
         */
        class OutPutThread extends Thread {
            private Res res;
    
            public OutPutThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                while (true) {
                    synchronized (res) {
                        System.out.println(res.userName + ", " + res.sex);
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            new ThreadTest().print();
        }
    
        public void print() {
            // 全局对象
            Res res = new Res();
            // 输入线程
            InputThread inputThread = new InputThread(res);
            OutPutThread outPutThread = new OutPutThread(res);
            inputThread.start();
            outPutThread.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
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    我们对输入线程和输出线程的res对象都加了锁,并且锁住的是同一个对象,这下不会再出现线程安全问题了,运行截图如下:

    在这里插入图片描述
    可是又出现了新的问题,那就是输入和输出一片一片的打印,并不能实现我们输入线程输入,输出线程立马输出的功能。

    出现问题的原因就是当输入线程获取锁的时候,那么输出线程就不能获取锁,就会进入阻塞状态,而当输出线程进行输出的时候,输入线程就不能输入了,所以就会出现这种现象。

    最后,我们使用生产者和消费者模型进行改进代码,代码如下:

    public class ThreadTest {
        class Res {
            public String userName;
            public char sex;
    
            /**
             * flag 标志
             *  当flag = false时,输入线程输入,输出线程等待
             *  当flag = true时,输出线程输出,输入线程等待
             */
            public boolean flag = false;
        }
    
        /**
         *  输入线程
         */
        class InputThread extends Thread {
            private Res res;
    
            public InputThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    synchronized (res) {
                        if (res.flag) {
                            try {
                                res.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        if (count == 0) {
                            res.userName = "zal";
                            res.sex = '男';
                        } else {
                            res.userName = "zzal";
                            res.sex = '女';
                        }
                        // 输出线程可以输出值
                        res.flag = true;
                        // 唤醒输出线程
                        res.notify();
                    }
                    count = (count + 1) % 2;
                }
            }
        }
    
        /**
         * 输出线程
         */
        class OutPutThread extends Thread {
            private Res res;
    
            public OutPutThread(Res res) {
                this.res = res;
            }
    
            @Override
            public void run() {
                while (true) {
                    synchronized (res) {
                        // 如果res.flag = false,则输出线程主动释放锁
                        // 同时会阻塞线程
                        if (!res.flag) {
                            try {
                                res.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        System.out.println(res.userName + ", " + res.sex);
                        res.flag = false;
                        // 唤醒输出线程
                        res.notify();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            new ThreadTest().print();
        }
    
        public void print() {
            // 全局对象
            Res res = new Res();
            // 输入线程
            InputThread inputThread = new InputThread(res);
            OutPutThread outPutThread = new OutPutThread(res);
            inputThread.start();
            outPutThread.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
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
  • 相关阅读:
    【面试题精讲】为什么重写equals时必须重写hashCode方法?
    基础语法——组合与继承
    教小白搭建Hive分区分桶表
    【JAVA基础】【查漏补缺】06 - 字符串
    vscode_c++_slambook 编译配置
    快速排序-交换排序
    【云栖 2023】林伟:大数据 AI 一体化的解读
    MySQL数据库基础知识(二)
    安卓Compose(二)
    Nginx学习(在 Docker 中使用 Nginx)
  • 原文地址:https://blog.csdn.net/z318913/article/details/127696656