线程安全

线程安全性

定义

  当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程安全类的特点

  • 原子性

  提供互斥访问,同一时刻只能有一个线程对它进行操作。比如SynchronizedLockAtomic(包下的类)、CAS算法

  • 可见性

  一个线程对主内存的修改可以及时被其它线程观察到。比如:SynchronizedVolatile

  • 有序性

  一个线程观察其它线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。而符合有序性的类有:VolatileSynchronizedLock,除此之外,在某些情况下,其线程的执行也可能是有序的,比如Java内存模型具备一些先天的有序性,而该有序性要求其符合Happens-Before原则

线程安全类举例

原子性

  • Synchronized
  • Lock
  • Atomic(包下的类)
  • CAS算法

Synchronized

  依赖于JVM,其特点:

  • 修饰代码块

  大括号括起来的代码,作用于调用的对象

  • 修饰方法

  整个方法,作用于调用的对象

  • 修饰静态方法

  整个静态方法,作用于所有对象

  • 修饰类

  括号括起来的部分,作用于所有对象

各种原子性锁的对比

名称特点
Synchronized不可中断锁,适合竞争不激烈,可读性好
Lock可中断锁,多样化同步,竞争激烈时能维持常态
Atomic(包下的类)竞争激烈时能维持常态,比Lock性能好;只能同步一个值

可见性

  导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

Synchronized

  JMM关于Synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

Volatile

  通过加入内存屏障和禁止重排序优化来实现

  • Volatile变量写操作时,会在写操作后加入一条Store屏障指令,将本地内存中的共享变量值刷新到主内存
  • Volatile变量读操作时,会在读操作前加入一条Load屏障指令,从主内存中读取共享变量

有序性

  Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

   具有有序性的类有:VolatileSynchronizedLock

   其中SynchronizedLock通过保证某一时刻只有一个线程在执行,相当于单线程,因而可以保证有序性,而Volatile则是通过禁止编译器和处理器对指令重排序进而保证有序性。

  除了这些之外,Java内存模型具备一些先天的有序性。即不需要通过任何手段就能够得到保证的有序性,这个通常称为Happens-Before原则。如果两个操作的执行次序无法从Happens-Before原则推导出来,那么它们就不能保证它们的有序性。虚拟机可以随意的对它们进行重排序。

Happens-Before原则

  Happens-Before原则,就是先行发生原则,总共涉及到8条原则:

  • 程序次序规则

  一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  • 锁定规则

  一个Unlock操作先行发生于后面对同一个锁的Lock操作

  • Volatile变量规则

  对一个变量的写操作先行发生于后面对这个变量的读操作

  • 传递规则

  如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  • 线程启动规则

  Thread对象的start()方法先行发生于此线程的每一个动作

  • 线程中断规则

  对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  • 线程终结规则

  线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行

  • 对象终结规则

  一个对象的初始化完成先行发生于它的finalize()方法的开始

其它保证线程安全的方法

堆栈封闭

  从JMM中我们得知,所谓堆栈,指的是线程的堆栈。而堆栈封闭,指的就是线程在执行过程中,动态的创建,使用本地变量或对象、在使用结束后,直接销毁该变量或对象,换言之,该变量与对象自始至终都是在一个线程内运作,永远不会在多个线程之间所共享。

  堆栈封闭,表面上感觉高大上,其实际上使用起来异常的简单。

  如下所示:

public void someMethod(){

  long threadSafeInt = 0;

  threadSafeInt++;
}

本地对象引用

  对对象的本地引用略有不同。引用本身不共享。但是,所引用的对象并不存储在每个线程的本地堆栈中。所有对象都存储在共享堆中。

  如果在本地创建的对象从未转义它在其中创建的方法,那么它就是线程安全的。因而我们可以将它传递给其他方法和对象,只要这些方法或对象中没有一个使传递的对象对其他线程可用。

  如下所示:

public void someMethod(){

  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

  本例中的LocalObject实例不从方法返回,也不传递给任何可以从someMethod()方法外部访问的其他对象。执行someMethod()方法的每个线程将创建自己的LocalObject实例,并将其分配给LocalObject引用。因此,这里LocalObject的使用是线程安全的。

  事实上,整个方法someMethod()是线程安全的。即使LocalObject实例作为参数传递给同一类或其他类中的其他方法,它的使用也是线程安全的。

  当然,唯一的例外是,如果以LocalObject作为参数调用的方法之一以允许从其他线程访问LocalObject实例的方式存储LocalObject实例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值