# JAVA 并发编程—基础


线程安全性

*通过同步避免多个线程在同一时刻访问相同的数据*

1、如果当多个线程同时访问一个可变的状态变量时没有使用合适的同步,那么程序会出现错误。有三种方式可以修复这个问题:
(1)、不在多个线程之间共享该变量
(2)、将状态变量改为不可变的
(3)、在访问状态变量时使用同步

2、什么是线程安全性?
在线程安全性的定义中,最主要的概念就是正确性。正确性意味着某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件来约束对象的状态,以及定义各种后验条件来描述对象操作的结果。
确定正确性的定义后,就可以定义线程安全性:当多个线程同时访问某个类时,这个类始终能表现正确的行为,那么就称这个类是线性安全的。

3、原子性、竞态条件、复合操作
在并发编程中,如果对一个可变变量进行非原子的复合操作,会出现执行时序不正确的情况,发生竞态条件,导致结果不正确。最常见的就是“先检查后执行”操作。
在实际情况中,应该尽可能使用已有的线程安全对象,它们可能会对一些常用的复合操作进行了封装。

4、内置锁、重入
java提供了一种内置的锁机制来支持原子性:同步代码块。包括两部分:一个作为锁的对象引用,一个作为这个锁保护的代码块。以关键字synchronized修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
每个Java对象都可以用作一个实现同步的锁,称为内置锁监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。内置锁相当于一个互斥体,意味着最多只能有一个线程持有这种锁,即每次只能有一个线程执行内置锁保护的代码块。
如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作粒度是“线程”,而不是“调用”。

5、用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时(不仅仅是写入共享变量)都要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
一种常见的枷锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。这种模式并无特殊之处,编译器或运行时都不会强制实施这种模式。
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
当类的不变性条件涉及多个状态变量时,那么还有另一个需求:在不变性条件中的每个变量都必须由同一个锁来保护。
为什么不在每个方法声明时都使用关键字synchronized?虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还需要额外的枷锁机制。此外,滥用同步方法会导致活跃性问题性能问题

6、活跃性与性能
synchronized意味着每次只有一个线程可以执行,当遇到执行时间较长的操作时,其它线程必须等待,会导致性能问题。幸运的是,可以通过缩小同步代码块的作用范围,在确保并发性的同时又维护线程安全性。应该尽量将不影响共享状态且执行时间较长的操作移出代码块。

对象的共享

*如何共享和发布对象,使它们能够安全地由多个线程同时访问。* 同步还有另一个重要的方面:**内存可见性**。我们不仅希望防止某个线程正在使用对象状态而另一个线程同时在修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

1、可见性、volatile变量
在没有同步的情况下,编译器、处理器以及运行时等都有可能对操作的执行顺序进行一些重排序。
只要有数据在多个线程之间共享,就使用正确的同步。
非volatile类型的64位数值变量可能会读取到某个值的高32位和另一个值的低32位。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:

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

2、发布与逸出
“发布”一个对象的意思是指,使该对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者将引用传递到其他类的方法中。当某个不该发布的对象被发布时,这种情况就被称为逸出
当发布一个对象时,可能会间接发布其他对象,比如List,Map等。
不要在构造过程中使this引用逸出。

3、线程封闭
一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。线程封闭是程序设计中的一个考虑因素,必须在程序中实现。
Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。这种方式是非常脆弱的,应该尽量少使用它。
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。在维持对象引用的栈封闭时,程序员需要多做一些工作以确保被引用的对象不会移除。
ThreadLocal类是一种更规范的方法,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。要避免滥用ThreadLocal。

4、不变性
满足同步需求的另一种方法是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改
  • 对象的所有域必须是final类型(从技术上看,不可变对象并不需要将其所有的域都声明为final,要对类的良性数据竞争情况做精确分析,因此需要深入理解Java内存模型)
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)

5、安全发布、事实不可变对象
在未被正确发布的对象中存在两个问题。首先,除了发布对象的线程外,其它线程可以看到的Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。然而,更糟糕的是,线程看到Holder引用的值是最新的,但Holder状态的值却是失效的。
由于不可变对象是一种非常重要的对象,因此Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。
可变对象必须通过安全的方式来发布,这通常意味这在发布和使用该对象的线程时都必须使用同步。要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方法来安全地发布:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型的域或者AtomicReference容器中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

如果对象从技术上来看是可变的,但其状态在发布后就不会再改变,那么把这种对象称为“事实不可变对象”。在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变变量。
可变对象不仅在发布对象时需要使用同步,而且在每次对向访问时同样需要使用同步来确保后续修改的可见性。
对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布
  • 事实不可变对象必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者是由某个锁保护起来

在并发线程中安全地共享对象,可以使用一些使用策略,包括:线程封闭只读共享线程安全共享保护对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值