线程安全和对象共享

来源:互联网 发布:2017淘宝店还能赚钱吗 编辑:程序博客网 时间:2024/05/16 02:49

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

2.确保线程安全的方法
(1)无状态对象:不包含任何域,也不包含任何对其他类中的域的引用。计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。

public class StatelessFactorizer implements Servlets{    public void service(ServletRequest req,ServletResponse resp){        BigInteger i = extractFromRequest(req);        BigInteger[] factors = factor(i);        encodeIntoResponse(resp,factors);    } }

(2)不可变对象:不可变对象需要满足以下的条件:对象创建后其状态就不可以修改;对象的所有域都是 final 类型;对象是正确创建的(在对象创建期间,没有this引用逸出)。

/** * 对数值及其因数分解结果进行缓存的不变容器类 * @author shier * */public class OneValueCache {    private final BigInteger lastNumber;    private final BigInteger[] lastFactors;    public OneValueCache(BigInteger i,BigInteger[] factors) {        lastNumber = i;        lastFactors = Arrays.copyOf(factors, factors.length);    }    public BigInteger[] getFactors(BigInteger i){        if(lastNumber == null && !lastNumber.equals(i)){            return null;        }else{            return Arrays.copyOf(lastFactors, lastFactors.length);        }    }}

(3)原子变量
原子操作:指对于访问同一个状态的所有操作(包括该操作本身)来说,这个方式是以一个原子方式执行的操作。

原子变量:自身是线程安全的,但是如果一个不变约束涉及多个变量时,变量间不是彼此独立的,无论这些变量是否是原子变量都不能确保线程安全,而需要在同一个原子操作中更新这些相互关联的状态变量才能确保线程安全。JDK的Java.util.concurrent.atomic包中包括了原子变量类,这些类用来实现数字和对象引用的原子状态转换。
/** * 使用AtomicLong类型的变量来统计已处理请求的数量 * 当在无状态的类中添加一个状态时,如果状态完全有线程安全的对象来管理,那么这个类仍然是线程安全的 */public class CountingFactorizer {    private final AtomicLong count = new AtomicLong(0);    public long getCount(){        return count.get();    }    public void service(ServletRequest req,ServletResponse resp){        BigInteger i = extractFromRequest(req);        BigInteger[] factors = factor(i);        count.incrementAndGet();        encodeIntoResponse(resp,factors);    }}

(4)正确使用同步加锁机制:例如synchronized内置锁、ReentrantLock显示锁以及validate轻量级锁等。

二、竞态条件:指多个线程或者进程在读写一个共享数据时结果依赖于它们指令执行的相对时序,即要想得到正确的结果,要依赖于幸运的时序。最常见的竞争条件是“检查再运行”,使用一个潜在的过期值作为决定下一步操作的依据。

/** * 延迟初始化加载实例 * @author shier * */public class LazyInitRace {    private Object inatance = null;    public Object getInstance(){        if(inatance == null){            inatance = new Object();        }        return inatance;    }}

三、加锁机制
1.内置锁
Java提供了强制原子性的内置锁机制:synchronized块。
一个synchronized块有两部分:锁对象引用,以及该锁保护的代码块。
synchronized方法的锁是该方法所在的对象本身,静态的synchronized方法的锁是从Class对象上获取的锁。内部锁的特性如下:
(1).自动获得和释放:
每个java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁或监视器锁,执行线程进入synchronized块之前自动获得锁,而无论是正常退出还是抛出异常,线程都会自动释放锁。因此获得内部锁的唯一途径是进入这个内部锁保护的同步块或方法。
(2).互斥性:
内部锁在java中扮演了互斥锁的角色,即至多只有一个线程可以拥有锁,没有获取到锁的线程只能等待或阻塞直到锁被释放,因此同步块可以线程安全地原子执行。
(3).可重入性:
可重入是指对于同一个线程,它可以重新获得已有它占用的锁。
可重入性意味着锁的请求是基于”每线程”而不是基于”每调用”,它是通过为锁关联一个请求计数器和一个占有它的线程来实现。
可重入性方便了锁行为的封装,简化了面向对象并发代码的开发,可以防止类继承引起的死锁,例子如下:

public class Widget {      public synchronized void doSomething(){          ......      }  }  public class LoggingWidget extends Widget {      public synchronized void doSomething(){          System.out.println(toString() + “: calling doSomething”);          super.doSomething();      }  }  

