赞
踩
简单来说,某个代码,无论是在单个线程下执行,还是在多个线程下执行,都不会产生BUG,把这个情况称为 “线程安全” 。
若是某个代码,在单个线程下顺利运行,但是转到多线程情况下,就可能会产生BUG,这个情况称为“线程不安全”或者“存在线程安全问题”
- 根本原因:操作系统上的线程是 “抢占式执行”、“随机调度” 的,我们无法预知,就导致了线程之间的执行顺序带来了很多变数。(可以称为 “罪魁祸首”)
- 代码结构问题:代码中多个线程,同时修改同一个变量(也是源于上面的根本原因,导致一些指令出现混乱的排序。后面会介绍到可以通过使用 “锁” 来解决这类问题。)
- 直接原因:多线程修改操作,本身不是原子(一个程序执行多个cpu指令,执行到一半,就可能会被调度走,从而给其他线程 “可乘之机”)
- 内存可见性问题:高度依赖编译器的优化的具体实现,一个线程对共享变量值的修改,未能够及时的被其他线程看到。(在执行一些指令操作时,花费的开销比较大,并且一直没有结果。这时JVM就会出来怀疑,判断,然后会进行代码优化,去除一些JVM认为没有必要的操作,提高程序运行效率。从而会导致,后面的一些操作执行时,被优化的代码无法产生响应,出现BUG。)
- 指令重排序问题: 在多线程下,通过JVM、CPU指令集会对某些代码进行优化。把1->2->3的顺序可能会优化为1->3->2,但是在多线程情况下代码的执行复杂程度更高,容易打乱 “保持逻辑不发生变化”这一重要前提。
下面的线程不安全代码中,涉及到多个线程针对 count 变量进行修改。
此时这个 count 是一个多线程都能访问的 “共享数据”。
- public static int count = 0;
- public static void main(String[] args) throws InterruptedException {
- //通过两个线程,同时修改count变量,使他们分别++
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
-
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
-
- t1.start();
- t2.start();
-
- t1.join();
- t2.join();
- System.out.println("count="+count);
- }
得到的结果:<100000
锁(synchronized):通过特殊的手段把系统的指令打包成一个整体(类似原子)
锁 具有 “互斥”、“排他” 的特性,在两个线程中对同一个对象 加锁 就会产生“锁竞争”,使其运行中产生 “阻塞(BLOCKED)”。通过锁竞争无法方第二个线程在第一个线程执行的时候插队,需要排队等待,在顺利执行完锁里面的操作后,继续执行。
- public static int count = 0;
- public static void main(String[] args) throws InterruptedException {
-
- Object locker = new Object();
-
- Thread t1 = new Thread(()->{
- for (int i = 0; i < 50000; i++) {
- //加锁,使得count++的指令操作在锁里面完成,不被插队,包装成一个完整的count++指令
- synchronized (locker){//注意:这里的locker参数,可以是任意的Object
- count++;
- }
- }
- });
- Thread t2 = new Thread(()->{
- for (int i = 0; i < 50000; i++) {
- //加锁,使得count++的指令操作在锁里面完成,不被插队,包装成一个完整的count++指令
- synchronized (locker){//注意:这里的locker参数,可以是任意的Object
- count++;
- }
- }
- });
-
- t1.start();
- t2.start();
-
- t1.join();
- t2.join();
- System.out.println("count="+count);
- }
- class Test{
- public static int count = 0;
- //1.
- public void add(){
- for (int i = 0; i < 50000; i++) {
- /*
- 为什么static中不能使用this
- 静态方法不依赖于任何对象就可以进行访问,既然都没有对象,就谈不上this了
- static叫静态方法,也叫类方法,它先于任何的对象出现。
- 在程序最开始启动(JVM初始化)的时候,就会为static方法分配一块内存空间,成为静态区,属于这个类。
- 而非static方法,必须在类实例化的时候,才会给分配内存空间,
- 在实例化对象的时候JVM在堆区分配一个具体的对象,this指针指向这个对象。
- 也就是说,this指针是指向堆区中的类的对象,而static域不属于this指向的范围所在,所以不能调用。
- */
- synchronized (this){
- //加锁的锁对象,写作this
- count++;
- }
- }
- }
- }
-
- public class Thread_12 {
- public static void main(String[] args) throws InterruptedException {
- Test t = new Test();
- //这里的 this 都指向的是 t ,是一样的两个对象加锁,因此任然存在锁竞争
- Thread t1 = new Thread(() -> {
- t.add();
-
- });
- Thread t2 = new Thread(() -> {
- t.add();
- });
-
-
- t1.start();
- t2.start();
-
- t1.join();
- t2.join();
- System.out.println("count=" + t.count);
- }
- }
- class Test{
- public static int count = 0;
- synchronized public void add(){
- for (int i = 0; i < 500000; i++) {
- count++;
- }
- }
- }
-
- public class Thread_12 {
- public static void main(String[] args) throws InterruptedException {
- Test t = new Test();
- //两个线程拿到的类对象是同一个对象,因此任存在锁竞争,可以保障线程安全
- Thread t1 = new Thread(() -> {
- t.add();
-
- });
- Thread t2 = new Thread(() -> {
- t.add();
- });
-
-
- t1.start();
- t2.start();
-
- t1.join();
- t2.join();
- System.out.println("count=" + t.count);
- }
- }
以上三个例子,通过实现“锁”后得到的结果:
加锁是能解决多线程安全问题的,但是如果添加方式不对,就可能产生死锁!!
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
- public static void main(String[] args) {
- Object locker = new Object();
- Thread t = new Thread(()->{
- //可重入 性,同一个线程可以两次加锁,不会出现阻塞
- //c++中这样使用两个锁会出现 卡死 “死锁”状态
- //正常情况下不使用 可重入锁,
- synchronized (locker){
- synchronized ((locker)){
- System.out.println("hello");
- }
- }
- });
- t.start();
-
-
- }
两个线程互不相让,A要获取B,B要获取A,导致接下来都无法执行,形成阻塞,出现死锁
- public static void main(String[] args) {
- Object A = new Object();
- Object B = new Object();
- Thread t1 = new Thread(()->{
- synchronized (A){
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- //尝试获取B,没有释放A
- synchronized (B){
- System.out.println("t2 ");
- }
- }
- });
- Thread t2 = new Thread(()->{
- synchronized (B){
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- //尝试获取A,没有释放B
- synchronized (A){
- System.out.println("t1 ");
- }
- }
- });
-
- t1.start();
- t2.start();
改进,约定加锁顺序,先对A加锁,后对B加锁
- Thread t1 = new Thread(()->{
- synchronized (A){
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- synchronized (B){
- System.out.println("t2 ");
- }
- }
- });
- Thread t2 = new Thread(()->{
- //改进,直接线获取A,破除循环等待
- synchronized (A){
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- synchronized (B){
- System.out.println("t1 ");
- }
- }
- });
在解决这里问题之前我们再来了解一下死锁的产生
1. 互斥使用,获取锁的过程是互斥的。
一个线程拿到了这把锁,另一个线程也想要获取,就需要阻塞等待
2. 不可抢占,一个线程拿到了锁之后,只能主动解锁,不让别的线程强行把锁抢走
3. 请求保持,一个线程拿到了锁A后,在持有A的前提下,尝试获取B
4. 循环等待 / 环路等待
上述条件1~3点都不方便去破坏,我们通常可以通过 指定加锁顺序 这样的方式破坏代码结构,来破除循环等待。
volatile 修饰的变量,能保证 “内存可见性”
在这个代码中
- private static int flag = 0;
- //volatile 保证内存可见性 禁止指令从排序
- public static void main(String[] args) {
-
- Thread t1 = new Thread(()->{
- while (flag == 0){
-
- }
- System.out.println("t1 线程结束");
- });
- Thread t2 = new Thread(()->{
- System.out.println("请输入flag的值:");
- Scanner scanner = new Scanner(System.in);
-
- flag = scanner.nextInt();
- });
- t1.start();
- t2.start();
- }
- //执⾏效果
- // 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)
此时t1 读的是自己工作内存中的内容
当 t2 对 flag 变量进行修改,此时 t1 感知不到 flag 的变化
如果给 flag 加上 volatile
-
- private volatile int flag = 0;
-
- // 执⾏效果
- // 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束.
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。