并发编程主要解决三个问题:
安全性问题-糟糕的事情一定不会发生
活跃性问题-某件正确的事情最终会发生
性能问题-某件正确的事情最终会发生,但是不够好
线程安全
编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是共享的和可变的状态
面对可变对象,使对其的访问线程安全的方式主要有:
不要在线程间共享该对象,
对象修改为不可变,
使用同步技术
一个无状态的对象一定是线程安全的,以下程序的对象没有共享的变量,所有的变量都是局部变量,所以它是线程安全的。
public class StatelessObject implements Servlet{
public void service(ServletRequest req,ServletResponse res){
BigDecimal i=extractFromReq(req);
BigDecimal[] factor=Factory(i);
encodeIntoResponse(res,factor);
}
}
有单独对象(变量)的对象,要保证操作的原子性
如果采用注释的语句,并不能保证count变量的原子性。因为count++,从机器指令的层面来看,要经过三个步骤,分别是“读取-修改-写入”。要保持一个变量的原子性,最便捷的办法是使用线程安全的类,例如下面的AtomicXXX类
public class StatelessObject implements Servlet{
//private long count=0;
private AtomicLong long count=new AtomicLong(0);
public void service(ServletRequest req,ServletResponse res){
BigDecimal i=extractFromReq(req);
BigDecimal[] factor=Factory(i);
//count++;
count.incresmentAndGet();
encodeIntoResponse(res,factor);
}
}
有多个变量,且变量之间存在相互约束时,要保证类的状态的原子性,即要保持状态的一致性,要在单个原子操作中同时更新所有的变量
下面程序中,需要保证了单个变量操作的原子性,但是并不能保证整个对象是线程安全的,因为变量之间会相互影响。
public class StatelessObject implements Servlet{
private AtomicLong long count=new AtomicLong(0);
private AtomicLong long state=new AtomicLong(100);
public void service(ServletRequest req,ServletResponse res){
BigDecimal i=extractFromReq(req);
BigDecimal[] factor=Factory(i);
state.decreaseAndGet();
count.incresmentAndGet();
encodeIntoResponse(res,factor);
}
}
要保持状态的一致性,可以使用同步方法-内置锁-synchronize
获取锁操作的粒度是线程,而不是调用。故而一个对象持有锁,它是可以无限重入同步区的。
这种设定保证了子类实现父类时,不会因为实现父类的synchronize块方法而发生竞态条件,如下所示
public class wildge{
public synchronize void dosomething(){
}
}
public class sonClass extends wildgs{
public synchronize void dosomething(){
dosomething;
super.dosomething();
}
}
传统的vertor和hashtable就是使用synchronize对整个方法进行同步,但是这种方式会导致的运行串行化违背了并发的本质。最好的方法,是在一个细粒度上对代码块加锁。
但是,与之相反的,又不能过于在一个较细粒度上加锁,因为上锁和释放锁也要占用性能。
Hashtable API:
synchronized void clear()
synchronized Object clone()
boolean contains(Object value)
synchronized boolean containsKey(Object key)
synchronized boolean containsValue(Object value)
synchronized Enumeration elements()
synchronized boolean equals(Object object)
synchronized V get(Object key)
synchronized int hashCode()
synchronized boolean isEmpty()
synchronized Set keySet()
synchronized Enumeration keys()
synchronized V put(K key, V value)
synchronized void putAll(Map< extends K, ? extends V> map)
synchronized V remove(Object key)
synchronized int size()
synchronized String toString()
synchronized Collection values()
对象共享
可见性是指:被同步操作修改的对象,其他所有的线程都是可以看到它的状态变化的
long和double是64位的数据,如果不对他们施加线程安全保证可见性,可能在并发访问中出现,高32位和低32数据不一致的情况
加锁可以保证原子性和内存可见性,volatile只能保证内存可见性。要理解前这一句话,就要理解清楚volatile的实质,volatile修饰符保证了被修饰变量的操作不会参与重排序(jvm为了优化程序性能,会对机器指令进行重排序),也不会将值缓存在寄存器或者其他处理器看不见的地方。这两点保证了volatile的实时更新性。但是并不足以保证其原子性,例如被volatile修饰的count变量,在执行count++操作时,如果有多个线程,仍然是线程不安全的.
发布:使对象能够在当前作用域之外被访问到
逸出:不该发布的对象被发布出去了
线程封闭将同步对象用单线程来访问,也就是·上面提到的不要在线程间共享该对象。例如Swing和JDBC中的connection分发,前者是固定由事件分发器线程来访问,后者是也是由单个线程来处理的。线程封闭主要包括两种技术:
栈封闭 将对象封闭在局部变量中
ThreadLocal技术实现了一个线程内的值与一个保存值相关联,每次get读取的值都是最新的set设置的值。通常用于全局变量的多线程化-
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换安全性”的方式,而ThreadLocal采用了“以空间换可见性”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
ThreadLocal具体描述
不变性:不可变对象一定是线程安全的。不可变的要求包括:所有的域都是final修饰,正确创建且创建后就不能修改
(除非了为了更高的可见性,否则应该将所有的域都声明为private一样,除非为了可变性,否则应该将所有的域都声明为final)
对象组合
实例封闭:将一个原本线程不安全的类封闭在另外一个对象中,然后用另外一个对象的内置锁来保证这个对象的安全性。在java平台很多类容器并不是线程安全的,但是通过使用装设者模式,运用对象组合就可以将一个线程不安全的类变成线程安全的类,比如:list->Collections.synchronizedList() 。
举个栗子:
@ThreadSafe
public class PersonSet{
private final Set<Person> mySet=new HashSet<>();
public synchronize void addPerson(Person p){
mySet.add(p);
}
public synchronize boolean isContainPerson(person p){
return mySet.contain(p);
}
}
再看一下Collections.synchronizedList()的源码:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
...
}
它对原list做了一层包装,将原list封闭在新的synchronizeList对象内部,并通过对mutex上锁来保证包装类的线程安全。
实例封闭也要防止逸出
java监视器模式就是使用了实例封闭,唯一的不同在于他的封装类没有使用内置锁,而是使用了一个私有锁。
安全性委托
当一个包装类的组合对象有多个时,多个对象的组合是否线程安全要视情况而定
通常情况下,多个对象独立时,可以将安全性委托给concurrentHashMap/CopyOnWriteList/内置锁等,但是当多个组合对象间有相互的约束关系,最终的组合对象的安全性就会失效(比如:两个对象,person ,dog,当 dog.num>10时,person.money<100),也是就说如果某个类含有复合操作,那么除非对整个对象的状态保持一致性,否则无法保证安全性
基础构建模块
同步容器类早期的包括vector 和 Hashtable,以及jdk1.2后添加的collection.synchronizeXXX包,他们实现线程安全的方式都是:运用对象组合,将线程不安全的容器包装到线程安全的容器当中,以使每次只能有一个线程访问该容器。
由于同步容器类存在种种问题,所以之后又有了并发容器,阻塞队列,生产者-消费者模式,阻塞方法中断方法,同步工具类和结果缓存等方式来保证容器的安全性,具体论述看下一章节