Java多线程~如何解决线程安全问题(volatile和synchronized)

目录

产生线程安全的原因

修改共享数据

原子性

可见性

代码顺序性

解决线程安全问题

synchronized关键字

volatile关键字

总结

用法示例


产生线程安全的原因

修改共享数据

简单地说,当多个线程并发并行的操作同一个共享变量,且至少有一个线程对这个共享变量进行修改时,就会存在线程安全问题 

原子性

多行指令是不可拆分的最小执行单位,就具有原子性,如果不满足原子性,就会存在线程安全问题.

例如java中的n++操作,其看似是一行代码,实际上可以分解为:

①从主存中读取读取数据到cpu

②进行数据更新

③再从cpu中将数据写回到主存 

该操作不满足原子性,因此会存在线程安全问题

可见性

可见性是指一个线程对共享变量的修改,能够及时的被其他线程看到.

多个线程同时操作同一个共享变量,首先需要从主存中读取数据到cpu,所以多个线程并发并行操作即使是同一个共享变量,也是互相看不到的,不满足可见性。

例如:有一个共享变量,a线程对共享变量进行读操作,b线程对共享变量进行修改操作,当a线程读到共享变量时,此时b线程完成了对共享变量的修改操作,此时a线程读到的数据不是最新的数据,因此存在线程安全问题.

代码顺序性

执行字节码指令或者是执行机器码指令时,都可能为了提高运行效率,使用指令重排序的方式来执行

例如new对象操作会分解为:

①申请对象的内存空间

②初始化操作

③赋值给变量

正确的执行顺序应该是①②③,但是发生指令重排序后就可能变成①③②,此时就会存在线程安全问题

解决线程安全问题

设计多线程代码的原则是:在满足线程安全的前提下,尽可能地提高代码的执行效率.

synchronized关键字

多个线程对同一个对象进行加锁,具有同步互斥的作用,其原理就是基于对象头加锁,满足了原子性、可见性、有序性. 

synchronized的作用

 · 互斥:某个线程执行到某个对象的synchronized中时,如果其他线程也执行到同一个对象的synchronized时就会阻塞等待,这样就保证了原子性

· 刷新内存:synchronized结束释放锁,会把工作内存中的数据刷新到主存,其他线程申请锁时,获取的始终都是最新的数据,保证了可见性

· 有序性:某个线程执行一段同步代码,不管如何重排序,期间都不可能有其他线程执行的指令,这样多个线程执行同步代码,就满足了一定的顺序

· 可重入:同一个线程可以多次申请同一个锁

synchronized用法

· 同步代码块

    public static void main(String[] args) {
        //synchronized语法一:同步代码块
        Object o = new Object();
        synchronized (o) {

        }
        //也可以使用类对象
        synchronized (Grammar.class) {

        }
    }

· 同步实例方法 

    //语法二:同步实例方法
    public synchronized void t1() {

    }
    //this是指当前线程,谁(当前类的某个实例对象 )调用这个实例方法就是谁
    public void t1_equals() {
        synchronized(this) {

        }
    }

· 同步静态方法

   //语法三:静态同步方法
    public static synchronized void t2() {

    }
    //等同于
    public synchronized void t2_equals() {
        synchronized(Grammar.class) {

        }
    }

volatile关键字

volatile的作用为:

①保证可见性:多个线程对同一个变量的操作,具有可见性

②禁止指令重排序,建立内存屏障

但是其不能保证原子性

其使用场景为:共享变量的读操作以及常量的赋值(即本身就满足原子性的操作)

语法

private volatile static Singleton3 instance;

volatile禁止指令重排序并建立内存屏障是什么意思? 

例如new对象操作可以分解为:

①申请对象的内存空间

②初始化操作

③赋值给变量

例如a线程对共享变量进行写操作,b线程对共享变量进行读操作,当a线程开始对共享变量操作时,必须等到①②③步全部执行完,并写到主存之后,b线程才能进行读操作.

两个线程中的volatile操作,一个写,一个读,如果是写操作先进行,必须等到写操作结束后,才能开始读操作.

总结

①如果某个线程对共享变量只是读操作,此时只需要给共享变量加上volatile即可保证线程安全,因为读操作本身具有原子性,而volatile可以保证可见性和有序性,所以结合起来就可以保证线程安全

 ②如果某个线程对共享变量进行写操作,可以通过加锁来保证线程安全,因为加锁可以同时解决三个线程不安全的原因:原子性、可见性、有序性

③如果多个线程对共享变量进行操作,既有读操作又有写操作,则需要在写操作时,对其进行加锁,而读操作,只需要给共享变量加上volatile即可,这样既满足了线程安全又可以提高运行效率.

用法示例

package threadhomework;

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

例如上述线程安全的单例模式就是synchronized和volatile结合的用法,当instance为空需要进行写操作时,就对其进行synchronized加锁,读操作就直接读,对共享变量加上volatile保证有序性和可见性. 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Li_yizYa

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值