《Java并发编程实战》笔记1——Java线程安全基础

1、什么是线程安全?

多个线程访问某个类时,不管运行时环境采用何种调用方式或者这些线程将如何交替执行,并且在主调代码中不需要任何的额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。(摘自Java并发编程实战)

2、无状态对象一定是线程安全的。

那么,什么才称为无状态呢?有状态是指有数据存储的功能,也就是有实例变量。无状态是指不包含任何域,也不包含对其他类中域的引用。(大多数servlet都是无状态的)

当在一个无状态的类中添加一个状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的

3、关于用锁来保护状态

下面给出《Java并发编程实战》的两个小例子

(1)

如果只是将每个方法都作为同步方法,例如Vector,并不足以确保Vector上的符合操作都是原子的。

比如:

		if(!vector.contains(element)){
			vector.add(element);
		}

上面包含了两个操作,判断集合是否含有某个元素、把元素添加到集合中,假设多线程执行顺序如下,那么就会添加了两个相同的element

《Java并发编程实战》笔记1——Java线程安全基础_第1张图片

 (2)

public class UnsafeCachingFactorizer implements Servlet{

	private final AtomicReference lastNumber = new AtomicReference<>();
	private final AtomicReference lastFactors = new AtomicReference<>();
	
	@Override
	public void service(ServletRequest req, ServletResponse resp)
			throws ServletException, IOException {
		
		BigInteger i = extractFromRequest(req);
		if(i.equals(lastNumber.get())){
			encodeIntoResponse(resp,lastFactors.get());
		}else{
			BigInteger[] factors = factor(i);
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp,factors);
		}
	}
}

在上面的例子中,虽然AtomicReference是线程安全类,对set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastFactors。如果只修改了其中一个变量,那么在这两次修改操作之间,其他线程将发现不变性条件被破坏了。

结论:当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

改进后的代码如下:

public class CachedFactorizer implements Servlet{

	private BigInteger lastNumber;
	private BigInteger[] lastFactors;
	private long hits;
	private long cacheHits;
	
	public synchronized long getHits(){
		return hits;
	}
	
	public synchronized double getCacheHitRatio(){
		return (double)cacheHits/(double)hits;
	}
	
	@Override
	public void service(ServletRequest req, ServletResponse resp)
			throws ServletException, IOException {
		
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		synchronized (this) {
			++hits;
			if(i.equals(lastNumber)){
				++cacheHits;
				factors = lastFactors.clone();
			}
		}
		
		if(factors==null){
			factors = factor(i);
			synchronized (this) {
				lastNumber = i;
				lastFactors = factors.clone();
			}
		}
		encodeIntoResponse(resp, factors);
	}
}

 在上面改进的代码中,使用了两个独立的同步代码块。对单个变量来实现原子操作来说,原子变量时很有用的,但由于已经使用了同步代码块,使用两种不同的同步机制会带来混乱也不会提高性能,所以不再使用原子类型。

4、非原子的64位操作

非volatile类型的64位数值变量(double、long),JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题(没有同步的情况下可能会产生),在多线程程序中使用共享且可变的long和double等变量也是不安全的,除非用volatile来声明,或者用锁保护起来。

PS:调试提示:启动JVM时要指定-server命令行选项。server模式比client模式进行更多的优化,例如将循环中未被修改的变量提升到循环外部,但client模式不会这样做。因此在开发环境(-client模式)中能正确运行的代码,可能在部署环境(-server)中会运行失败。

例如:如果没有把asleep变量声明为volatile类型,那么server模式的JVM会将asleep的判断条件提升到循环体外部(这将导致一个无限循环),所以一定要加上volatile,否则其他线程更新asleep变量后可能不可见,因为可能已经在循环体外面判断完毕,在循环体循环时就不会再次判断了

volatile boolean asleep;
while(!asleep){
  //执行相应操作
}

5、发布和逸出

发布对象是指使对象能够在作用域以外的代码中使用

(1)使内部的可变状态逸出

任何调用者都能修改states中的内容

class xxx{
    private String[] states= new String[]{"1","2"};
    public String[] getStates(){return states;}
}

(2)隐式使this引用逸出

this引用在构造函数中逸出了,当内部EventListener实例发布时,在外边封装的ThisEscape实例也逸出了。只有当对象返回时,this引用才应该从线程中逸出

public class ThisEscape{

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

    void doSomethings(Event e){

    }
}

改进代码:

如果想在构造函数中注册一个时间监听器或线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。

public class SafeListener{
    private final EventListener listener;
    
    private SafeListener(){
        listener = new EventListener(){
            public void onEvent(Event e){
                doSomethings(e);
            }  
        };
    }

    void doSomethings(Event e){

    }

    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

}

PS:在构造函数中创建启动线程,this引用都会被新创建的线程共享。在对象未完全构造之前,新的线程就可以看见它。在构造函数中创建线程最好不要立即启动它,而是通过一个方法来启动。

6、关于实例封闭

可以通过synchronize监视器或者将对状态的访问委托给线程安全的类(如concurrent包下,或者通过Collections工具类包装非安全集合)来保障线程安全,但需要注意的情况如:

(1)看似不可变的类

