Java并发编程(3)-构造线程安全类的模式

版权声明: https://blog.csdn.net/pbrlovejava/article/details/83280454


更多关于Java并发编程的文章请点击这里:Java并发编程实践(0)-目录页


到目前为止,前两篇文章已经介绍了线程安全与同步的基础知识。但是我们并不希望为了获得线程安全而去分析每次内存访问;而希望线程安全的组件能够以安全的方式组合成更大的组件或者程序。这一篇将介绍一些构造线程安全类的通用模式,这些模式让类更容易成为线程安全类。
本篇总结自《Java并发编程实践》第四章 组合对象 章节的内容 ,详情可以查阅该书。

一、实例限制模式

即使一个对象不是线程安全的,仍然有许多技术可以使它成为安全的多线程程序。比如,你可以确保它只被单一的线程所访问(上篇讲的线程限制技术),也可以确保所有的访问都正确地被锁保护。

1.1、 限制变量确保线程安全

将数据封装在对象内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时总能获得正确的锁。如下面的程序:

public class PersonMap{
	//persons是一个有状态的对象,每个线程访问该变量可能都不同
	private final Map persons = new HashMap<String,Person>();
	
	//为添加Person的方法上锁
	public synchronized Map addPerson(String personName,Person person){
		return persons.put(personName,person);
	}
}

可以看出,persons是一个有状态的变量,有状态的变量通常是导致类线程不安全的因素,可是这个PersonMap类是线程安全的,这是因为在这个类中,线程需要改变变量的唯一方法是进入addPerson这个方法,但是这个方法是被synchronized关键字上锁的,所以唯一的入口已经安全,那么整个类都是安全的。
在这里插入图片描述

1.2、分析ArrayList的线程安全性

ArrayList的主要源码:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { /**
     * 列表元素集合数组
     * 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData,
     * 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY 
     */ transient Object[] elementData; /**
     * 列表大小,elementData中存储的元素个数
     */ private int size; }

所以通过这两个字段我们可以看出,ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。其中的elementData和size两个变量是有状态变量,且没有任何的处理,接下来看看add方法:

public boolean add(E e) { /**
     * 添加一个元素时,做了如下两步操作
     * 1.判断列表的capacity容量是否足够,是否需要扩容
     * 2.真正将元素放在列表的元素数组里面
     */ ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }

当2个线程共同进入add方法时,例如当前size为5,那么线程1访问到size为5时,ensureCapacityInternal()方法对size进行扩容,将size扩容为6;在这个时候线程2读取到的size是6,已经可以插入数据,但是这个数据已经被线程1插入了,所以产生了线程的不安全。那应该怎么解决呢?我们可以限制变量确保线程安全,如下列代码:

public synchronized boolean add(E e) { /**
     * 添加一个元素时,做了如下两步操作
     * 1.判断列表的capacity容量是否足够,是否需要扩容
     * 2.真正将元素放在列表的元素数组里面
     */ ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }

1.3、总结

限制性使构造线程安全的类变得更容易。因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的查询。

二、委托线程安全模式

2.1、什么是委托线程安全

我们还可以将线程的安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。

2.2、委托线程安全的实例

public class StatefulServlet extends HttpServlet{
    //使用concurent并发包中的atomic工具包下的原子变量类,保证多线程请求下的原子操作
    private AtomicLong count = new AtomicLong(0);
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String paramName = servletRequest.getParameter("paramName");
        count.incrementAndGet();//自增1
        servletResponse.getWriter().write(paramName+count);
    }
}

这就是个很好的委托线程安全模式,即把整个类的线程安全状况委托给了这个count变量,由于count变量采用的了原子变量类,所以这个委托是生效的,该类也线程安全。

三、基于线程安全类的扩展模式

Java类库中包含了许多有用的“构建块”类。重用这些已有的类要好与创建一个新的类。重用能够降低开发的难度、风险以及维护成本。有时一个线程安全类支持我们需要的全部操作,但更多的时候,一个类只支持我们需要的大部分操作,这时需要在不破坏其线程安全性的前提下,向它添加一个新的操作。这就是基于线程安全类的扩展模式。

3.1、基于Vector的功能扩展

添加一个新原子操作的最安全的方式是,修改原始的类,以支持期望的操作。但是你可能无法访问源代码或者修改的自由,所以通常需要扩展类。

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

3.2、基于List的组合

向已有的类中添加一个原子操作,还有更健壮的选择:组合。下列代码中,ImprovedList通过将操作委托给底层的List实例,实现了List的操作,同时还添加了一个原子的putIfAbsent方法。

public class ImproveList<T> extends List<T>{
	private final List<T> list;
	
	public ImproveList(List<T> list){
		this.list = list;
	}
	
	public synchronized boolean addIfAbsent(T x){
		if(contains){
			list.add(x);
			return true;
		}
		return false;
	}
	
}

在这里插入图片描述
通过内部锁,ImproveLis引入了一个新的锁层。它并不关系底层的List是否线程安全,即使List不是线程安全的,虽然额外的一层同步可能会带来一些微弱的性能损失,但是对于并发程序来说,应该永远坚持:宁可牺牲时间,也要保证安全!

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/83280454