【JavaEE】多线程安全问题

时间:2024-05-05 07:09:09

文章目录

  • 1、什么是多线程安全问题
  • 2、出现线程不安全的原因
    • 2.1 线程在系统中是随机调度,抢占式执行的
    • 2.2 多个线程同时修改同一个变量
    • 2.3 线程针对变量的修改操作,不是“原子”的
    • 2.4 内存可见性问题
    • 2.5 指令重排序
  • 3 、如何解决线程安全问题
    • 3.1 锁操作
    • 3.2 synchronized关键字
  • 4、不正确加锁引发的问题
    • 4.1 一个加锁一个不加锁
    • 4.2 可重入锁
    • 4.3 两个线程两把锁-死锁
    • 4.4 死锁的四个特性
      • 5.1 互斥特性
      • 5.2 不可抢占(不可被剥夺)
      • 5.3 请求和保持
      • 5.4 循环等待
    • 4.5 如何避免死锁


1、什么是多线程安全问题

线程是随机调度,抢占式执行,这样的随机性会使程序的执行顺序产生变数,从而产生不同的结果,但是有时候,遇到不同的结果,认为不可接收,认为是bug
多线程代码引起的bug,这样的问题就是“线程安全问题”存在“线程安全问题的代码”,就称为“线程不安全”
线程不安全的例子:

public class Test19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + count);
    }
}

thread1和thread2都对count这个变量进行了++操作,所以我们预期的结果是10w,但是由于出现了线程不安全的问题所以这里输出的结果是小于10w的
注意:每次运行后的结果都是不一样的,大部分情况都是大于5w,但是也有一部分情况是小于5w的
在这里插入图片描述
具体说明:
count++这个代码其实是3个cpu指令
1.把内存count中的数值,读取到cpu寄存器中,我们取个名字叫load
2.把寄存器中的值+1,还是继续保存在寄存器中,取个名字叫add
3.把寄存器上述计算的值2,写回到内存count里,取个名字叫save
两个线程并发的进行count++,多线程的执行,是随机调度,抢占式的执行模式

综上所述在实际并发执行的时候,两个线程执行指令的相对顺序就可能会出现多种情况,不同的执行顺序,得到的结果也就可能会存在差异
在这里插入图片描述
第一种和第二种最终的结果都是正确的,第三种虽然t1和t2都执行了count++操作,但是t1将t2线程中的count进行了覆盖,重新赋值,所以t2线程的操作就是无效的
出现结果小于5w的情况:
在这里插入图片描述
注意:多个线程并发执行的时候,具体指令执行的先后顺序,可能存在无数种情况

2、出现线程不安全的原因

2.1 线程在系统中是随机调度,抢占式执行的

这个是线程不安全的罪魁祸首,万恶之源

2.2 多个线程同时修改同一个变量

一个线程修改同一个变量,没事
多个线程读取同一个变量,没事
多个线程修改不同变量,没事

2.3 线程针对变量的修改操作,不是“原子”的

“原子”指的是不可拆分的最小单位
count++这种不是原子操作,但是针对int/double进行赋值操作,在cpu中就是一个move指令

2.4 内存可见性问题

2.5 指令重排序

后面两个我们会在后面详细介绍

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

原因1:因为涉及到操作系统所以我们无法干预
原因2:这种做法在Java中不是很普适,只能针对一些特定的场景
原因3:是解决线程安全问题最普适的方法,我们可以通过加锁来解决线程安全问题

3.1 锁操作

关于锁操作的两个方面
1.加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待
2.解锁:直到t1解锁之后,t2才有可能拿到锁(加锁成功)
锁的主要特性:互斥
互斥指的是一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(也叫锁竞争/锁冲突)
在代码中,我们可以创建多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会

3.2 synchronized关键字