class xx{
    private final Map maps;

    //or:private final ConcurrentHashMap maps

    public synchronized void addPerson(Person p){
       //...
    }

    public synchronized void delPerson(){
       //...
    }

    public synchronized Map getMaps(){
        return maps;
    }
}

虽然属性是final,看似是不可变的,但实际上,如果Person类是可变的,那么获取Person对象时仍需要进行额外的同步或者使得Person成为线程安全的类(如:使其成为不可变类、返回maps时对Person可变数据进行深拷贝[这种方式可能会出现信息得不到及时更新])

(2)如果状态之间不是彼此独立,通过线程安全的类进行组合的结果也不能保证线程安全,此时必须通过加锁机制保证

eg:AtomicInteger 是线程安全的,但lower和upper两个状态都是彼此独立的,没有通过加锁来是不能保证操作的原子性和线程安全的。必须保证这些复合操作是原子的。

public class XXX{

    private final AtomicInteger lower = new AtomicInteger(0);//下界
    private final AtomicInteger upper = new AtomicInteger(1);//上界

    public void setLower(int i){
        if(i>upper.get()){
            //...
        }
    }

    public void setUpper(int i){
        if(i

7、在现有的线程安全类添加功能

(1)客户端加锁机制

①错误案例:非线程安全的“若没有则添加”

public class ListHelper {

	public List list = Collections.synchronizedList(new ArrayList());

	/**
	 * 不是同一个锁,导致添加和判断不能同步。将list包装成线程安全的类,使用Collections.synchronizedList是将集合中每个方法加上同步锁,
	 * 但是这个同步锁和ListHelper类中synchronized方法的锁是不一样的,
	 * 所以putIfAbsent方法对于list的其他操作并不是原子的,因此无法确保当putIfAbsent执行时另一个线程不会修改链表
	 */
	public synchronized boolean putIfAbsent(E x) {
		boolean absent = !list.contains(x); // 1-ListHelper的锁
		if (absent)
			list.add(x);
		return absent;
	}

	public static void main(String[] args) {
		ListHelper helper = new ListHelper();
		new Thread(new Runnable() {
			@Override
			public void run() {
				helper.list.add("a"); // 2-java虚拟机维护的锁
			}
		}).start();
		helper.putIfAbsent("a");
	}

}

《Java并发编程实战》笔记1——Java线程安全基础_第2张图片

②通过客户端加锁实现“若没有则添加”

public class ListHelper {

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

(2)组合

ImprovedList通过自身的内置锁增加了一层额外的锁。它并不关心底层的List的加锁实现,即时修改了,也会提供一直的加锁机制保证线程安全。但如果是通过扩展的方式来实现同步策略,如果底层同步策略修改并选择不同的锁来保护状态,子类就会被破坏,因为同步策略修改后他无法使用正确的锁来控制对基类状态的并发访问。

public class ImprovedList implements List{

    private final List list;

    public ImprovedList(List list){
        this.list = list;
    }

    public synchronized boolean putIfAbsent(T x){
        boolean contains = list.contains(x);
        if(contains){
            list.add(x);
        }
        return !contains;
    }

    public synchronized void clear(){list.clear();}
    //...
}

8、同步容器类

(1)可能抛出ArrayIndexOutOfBoundsException的操作

①交替使用getLast和deleteLast时可能会报错

《Java并发编程实战》笔记1——Java线程安全基础_第3张图片

 

需要使getLast和deleteLast成为原子操作,并确保Vector的大小在调用size和get之间不会发生变化。修改后代码:

	public static Object getLast(Vector list) {
		synchronized(list) {
			int lastIndex = list.size() - 1;
			return list.get(lastIndex);
		}

	}
	
	public static void deleteLast(Vector list) {
		synchronized(list) {
			int lastIndex = list.size()-1;
			list.remove(lastIndex);
		}
	}

②如果在调用size和set之间有线程并发修改list时,可能报错

	public void foreach(Vector list) {
		for(int i = 0;i

修改后代码:

	public void foreach(Vector list) {
		synchronized (list) {
			for(int i = 0;i

(2)可能抛出ConcurrentModificationException的操作

①迭代过程中被修改会出错

	List widgetList = Collections.synchronizedList(new ArrayList<>());

	public void test() {
		for (Widget w : widgetList) {
			doSomethings(w);
		}
	}

如果在迭代期间对容器加锁,那么在调用doSomethings需要持有一个锁(Widget的锁),可能会产生死锁 。真正要保证同步,需要对vector加锁,还要对集合里面的对象也要进行加锁。这样的话,如果你对vector加锁了,另一个线程对你那个对象进行加锁了,现在两个线程都希望对方的锁,又不释放锁,就会造成死锁。

如果不希望在迭代期间对容器加锁,那么可以克隆容器,并在副本上进行迭代。由于副本被封闭在线程内,其他线程不会在迭代期间修改,这样就避免了抛出ConcurrentModificationException(在克隆过程仍然需要对容器加锁)

②隐藏的迭代器

如toString方法、containsAll、removeAll、retainAll、以及把容器作为参数的构造函数,都会对容器进行迭代。

 

 

你可能感兴趣的:(多线程并发)