java并发编程实战-First part

并发编程主要解决三个问题:
安全性问题-糟糕的事情一定不会发生
活跃性问题-某件正确的事情最终会发生
性能问题-某件正确的事情最终会发生,但是不够好

线程安全

编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是共享的和可变的状态

面对可变对象,使对其的访问线程安全的方式主要有:
不要在线程间共享该对象,
对象修改为不可变,
使用同步技术

一个无状态的对象一定是线程安全的,以下程序的对象没有共享的变量,所有的变量都是局部变量,所以它是线程安全的。

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包,他们实现线程安全的方式都是:运用对象组合,将线程不安全的容器包装到线程安全的容器当中,以使每次只能有一个线程访问该容器。

由于同步容器类存在种种问题,所以之后又有了并发容器,阻塞队列,生产者-消费者模式,阻塞方法中断方法,同步工具类和结果缓存等方式来保证容器的安全性,具体论述看下一章节

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值