Java并发编程——线程安全

活跃度失败:当一个活动进入某种它无法再继续执行的状态时,活跃度失败就发生了。(如死循环、死锁、饥饿、活锁等)

1、线程安全

一个对象是否应该是线程安全的取决于它是否会被多个线程访问。

无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。

在没有正确使用同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。3种方法修复它:不用跨线程共享变量;使状态变量为不可变的;在任何访问状态变量的时候使用同步。

设计线程安全的类时,优秀的面向对象技术——封装、不可变性以及明确的不变约束——会给你提供诸多的帮助。

线程安全的:当多个线程访问一个类时,如果不用考虑这些线程在运行环境下的调度和交替执行,并且不需要额外的同步以及在调用方式代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

无状态对象永远是线程安全的。无状态:不包含域也没有引用其他类的域。

2、原子性

假设有操作A和B,如果执行A的线程角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有操作,包括它自己,都满足前面描述的状态。

利用像AtomicLong这样已有的线程安全对象管理类的状态是非常实用的。相比于非线程安全对象,判断一个线程安全对象的可能状态和状态的转换要容易得多。这简化了维护和验证线程安全性的工作。

3、锁

3.1内部锁:

强制原子性的内置锁机制:synchronized块。一个synchronized块有两部分:锁对象的引用和这个锁保护的代码块。

内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。——导致糟糕的,无法接受的响应性

   synchronized(lock对象){

       ……//同步代码块

   }

3.2可重进入:

内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。重进入是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并将请求计数设置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数值将递减。直到计数器达到0时,锁被释放。

   互斥锁针对于每调用,可重进入针对于每线程。

public class Widget{

      public synchronized void doSomething(){

         …

      }

   }

  public class LoggingWidget extends Widget{

      public synchronized void doSomething(){

         System.out.println(toString()+":calling doSomething");

         super.doSomething();

      }

   } 

4、用锁来保护状态

操作共享状态的复合操作必须是原子的,以避免竞争条件,比如递增中计数器(读-改-写)或者惰性初始化(检查再运行)。

对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁。

对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。

5、活跃度与性能

应尽量从synchronized块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。

通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早的为了性能而牺牲简单性(这是对安全性潜在的妥协)。

有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。 



留言