synchronized关键字用于实现线程同步确保在同一时刻只有一个线程可以访问某个代码块或者方法
synchronized后面带上( ),括号里面写的就是“锁对象”
注意:锁对象的用途,有且只有一个,就是区分两个线程是否是针对同一个对象进行加锁,如果是就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待,如果不是就不会出现锁竞争,也就不会阻塞等待
synchronized下面跟着{ },当进入到代码块就是给上述( )锁对象进行加锁操作,出代码块就是给上述( )锁对象进行解锁操作

public class Test19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //Java中随便拿一个对象,都可以做为加锁的对象
        Object locker1 = new Object();
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker1) {
                    count++;
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker1) {
                    count++;
                }
              
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + count);
    }
}

运行结果:
在这里插入图片描述
此时我们通过加锁就得到了我们预期的结果

使用synchronized关键字修饰实例方法

class Counter {
    private int count = 0;
    synchronized public void add() {
        count++;
    }
    public int get() {
        return count;
    }
}
public class Test20 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + counter.get());
    }
}

使用synchronized关键字修饰静态方法

class Counter {
    private static  int count = 0;
    synchronized public static void add() {
        count++;
    }
    public int get() {
        return count;
    }
}
public class Test20 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + counter.get());
    }
}

4、不正确加锁引发的问题

4.1 一个加锁一个不加锁

public class Test19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker1) {
                    count++;
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                    count++;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + count);
    }
}

运行结果:
在这里插入图片描述
观察结果发现这种情况同样会发生多线程不安全的问题
原因:thread2没加锁,意味着即使thread1加锁了,thread2执行过程中没有任何阻塞,没有互斥,仍然会使thread1 ++到一半的时候,被thread2进来把结果覆盖掉
所有当我们需要对两个线程中且操作同一个方法或者代码块,同时加锁才能解决多线程安全的问题

4.2 可重入锁

同一个线程中,对一个对象进行多次加锁就叫做可重入锁

public class Test19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Thread thread1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker1) {
                    synchronized (locker1) {
                        count++;
                    }
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker1) {
                    count++;
                }

            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count = " + count);
    }
}

假设thread1对代码块进行加锁,那么thread2就不能进行加锁操作,此时就会发生锁冲突,只有thread1解锁之后,threadd2才能进行加锁操作。对于thread1来说外层加完锁之后,此时内层在加锁之前就会判断当前是哪个线程对当前的代码块进行的加锁,如果是当前线程就会无视内层加锁这个操作继续往下执行,如果不是就会阻塞等待
所有当前运行结果是正确的:
在这里插入图片描述
注意:当加了多层锁的时候,代码要执行到最外层 } 花括号才会自动解锁,而不是内层的 } 括号解锁,所有内层的加锁是没用的,在最外层加锁就足够了

4.3 两个线程两把锁-死锁

死锁:两个或者多个线程(或进程)相互等待对方释放资源而造成的一种僵局,导致代码无法正常执行

public class Test21 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread thread1 = new Thread(()-> {
            synchronized (locker1) {
                try {
                    //引用sleep是为了更好的控制线程执行的顺序
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("thread1获取到两把锁");
                }
            }

        });
        Thread thread2 = new Thread(()-> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("thread2获取到两把锁");
                }
            }

        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

在这里插入图片描述
在这里插入图片描述
运行结果:
在这里插入图片描述

4.4 死锁的四个特性

5.1 互斥特性

一个线程拿到锁之后,其他线程就得阻塞等待

5.2 不可抢占(不可被剥夺)

一个线程拿到锁之后,除非它自己主动释放锁,否则别人抢不走

5.3 请求和保持

一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁

5.4 循环等待

多个线程获取多个锁的过程中,出现了循环等待,A等B,B又等A

4.5 如何避免死锁

1.锁具有互斥特性
2.锁不可抢占(不可被剥夺)
前面两种可以自己实现锁来打破,但是对于synchronized这样的锁是不行的
3.请求和保持,打破方法是不要让锁嵌套获取
4.循环等待,打破循环等待,即使出现嵌套也不会死锁,约定好加锁的顺序让所有的线程按照固定的顺序来获取锁