线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器(Program Counter)、栈以及局部变量等。
在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。
Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显式锁(Explicit Lock)以及原子变量。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
不在线程之间共享改状态变量。
将状态变量修改为不可变的变量。
在访问状态变量时使用同步。
线程安全性定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
无状态对象一定是线程安全的。
大多数竞态条件的本质:基于一种可能失效的观察结果来做出判断或者是执行某个计算。这种类型的竞态条件成为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致了各种问题(未预期的异常、数据被覆盖、文件被破坏等)。
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
在实际情况中,应尽可能地使用现有的线程安全对象(例如AtomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更加容易维护和验证线程安全性。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将会记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将会递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁。
状态的理解,我认为是类的成员变量。无状态对象就是成员变量不能储存数据,或者是可以储存数据但是这个数据不可变。无状态对象是线程安全的。如果方法中存在成员变量,就需要对这个成员变量进行相关的线程安全的操作。
不要一味地在方法前加synchronized,这可以保证线程安全,但是方法的并发功能会减弱,导致本来可以支持并发的方法变成堵塞,导致程序处理速度的变慢。
synchronized包围的代码要尽可能的短,但是要保证有影响的所有成员变量在一起。没有关系的成员变量可以用多个synchronized包围。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重新排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
volatile变量通常用做某个操作完成、发生中断或者是状态的标志。volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:
对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
该变量不会与其他状态变量一起纳入不变形条件中。
访问该变量时不需要加锁。
“发布(Publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。
当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。
不要在构造过程中使this引出逸出。
如果想在构造函数中注册一个事件监听器或者启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程。
栈封闭是线程封闭的一种特例,在栈封闭中,只有通过局部变量才能访问对象。
维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能够使线程中的某个值与保存值的对象关联起来。
ThreadLocal对象通常用于防止对可变的单实例对象(Singleton)或全局变量进行共享。
当满足以下条件时,对象才是不可变的:
对象创建以后其状态就不能修改。
对象的所有域都是final类型。
对象是正确创建的(在对象的创建期间,this引用没有逸出)。
不可变对象一定是线程安全的。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
在静态初始化函数中初始化一个对象引用。
将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。
在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
对象的发布需求取决于它的可变性:
不可变对象可以通过任意机制来发布。
事实不可变对象必须通过安全方式来发布。
可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
发布和逸出的理解:就是说一个类中的成员变量或者对象可以被其他的类所引用使用就是发布,如用static修饰的静态变量或者是当前调用方法的对象。逸出是指该成员变量或对象在本来不应该被多线程引用的情况下暴露出去被引用,导致其值可能被错误修改的问题。一句话,不要随便扩大一个类以及内部使用成员变量和方法的作用域。这也是封装应该考虑的问题。
this逸出:即在构造方法的内部类中启动另一个线程引用了这个对象,但是这时这个对象还没有构造完成,可能会导致出乎意料的错误。解决方法是创建一个工厂方法,然后将构造器设置成私有构造器。
final修改的成员变量需要在构造器在构造器中初始化,否则对象实例化后这个成员变量不能赋值。final修饰的成员变量是引用对象时,这个对象的地址不能修改,但是这个对象的值是可以修改的。
安全发布一个对象的四种方式的理解,如A类中有B类的引用:
A的静态初始化方法,如public static A a = new A(b);这样的静态工厂类中,引用B的时候初始化B。
A类中的B成员变量用volatile b或者是AtomicReferance b这样修饰。
A类中的B成员变量用final B b这样修饰。
A类中的方法使用到B的时候用synchronized(lock){B…}包围。
事实不可变对象很简单的理解就是技术上是可变的,但是在业务逻辑处理中是不会去修改的对象。
相关推荐:
以上是JAVA并发编程总结:线程安全性、对象的共享的详细内容。更多信息请关注PHP中文网其他相关文章!