• Java EE——线程(3)


    上一篇博客主要介绍了Thread类的各种方法,并且阐述了线程不安全的缘由,这篇博客就来讨论一下如何解决线程不安全的问题

    线程不安全的原因

    抢占式执行

    为了程序执行的效率,我们的线程采用抢占式执行,也就是谁抢到资源谁就可以完成任务,因此多个线程执行任务时线程的调度是随机的,因此我们很难找到规律

    多个线程修改同一个变量

    这个原因我们在上一篇博客讲过了。需要注意的是,一个线程修改一个变量,多个线程修改不同的变量,多个线程读不同的变量都是不存在线程安全问题的,只有多个变量修改同一个变量才会出现问题

    修改不是原子的

    上一条出现问题的原因就是因为修改并不是原子的——即有可能在修改的读操作时另一个线程就把资源占用了,因此,我们把修改操作锁起来,使得修改时别的线程无法对这个变量进行操作,就能够解决线程安全问题。

    需要注意的是,我们不能看代码是一行,修改操作就是原子性的,而是应该看代码背后的cpu操作逻辑

    内存可见性问题

    由于机器会优化我们的代码,即如果进行100次a++操作,本身应该cpu从内存读取a变量的值,++后再放回内存,这个操作进行100次
    但是由于代码自动的优化,就有可能变成了cpu从内存读取a变量的值,++100次后再放回内存。
    因此,虽然代码跑的效率更高了,但是在多线程操作时可能产生意想不到的后果

    指令重排序

    也是代码的优化,代码将指令的顺序重新排序,使得执行的逻辑不变,效率提升,但是在多线程操作下可能产生问题

    线程不安全解决方案

    我们通过将修改操作变成原子的来解决线程不安全问题

    加锁

    当一个线程访问变量时,先对变量加锁,完成任务后再对变量进行解锁,当别的线程访问这个已经被加锁的变量,那么就会触发阻塞等待的状态

    synchronized

    我们的java使用synchronized关键字来加锁,在一个方法前用这个关键字来修饰,那么就可以使这个方法变成原子性的

    public synchronized void 方法名(){
    
    }
    
    • 1
    • 2
    • 3

    需要注意的是,由于加锁使得原来多线程的并发执行,变成了串行执行,因此效率会下降,因此我们在确保必要的情况下再进行加锁操作

    synchronized还可以修饰代码块

    synchronized (对象) {
    
    }
    
    • 1
    • 2
    • 3

    我们要对哪个对象加锁,就在括号中填哪个对象,如果填的是this,那么谁调用这个代码块外面的方法,那么谁就是this。

    1. synchronized直接修饰方法,就相当于锁的对象是this
    2. 两个线程在锁同一个对象时会触发锁的阻塞等待,在执行不同对象时就不存在竞争

    因此通过加锁操作,我们可以让之前两个线程同时对一个变量++操作的代码进行优化

    public class sumByThread {
        final static int SUM = 100;
        static long a = 0;
        static Count c = new Count();
    
        static class Count {
            public synchronized void count(){
                a++;
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < SUM; i++) {
                    c.count();
                }
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < SUM; i++) {
                    c.count();
                }
            });
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(a);
        }
    
    }
    
    
    • 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

    下面演示一下不同情况下的加锁状态

    demo1
    public class demo1 {
        static int count = 0;
        public static class Counter{
            public void increase(){
                synchronized (this){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase();
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    我们的synchronized的加锁对象是this,也就是谁调用increase方法,谁就加锁。我们的t1和t2都是通过counter来调用increase方法的,因此会产生锁冲突

    demo2
    public class demo2 {
        static int count = 0;
        public static class Counter{
            public void increase(){
                synchronized (this){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter1 = new Counter();
            Counter counter2 = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter1.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter2.increase();
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    上面的代码同样是对this进行加锁,但是t1和t2是通过不同的对象调用increase方法,因此两个线程不会出现锁竞争

    demo3
    public class demo3 {
        static int count = 0;
        public static class Counter{
            public Object locker = new Object();
            public void increase(){
                synchronized (locker){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase();
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这次我们在Counter类中加入了一个专门用来加锁的对象locker,我们synchronized修饰了这个对象,那么如果我们访问的是同一个locker对象,就会发生锁竞争,由于t1和t2访问的是同一个counter对象,而同一个counter对象中的locker对象就是相同的,因此会发生竞争。

    和demo1不同的是,我们专门创建了一个locker对象来对increase方法进行加锁,以后如果还有increase2方法的话,我们可以创建locker2对象来对increase2方法进行加锁,从而使这两个方法互相没有影响

    demo4
    public class demo4 {
        static int count = 0;
        public static class Counter{
            public Object locker = new Object();
            public void increase(){
                synchronized (locker){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter1 = new Counter();
            Counter counter2 = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter1.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter2.increase();
                }
            });
        }
    }
    
    • 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

    和demo3不同的是,demo4中t1和t2访问的是不同的counter对象,因此其内部的locker对象也是不同的,因此不构成锁冲突

    demo5
    public class demo5 {
        static int count = 0;
        public static class Counter{
            public static Object locker = new Object();
            public void increase(){
                synchronized (locker){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter1 = new Counter();
            Counter counter2 = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter1.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter2.increase();
                }
            });
        }
    }
    
    • 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

    和demo4不同的是,demo5中的locker对象是static修饰的,也就是说其是一个静态成员,也就是类属性,而一个进程中类对象只有一个,类属性也只有一个,因此t1和t2虽然是通过不同的counter对象调用increase方法,但是这两个实例中的locker对象是同一个,因此还是会发生锁冲突

    demo6
    public class demo6 {
        static int count = 0;
        public static class Counter{
            public static Object locker = new Object();
            public void increase(){
                synchronized (locker){
                    count++;
                }
            }
            public void increase2(){
                synchronized (this){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter.increase2();
                }
            });
        }
    }
    
    • 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

    demo6中increase1针对locker加锁,而increase2针对调用该方法的当前对象加锁,因此t1针对静态的locker加锁,而t2针对counter对象加锁,二者并不是访问同一个对象,因此不构成锁冲突

    demo7
    public class demo7 {
        static int count = 0;
        public static class Counter{
            public void increase(){
                synchronized (Counter.class){
                    count++;
                }
            }
        }
        public static void main(String[] args) {
            Counter counter1 = new Counter();
            Counter counter2 = new Counter();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter1.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    counter2.increase();
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    demo7中我们针对类对象进行加锁。类对象在之前的反射中讲过,来自于.class文件,在jvm进程中只有一个,因此多个进程针对类对象加锁会产生锁竞争

  • 相关阅读:
    Modbus动态链接库供多语言使用 | Go
    PostgreSQL 13支持增量排序(Incremental Sorting)
    vue移动端高德地图的使用及实现最简单的地图功能
    c#.NET技术做到ChatGPT流式响应并实现打字机效果 实现ChatGPT的Stream传输
    【python&flask-1】简单实现加减乘除输入界面
    设定并理解随机数种子 && Pytorch在Dataloader类中设置shuffle的随机数种子方式
    Pandas中数据类型的理解
    手撕Vue-编译指令数据
    【Linux】进程地址空间
    ubuntu用户与用户组管理
  • 原文地址:https://blog.csdn.net/m0_60867520/article/details/126770086