dcbeyond 2019-06-29
当多个线程去访问某个类时,如果类会表现出我们预期出现的行为,那么可以称这个类是线程安全的。
操作并非原子。多个线程执行某段代码,如果这段代码产生的结果受不同线程之间的执行时序影响,而产生非预期的结果,即发生了竞态条件,就会出现线程不安全;
常见场景:
count++
。它本身包含三个操作,读取、修改、写入,多线程时,由于线程执行的时序不同,有可能导致两个线程执行后count只加了1,而原有的目标确实希望每次执行都加1;instance == null
成立,然后新建了两个对象,而原有目标是希望这个对象永远只有一个;public MyObj getInstance(){ if (instance == null){ instance = new MyObj(); } return instance }
解决方式是:当前线程在操作这段代码时,其它线程不能对进行操作
常见方案:
当线程要去获取它自己已经持有的锁是会成功的,这样的锁是可重入的,synchronized是可重入的
class Paxi { public synchronized void sayHello(){ System.out.println("hello"); } } class MyClass extends Paxi{ public synchronized void dosomething(){ System.out.println("do thing .."); super.sayHello(); System.out.println("over"); } }
它的输出为
do thing .. hello over
修改不可见。读线程无法感知到其它线程写入的值
常见场景:
常见方案:
注意:Volatile并不能保证操作的原子性,比如count++
操作同样有风险,它仅保证读取时返回最新的值。使用的好处在于访问Volatile变量并不会执行加锁操作,也就不会阻塞线程。
线程封闭。即仅在单线程内访问数据,线程封闭技术有以下几种:
读取-修改-写入
只读共享。即使用不可变的对象。
使用final去修饰字段,这样这个字段的“值”是不可改变的
注意final如果修饰的是一个对象引用,比如set,它本身包含的值是可变的创建一个不可变的类,来包含多个可变的数据。
class OneValue{ //创建不可变对象,创建之后无法修改,事实上这里也没有提供修改的方法 private final BigInteger last; private final BigInteger[] lastfactor; public OneValue(BigInteger i,BigInteger[] lastfactor){ this.last=i; this.lastfactor=Arrays.copy(lastfactor,lastfactor.length); } public BigInteger[] getF(BigInteger i){ if(last==null || !last.equals(i)){ return null; }else{ return Arrays.copy(lastfactor,lastfactor.length) } } } class MyService { //volatile使得cache一经更改,就能被所有线程感知到 private volatile OneValue cache=new OneValue(null,null); public void handle(BigInteger i){ BigInteger[] lastfactor=cache.getF(i); if(lastfactor==null){ lastfactor=factor(i); //每次都封装最新的值 cache=new OneValue(i,lastfactor) } nextHandle(lastfactor) } }
实例封闭。将一个对象封装到另一个对象中,这样能够访问被封装对象的所有代码路径都是已知的,通过合适的加锁策略可以确保被封装对象的访问是线程安全的。
java中的Collections.synchronizedList使用的原理就是这样。部分代码为
public static <T> List<T> synchronizedList(List<T> list) { return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list)); }
SynchronizedList的实现,注意此处用到的mutex是内置锁
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { private static final long serialVersionUID = -7754090372962971524L; final List<E> list; public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } }
mutex的实现
static class SynchronizedCollection<E> implements Collection<E>, >Serializable { private static final long serialVersionUID = 3053995032091335093L; final Collection<E> c; // Backing Collection final Object mutex; // Object on which to synchronize SynchronizedCollection(Collection<E> c) { if (c==null) throw new NullPointerException(); this.c = c; mutex = this; // mutex实际上就是对象本身 }
java的监视器模式,将对象所有可变状态都封装起来,并由对象自己的内置锁来保护,即是一种实例封闭。比如HashTable就是运用的监视器模式。它的get操作就是用的synchronized,内置锁,来实现的线程安全
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null; }
内置锁
每个对象都有内置锁。内置锁也称为监视器锁。或者可以简称为监视器
线程执行一个对象的用synchronized修饰的方法时,会自动的获取这个对象的内置锁,方法返回时自动释放内置锁,执行过程中就算抛出异常也会自动释放。
以下两种写法等效:
synchronized void myMethdo(){ //do something } void myMethdo(){ synchronized(this){ //do somthding } }
> [官方文档](https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html)
私有锁
public class PrivateLock{ private Object mylock = new Object(); //私有锁 void myMethod(){ synchronized(mylock){ //do something } } }
它也可以用来保护对象,相对内置锁,优势在于私有锁可以有多个,同时可以让客户端代码显示的获取私有锁
视情况而定。
只有单个组件,且它是线程安全的。
public class DVT{ private final ConcurrentMap<String,Point> locations; private final Map<String,Point> unmodifiableMap; public DVT(Map<String,Point> points){ locations=new ConcurrentHashMap<String,Point>(points); unmodifiableMap=Collections.unmodifiableMap(locations); } public Map<String,Point> getLocations(){ return unmodifiableMap; } public Point getLocation(String id){ return locations.get(id); } public void setLocation(String id,int x,int y){ if(locations.replace(id,new Point(x,y))==null){ throw new IllegalArgumentException("invalid "+id); } } } public class Point{ public final int x,y; public Point(int x,int y){ this.x=x; this.y=y; } }
线程安全性分析
综上,DVT的安全交给了‘locations’,它本身是线程安全的,DVT本身虽没有任何显示的同步,也是线程安全。这种情况下,就是DVT的线程安全实际是委托给了‘locations’,整个DVT表现出了线程安全。
只要多个状态变量之间彼此独立,组合的类并不会在其包含的多个状态变量上增加不变性。依赖的增加则无法保证线程安全
public class NumberRange{ private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i){ //先检查后执行,存在隐患 if (i>upper.get(i)){ throw new IllegalArgumentException('can not ..'); } lower.set(i); } public void setUpper(int i){ //先检查后执行,存在隐患 if(i<lower.get(i)){ throw new IllegalArgumentException('can not ..'); } upper.set(i); } }
setLower和setUpper都是‘先检查后执行’的操作,但是没有足够的加锁机制保证操作的原子性。假设原始范围是(0,10),一个线程调用 setLower(5),一个设置setUpper(4)错误的执行时序将可能导致结果为(5,4)
组合。将类放入一个辅助类中,通过辅助类的操作代码。
比如扩展 Collections.synchronizedList。期间需要注意锁的机制,错误方式为
public class ListHelper<E>{ public List<E> list=Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } }
这里的putIfAbsent并不能带来线程安全,原因是list的内置锁并不是ListHelper,也就是putIfAbsent相对list的其它方法并不是原子的。Collections.synchronizedList是锁在list本身的,正确方式为
public boolean putIfAbsent(E x){ synchronized(list){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } }另外可以不管要操作的类是否是线程安全,对类统一添加一层额外的锁。 实现参考Collections.synchronizedList方法