子类LoggingWidget覆盖了父类Widget中synchronized类型的doSomething方法,并调用了父类的中的同步方法,因此子类LoggingWidget和父类Widget在调用doSomething方法之前都会先获取Widget的锁,若内部锁没有可重入性,则super.doSomething的调用就会因为无法获得锁而被死锁。

四、内存可见性
当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
1.指令重排序:在没有同步的情况下,编译期、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要相对内存操作的执行顺序进行判断,几乎无法得到正确结论。

2.保证多线程的内存可见性的方式
(1)锁:锁不仅仅是关于同步与互斥的的,也是关于内存可见性的,为了保证所有线程都能看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步,锁确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

(2)validate变量:volatile是一种弱形式的同步, volatile确保对一个变量的更新以可预见的方式告知其他线程。当一个域被声明为volatile类型后,编译器与运行时会监视这个变量:它是共享的,而且对它的操作不会与其他内存操作一起被重排序,volatile变量不会缓存在寄存器或者缓存在其他处理器隐藏的地方,因此读一个volatile类型的变量时,总是返回由某一线程所写入的最新值。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势,但是请注意volatile只保证可见性,不保证原子性。
只有满足如下条件才能使用volatile 变量:
A. 对变量的写操作不依赖于当前值。
B. 该变量没有包含在具有其他变量的不变式中。

五、线程封闭
访问共享的、可变的数据要求使用同步。线程封闭是一种将数据仅在单线程中访问而不共享,不需要任何同步的最简单的实现线程安全的方式。
当对象(无论本身是否线程安全)封闭在一个线程中,会自动成为线程安全,线程封闭的常用做法有:
(1).栈限制:
栈限制是线程封闭的一种特例,只能通过本地变量才可以触及对象,本地变量使对象限制在执行线程中,存在于执行线程栈,其他线程无法访问这个栈,从而确保线程安全。栈限制的例子如下:

/** *栈限制例子中animals不能逸出,否则就会破坏限制性 **/public int loadTheArk(Collection<Animal> candidates){      SortedSet<Animal> animals;      int numPairs = 0;      Animal candidate = null;      //animals被限制在本地方法栈中      animals = new TreeSet<Animal>(new SpeciesGenderComparator());      animals.addAll(candidates);      for(Animal a : animals){          if(candidate == null || !candidate.isPotentialMate(a)){              candidate = a;          }else{              ark.load(new AnimalPair(candidate, a));              ++numPairs;              candidate = null;          }      }      return numPairs;  }  

(2)ThreadLocal:
ThreadLocal线程本地变量是一种规范化的维护线程限制的方式,它允许将每个线程与持有数值的对象关联在一起,为每个使用它的线程维护一份单独的拷贝。ThreadLocal提供了set和get访问器,get总是返回由当前线程通过set设置的最新值。
ThreadLocal线程本地变量通过用于防止在基于可变的单例(singleton)或全局变量的设计中,出现不正确的共享。ThreadLocal线程本地变量的例子如下:

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){      public Connection initialValue(){          return DriverManager.getConnection(DB_URL);      }  };  public static Connection getConnection(){      return connectionHolder.get();  }  

六、发布和逸出
1.发布:发布一个对象是指使该对象能够被当前范围之外的代码所使用,例如将一个引用存储到其他代码可以访问的地方;在一个非私有的方法中返回该引用;将该对象传递到其他类的方法中等。安全发布的几种模式:在静态初始化函数中初始化一个对象引用;将对象的引用保存到validate类型的域或者AtomicReference对象中;将对象引用保存到某个正确构造对象的final类型域中;将对象的引用保存到一个有锁保护的域中。
最常见的发布方式是将对象的引用存储到公共静态域中,例如:

public static Set<Secrte> knownSecrets;  public void initialize(){      knowSecrets = new HashSet<Secret>();  }  

2.逸出
逸出是指一个对象在尚未准备好时就将它发布。
对象逸出会导致对象的内部状态被暴露,可能危及到封装性,使程序难以维持稳定;若发布尚未构造完成的对象,可能危及线程安全问题。
最常见的逸出是this引用在构造时逸出,导致this引用逸出的常见错误有:
A.在构造函数中启动线程:
当对象在构造函数中显式还是隐式创建线程时,this引用几乎总是被新线程共享,于是新的线程在所属对象完成构造之前就能看见它。
避免构造函数中启动线程引起的this引用逸出的方法是不要在构造函数中启动新线程,取而代之的是在其他初始化或启动方法中启动对象拥有的线程。
B.在构造方法中调用可覆盖的实例方法:
在构造方法中调用那些既不是private也不是final的可被子类覆盖的实例方法时,同样导致this引用逸出。
避免此类错误的方法时千万不要在父类构造方法中调用被子类覆盖的方法。
C.在构造方法中创建内部类:
在构造方法中创建内部类实例时,内部类的实例包含了对封装实例的隐含引用,可能导致隐式this逸出。例子如下:

