java并发编程(1)

一、基本概念
线程安全的定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类是线程安全的。

无状态的类一定是线程安全的。(无状态:类中不含有任何的域,同时不包含对其他类中域的作用)

竞态条件:由于不恰当的执行时序而出现不正确的结果。最常见的类型就是先检查后执行,而很多时候检查结果在我们执行完检查语句后,在开始执行正式的逻辑之间就失效了。

如果在无状态的类中添加一个状态(一个域),此时这个类就不再是无状态的了,而是状态取决于这个域,如果这个域由线程安全的类来管理,那么含有这个域的类也仍然是线程安全的。例如:在servlet这个无状态的类中添加一个count变量计数,如果对count自增时不加管理,那么该servlet就不再是安全的。而如果对count自增时,使用AtomicLong这种线程安全的类来操作,那么该servlet仍然是线程安全的。

而且还需要注意,两个都是原子操作的操作放到一起并不意味着线程安全,虽然两个操作单独来看都是线程安全的,但是这两个一起执行的时候,这两个操作不是线程安全的,完全可能执行完第一个原子操作还没开始执行第二个原子操作,线程就被中断让出cpu,此时另一个线程可能对数据进行更改。这样数据的一致性就被破坏了。所以,如果把多个原子操作合并为一个复合操作时,还需要额外的加锁机制。

所以,要保持状态的一致性,需要在单个原子操作中更新所有与状态有关的状态变量。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

二、java内置锁

java的每个对象都可以用作一个实现同步的锁,这些锁被称为内置锁或者监视器。内置锁的唯一获取途径就是进入由这个锁保护的同步代码块或方法(synchronized标记的方法或代码块)。而且java内置锁是一种可重入互斥锁,即每次最多只有一个线程持有该锁,同时如果获得锁的线程继续试图获得该锁,那么还是会请求锁成功,计数器加一。

而且java的内置锁并不能阻止其他线程访问该对象,只能阻止其他线程来获得同一把锁。这也就是为什么一个线程调用一个类的标记了synchronized方法时,其他线程还是可以调用该对象的普通方法。

三、对象共享

1.可见性

要保证对象可以被多个线程共享且正确的访问,就需要克服重排序以及可见性的问题,所以需要同步。如果不加控制,编译器和cpu都会对我们编写的代码进行重排序优化,可能会导致错误的结果。
就像单例模式中,为什么要给我们创建的私有单例对象加volatile关键字一样。

最低安全性:当线程在没有同步情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的,而不是随机值。

最低安全性适用于绝大多数变量,但不适用于非volatile类型的64位数值变量。这是因为java内存模型要求,变量的读取/写入操作必须是原子性的,但是对于非volatile类型的long和double类型来说,JVM允许将64位的读操作和写操作分解为两个32位的操作。所以,当读取一个非volatile类型的64位变量时,如果在并发情况下,有可能读到某个值得高32位或者低32位。

volatile关键字可以禁止针对该数据的指令重排序,同时不会将该数据缓存在寄存器或者其它地方。(具体可以参考MESI协议等缓存一致性协议),同时volatile关键字可以保证64位数据读写的原子性,因为volatile关键字会让64位数据的读写变为一次读写,而不会再分为两次32位的读写。

所以被volatile修饰的关键字,只有对其执行的指令是原子指令时,才能保证原子性(例如对基本类型数据赋值)。而如果是复杂操作,则volatile并不保证原子性。volatile只保证可见性和顺序性。保证64位数据读写的原子性,也只是将两次原子操作合并为一次原子操作,从而保证了64位数据读取的原子性而已。

volatile关键字应该在满足以下所有条件时,才使用:

  • 对变量的写入操作不依赖变量的当前值,或者可以确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

2.发布和逸出

逸出:某个不应该发布的对象被发布。(不加volatile关键字的双重判断单例模式,当多个线程同时使用这个单例对象时,由于指令重排,可能还没初始化对象,就将对象发布,从而使得别的线程访问到一个未被初始化的单例对象并使用,造成错误)

发布的情形:

  • 将对象的引用保存到一个静态的公有静态变量中。
  • 非私有方法返回的引用,这个引用指向的对象被发布
  • 发布一个对象时,该对象非私有的引用所指向的对象也被发布
  • 在内部类中,调用本类的函数,会导致通过隐式的this指针,将自己也发布出去
