java并发编程实战学习 第3章

java并发编程实战学习 第3章

第1章 简介

第2章 线程安全性

第3章 对象的共享

同步除了“互斥”还有另外一个重要的方面:内存可见性。

例子:主线程启动读线程,然后将number设为42,ready设为true。读线程一致循环直到发现ready的值为true,然后输出number的值。虽然看起来会输出42,但是可能输出0,或者根本无法终止。

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
                System.out.println(number);
            }
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些想不到的调整。即“重排序”

所以说,在没有同步的情况下,判断结果很容易出错,要避免这些问题,必须使用同步

NoVisibility展示了缺乏同步可能产生错误的一种情况:失效数据。

下面这个例子,就很容易看到失效值

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

可以使用synchronized使其变为一个线程安全带类

@ThreadSafe
public class SynchronizedInteger {
    @GuardedBy("this")
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized void setValue(int value) {
        this.value = value;
    }
}

在没有同步的情况下,可能得到一个失效值,至少是之前某个线程设置的,而不是一个随机值。这种安全保障被称为最低安全性

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量double和long。

Java内存模型要求,变量的读取操作和写入都必须是原子操作,但非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解成为两个32位的操作。所以说是不安全的。除非用volatile或者锁。

内存锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,如下图,线程A执行某个同步代码块时,线程B随后进入由同一个锁保护的同步代码块,这样可以保证,在锁释放之前,A看到的变量值在B获得锁后同样可以由B看到

3.1线程可见性.png

加锁的含有不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看,能共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。
把变量声明为volatile类型后,就不会对该变量做重排序。不会缓存在寄存器或者处理器不可见的地方,因此返回的总是最新的写入值。

不建议过度依赖volatile变量提供的可见性。

一个典型用法:作为一个标识判断是否退出循环

volatile boolean asleep;
...
    while(!asleep)
	    countSomeSheep();

仅当满足一下所有条件时,才应该使用volatile变量

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

“发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。当某个不应该发布的对象被发布时,这种情况被称为逸出。

发布最简单方法是将对象引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象

例如:

public static Set<Secret> knownSecrets;
    
public void initialize() {
	knownSecrets = new HashSet<Secret>();
}

从私有方法中返回一个引用,同样会发布返回的对象

public class UnsafeStates {
    private String[] states = new String[]{"AK","AL"};
    
    public String[] getStates() {
        return states;
    }
}

如果按照上述方法来发布states,就会出现问题,因为任何调用者都能修改这个数组的内容。无论其它的线程对已发布的引用执行何种操作,其实都不重要,因为误用该引用的风险始终存在。

最后一种发布对象或者其内部状态的机制就是发布一个内部的类实例,例如:

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

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

public class SafeListener {
    private final EventListener listener;

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

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

当访问共享的可变数据时,通常需要使用同步。一种避免是使用同步的方式就是不共享数据。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。

常见场景

  • Swing
  • JDBC

几种封闭技术

  • Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。比较脆弱。

  • 栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。

例如:

public int loadTheArk(Collection<Animal> candiadtes) {
	SortedSet<Animal> animals;
	int numPairs = 0;
	Animal candidate = null;

	// animals被封闭在方法中,不要使它们逸出!
	animals = new TreeSet<Animal>(new SpeciedGenderComparator());
	animals.addAll(candiadtes);
	for(Animal a : animals) {
		if(candiadte == null || !candiadte.isPotentialMate(a)) {
			candidate = a;
		} else {
			ark.load(new AnimalPair(candidate, a));
			++numPairs;
			candidate = null;
		}
	}
	return numPairs;
}

简单的说,就是numPairs是基本类型,没有引用逸出。

  • ThreadLocal类

ThreadLocal提供了get与set等方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。

ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
	@Override
	public Connection initialValue() {
		return DriverManager.getConnection(DB_URL);
	}
};

public static Connection getConnection() {
	return connectionHolder.get();
}

满足同步需求的另一个方法是使用不可变对象。简单的说,并发问题就是同时访问一个可变的状态引起的,现在不可变。所以就没有并发问题了。

不可变对象一定是线程安全的。

对象中所有的域都是final类型,这个对象仍是可变的,因为在final类型的域中可以保存对可变对象的引用。

当满足一下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型。
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

例子:可变对象基础上构建不可变类

@Immutable
public class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }
    
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

正如“除非需要更高的可见性,否则将所有的域声明为私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否者将其声明为final域”也是一个良好的编程习惯。

在某些情况下,不可变对象能提供一种弱形式的原子性。我们看之前的一个因式分解例子,我们将具有原子性的对数值以及因数分解结果进行缓存构建成不可变容器类

@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    
    
    public OneValueCache(BigInteger i, BigInteger[] factorys) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factorys, factorys.length);
    }
    
    public BigInteger[] getFactors(BigInteger i) {
        if(lastNumber == null || !lastNumber.equals(i)) {
            return null;
        } else {
            return Arrays.copyOf(lastFactors, lastFactors.length);
        }
    }
}

对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中消除。然后将这个对象设置为volatile类型,当引用是一个新OneValueCache时,其他线程就会立即看到新缓存数据。

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if(factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得Volatile Cached Factorizer在没有显式地使用锁的情况下仍然是线程安全的。

一个不正确发布例子

public Holder holder;

public void initialize() {
	holder = new Holder(42);
}

这种不正确的发布导致其他线程看到尚未创建完成的对象。

另一个例子

public class Holder {
    private int n;
    
    public Holder(int n) {
        this.n = n;
    }
    
    public void assertSanity() {
        if(n != n) {
            throw new AssertionError("This statement is false");
        }
    }
}

另一个线程在调用assertSanity时将抛出AssertionError。

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。、

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”。

在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值