线程安全(1)

线程安全

我用了两个线程去调用Count类中的countAdd方法,来看看结果是否与预想的一样.
如果它是线程安全的,那么两个线程调用countAdd方法之后,count结果应该是10000.
class Count {
    public int count = 0;
    public void countAdd() {
        
        this.count++;
    }
}

public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Count c1 = new Count();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                c1.countAdd();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                c1.countAdd();
            }
        });

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

        System.out.println("count = " + c1.count);
    }
}

在这里插入图片描述
可是最终结果是6457,说明它是线程不安全的(每次运行结果可能都不一样).
那么,为什么呢?
我们在进行count++的时候,本质上是三个步骤.

  • 把内存中的数据放到寄存器中(load).
  • 把寄存器中的数据+1(add).
  • 把寄存器中的数据写回到内存中(save).
    所以,我们在进行count++操作时,线程并发执行就会出现问题.
    如果在进行这三个操作的时候,顺序错乱了,就会出现不一样的结果.
    在这里插入图片描述

线程安全的原因

    1. [根本原因]多个线程之间的调度是"随机"的,操作系统通过"抢占式"执行的策略来调度线程.
    1. 多个线程同时修改同一个变量,容易产生线程安全问题.
    1. 线程修改变量的时候,并不是"原子的",这也是我们解决线程安全问题的最主要的切入手段.
    1. 内存可见性问题.(放到后面)
    1. 指令重排序.(放到后面)

我们可以通过加锁来解决:

class Count {
    public int count = 0;
    public void countAdd() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (this) {
            this.count++;
        }

    }
}

public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Count c1 = new Count();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                c1.countAdd();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                c1.countAdd();
            }
        });

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

        System.out.println("count = " + c1.count);
    }
}

在这里插入图片描述
在这里插入图片描述
通过锁操作后,让多个线程在同一时刻,只有一个线程能够使用这个变量.
通过加锁操作后,把并发执行变成串行执行了,为什么还要使用多线程?

我们只是在count++的位置加锁了,for循环中,我们并没有加锁.
因为在for循环中,变量i只是一个栈上的局部变量.
这两个for循环,也就是两个线程是有着独立的栈空间,是完全不同的变量,也就不涉及线程安全问题了.
在这两个线程中,有一部分是串行执行的,一部分是并行执行的,比纯粹的串行执行效率要高,所以我们要使用多线程.

synchronized

这是java中给我们提供的加锁的关键字.
1)进入代码块就加锁.
2)出了代码块就解锁.
是不是很方便!
synchronized进行加锁操作,是以"对象"为维度展开的.

我们在上述代码中,就根据this对象进行加锁.
此时两个线程针对用一个this对象加锁,就会出现锁竞争(锁冲突),即一个线程加锁成功,其它线程阻塞等待.

// 两个线程针对同一个locker对象进行加锁
class Count {
    public int count = 0;
    private Object locker = new Object();
    public void countAdd1() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

    }

    public void countAdd2() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

    }
}
// 两个对象针对不同对象加锁,就不会出现锁竞争,就会有线程安全问题.
class Count {
    public int count = 0;
    private Object locker = new Object();
    public void countAdd1() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (this) {
            this.count++;
        }

    }

    public void countAdd2() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

    }
}

总结 : 只有两个线程针对同一个线程进行加锁,就会出现锁竞争(锁冲突),即一个线程加锁成功,其它线程阻塞等待.

互斥性

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

刷新内存

synchronized 的工作过程:
1. 得到互斥锁.
2. 从主内存中拷贝变量的副本到工作内存中.
3. 执行代码逻辑.
4. 将更改后的变量刷新到主内存.
5. 释放互斥锁.

可重入性

// 两个线程针对同一个locker对象进行加锁
class Count {
    public int count = 0;
    private Object locker = new Object();
    public void countAdd1() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

    }

    public void countAdd2() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

    }
}

在这段代码中,假设countAdd1()已经加锁,当执行到countAdd2()时(是可以执行到的,因为是同一个 对象),又会再次加锁,这份代码是没有问题的,synchronized是可重入锁.

synchronized的使用

  1. 修饰普通方法:
public class Count {
    public synchronized void countAdd() {
   }
}
  1. 修饰静态方法:
public class SynchronizedDemo {
    public synchronized static void methond() {
   }
}
  1. 修饰代码块:
public void countAdd1() {
        // 加锁,把三个步骤变成一个原子性问题
        synchronized (locker) {
            this.count++;
        }

}

结语

在下篇博客中,依旧会总结线程安全问题,请大家多多支持.

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值