//例如
public class A{
public void doSomething(Event e){
  //do something
}

public A(EventSource source){
 source.registerListener(
   new EventListener(){
     public void onEvent(Event e){
        doSomething(e);//这里相当于A.this.doSomething(e),将A也发布了出去
     }
   }
 )
}
}
/*当且仅当构造函数返回时,对象才被认为处于可预测和一致的状态。
如果在对象的构造函数中通过this发布对象时,只是发布了一个尚未构造完成的对象
所以,不要在构造函数中使用this。
*/
//EG2:this逸出导致错误
public class ThisTest {
    private int a = 10;
    public ThisTest(int a) throws InterruptedException {
        new Thread(this::doSomething).start();
        Thread.sleep(3000);//睡3s,保证thread先运行
        this.a = a;
        System.out.println("我快要创建完毕了,现在a="+a);
    }

    private void doSomething(){
        System.out.println("do"+a);
    }

    public static void main(String[] args) throws InterruptedException {
        ThisTest thisTest = new ThisTest(100);
    }
}
//output:
//do10
//我快要创建完毕了,现在a=100

//此处也是this逸出,因为在构造器中无论以何种方式创建线程,本对象的this都会被线程感知到
//所以,可以在构造器中创建线程,但不应该立即执行,否则会造成错误。

3.不可变对象

不可变对象一定是线程安全的,因为其创建完成后,状态就不会再发生改变了。

不可变对象的条件:

  • 对象创建后其状态不可修改
  • 所有域都为final类型
  • 对象正确被创建(创建期间,this指针没有逸出)

不可变对象所有域一定都是final的,但所有都是final的对象不一定是不可变的,因为final域可以存放一个可变对象的引用,final只是限定了这个引用的指向不可再变化,但是指向的那个对象仍然是可变的。

final域能保证初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步。

4.安全发布

当一个正确的对象,未被安全发布出来,也会导致错误。

public class Holder{
  private int n;
  public Holder(int n){
    this.n = n;
  }
  public void assertSanity(){
     if(n != n){
        throw new AssertionError("This statement is false.");
     }
  }
}
/*这段代码如果在并发程序中运行,可能会抛出AssertionError。
这是因为,没有使用同步机制来保证Holder实例对其他线程可见。假设有两个进程A,B,A负责创建一个Holder对象,B负责调用assertSanity方法。由于new 一个对象并不是原子性的操作,而是很多条JVM指令,所以,可能会发生指令重排。A还没开始new,此时B抢占了cpu,取得holder对象就是一个空对象,n可能还未被分配空间,A再继续执行,此时创建出了holder对象,但还未初始化,B又抢占cpu,此时的n为jvm赋予的初值。
A又抢占cpu,完成初始化等工作,B此时再访问holder对象,取得的n值就是用户赋予的初值了。
所以,可能会产生AssertionError。
*/

一般安全发布对象的常用模式有以下几种:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象引用加volatile关键字
  • 将对象引用保存到某个正确构造对象的final域中
  • 将对象的引用保存到一个由锁保护的域中

这样的话,是可以保证可见性的,保证了可见性,那么别的线程访问变量的时候,会强制线程从主内存中重新读取数据。(如果不保证可见性的话,由于n!=n不是一条原子指令,第一次B可能获取的n为jvm赋予的初值,缓存到cpu的一个寄存器中,然后再次读取的一次n的值,将其缓存到cpu的另一个寄存器,然后将这两个寄存器中的数据进行比较,就会发现不一样。而保证了可见性后,当n发生变更,会告知cpu,其缓存的值已经过期,cpu会重新从主存中读取,再重新读取两次,再比较,就不会发生AssertError错误了。)

事实不可变对象:对象从技术上来看是可变的,但其状态在发布后就不会再改变。(比如String,或者成员变量时私有的,且不提供set方法,这种可以使用反射来进行更改,但是初衷是创建后其不可更改)
在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

这里有个疑问,若事实不可变对象的初衷是其创建后状态不改变,那为何不直接实现为不可变对象呢?

对于可变对象,安全发布只能保证“发布当时”的状态的可见性。所以,对于可变对象,不仅要在发布时需要使用同步(锁或者volatile等),每次访问对象时同样需要使用同步(锁)来确保后序修改操作的可见性。

所以,对于对象的发布需要取决于它的可变性:

  • 不可变对象可以通过任意机制来发布
  • 事实不可变对象必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值