public class ThisEscape {      public ThisEscape(EventSource source) {          source.registerListener(new EventListener() {              public void onEvent(Event e) {                  doSomething(e);              }          });      }  }  

上述例子中的this逸出可以使用工厂方法来避免,例子如下:

public class SafeListener {      private final EventListener listener;      public SafeListener(){          listener = new EventListener(){              public void onEvent(Event e){                  doSomething(e);              }          );      }      public static SafeListener newInstance(EventSource source) {          SafeListener safe = new SafeListener();          source.registerListener(safe.listener);          return safe;      }  }  

七、向已有线程安全类添加新功能
如果JDK或者第三方类库提供的线程安全类只能满足我们大部分的要求,即不能完全满足要求时就需要对其进行添加新操作,这个看似简单的问题往往会引起线程安全问题。
以向一个线程安全的List添加一个原子的缺少即加入操作为例,有如下方法:
(1).修改原始类:
添加一个新原子操作最安全的方式就是修改原始类,在原始类中添加新操作,但是软件设计的开放闭合原则以及有可能无法得到源码(或无法自由修改源码)等问题决定修改原始类是最不可能的。即便可以修改原始类,也要保证理解原有的同步策略,维持原有的设计。

(2).扩展原始类:
如果原始类在设计上是可扩展的(没有声名为final,即允许继承),则扩展原始类并添加新方法,例子如下:

public class BetterVector<E> extends Vector<E> {      public synchronized boolean putIfAbsent(E x){          boolean absent = !contains(x);          if(absent){              add(x);          }          return absent;      }  }  

扩展原始的线程安全类要特别小心,虽然做法非常直观,但是一定要明白原始的线程安全类的同步策略,新扩展的类要使用和原始类相同的锁来控制对基本类状态的并发访问,这种访问有很大的局限性,如果原始类使用的是内部私有锁同步策略或者没有告知使用者同步策略,则该方式是不能支持的。
Vector是使用内部锁来控制并发访问的,因此上面代码中BetterVector也使用内部锁控制并发访问是可以维持Vector同步策略。

(3).客户端加锁:
对于一个由Collections.synchronizedList封装的ArrayList,客户端不知道同步封装工厂方法返回的List对象的同步策略的时候,前面所介绍的扩展原始类方案就无法支持,这时我们就需要将新增功能添加在客户端,并在客户端进行加锁同步。
客户端加锁要非常小心,不注意就会发生错误,下面例子演示一个常见的错误:

public class ListHelper<E> {      public List<E> list = Collections.synchronizedList(new ArrayList<E>());      public synchronized boolean putIfAbsent(E x) {          poolean absent = !list.contains(x);          if (absent) {              list.add(x);          }          return absent;      }  }  

上述代码错误在于使用了与线程同步List不同的锁,上面代码的putIfAbsent方式使用的是ListHelper对象的内部锁,而线程同步List的其他原子操作肯定用的不是ListHelper对象内部锁,因此putIfAbsent对于List的其他操作而言并不是原子化的,上述代码是很多人经常不小心犯的错误。
避免上述的错误的办法是让客户端的putIfAbsent方法所使用的锁与List的其他操作所使用的锁是同一个锁,正确的代码如下:

public class ListHelper<E> {      public List<E> list = Collections.synchronizedList(new ArrayList<E>());      public boolean putIfAbsent(E x) {          synchronized (list) {              poolean absent = !list.contains(x);              if (absent) {                  list.add(x);              }              return absent;          }      }  }  

(4).组合:
面向对象有两种常用的扩展方式:继承(is-a)和组合(has-a),设计原则也经常推荐优先使用组合,除非情况非常合适,否则尽量少使用继承。
组合对于向现有线程安全类添加新功能时同样适合,通过添加一层额外的锁层,组合对象将操作委托给底层List实例,无论底层List实例是否实现线程安全,组合对象的putIfAbsent方法都可以保证操作的原子性,例子如下:

public class ImprovedList<T> implements List<T>{      private final List<T> list;      public ImprovedList<T>(List<T> list){          this.list = list;      }      public synchronized boolean putIfAbsent(T x){          boolean absent= !list.contains(x);          if(absent){              list.add(x);          }          return absent;      }      public synchronized void clear(){          list.clear();      }      ......  }  
0 0
原创粉丝点击