通过《java并发:进程与线程》已经大致了解了促使进程线程产生的原因,以及java中线程的操作方式,那么多线程并发操作下会产生什么问题呢? A.线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步,这个类的行为仍然是正确的,那么称这个类是线程安全的。 B.引起线程安全问题的因素 无状态就是线程安全 多线程编程或者分布式编程最忌讳有状态,一有状态就不但限制了其横向扩展能力,也是产生并发问题的起源。当你设计的类是无状态的,那么它永远都是线程安全的。因此在设计阶段需要考虑如何用无状态的类来满足你的业务需求 原子操作 所谓原子性,是说一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。如果所有程序块都是原子性的,那么就不存在任何并发问题。而很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count++)和check-then-act,这些都是很容易被忽视的,例如大家所常用的惰性初始化模式,以下代码就不是线程安全的: @NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; Public synchronized ExpensiveObject getInstance() {
return instance; } } 这段代码具体问题在于没有认识到if(instance==null)和instance = new ExpensiveObject();(假设没有synchronized)是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance==null)后会被中断,另一个线程也去执行if(instance==null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。 虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。 另外还有: 非原子的64位操作,JVM允许将64位读或写划分为两个32位的操作。 可见性 我们不仅要避免一个线程修改其他线程正在使用的对象的状态,还希望确保党一个线程修改了对象的状态之后,其他的线程能够真正看到改变。这就是:内存可见性。 看下面的例子: public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() {
System.out.println(number); } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } } 上面的程序可能打印出0(重排序,在number可见之前,ready就已经写入,并对读取线程可见),或者永远不会终止,这是因为它没有保证写入ready和number的值对读线程是可见的。 更多关于可见性的内容参考:
一些确保线程安全的方法 访问共享的,可变的数据,要求同步,为了保证变量元素的可见性,可以采用如下方法: ① 线程封闭 最简单的方式就是不共享数据,如果数据仅在单线程中访问,就不需要任何同步。线程封闭技术是实现线程安全的最简单的方式,当对象封闭在一个线程中,这种做法会自动成为线程安全的,即使被封闭的对象本身并不是。比如JDBC的连接池,虽然JDBC本身规范并没有要求Connection对象是线程安全的,但是在典型的服务器应用中,线程总是从池中获得一个Connection对象,并且用它处理一个单一的请求,最后把它归还,每个线程都会同步地处理大多数请求,而且在Connection对象在被归还前,池不会将它再分配给其他线程 ② 栈限制 将变量限制在方法中。 ③ ThreadLocal 它允许将变量和线程关联在一起,使得每个线程都有一份单独的拷贝。 ④ 不可变性 不可变对象永远是线程安全的,一个对象是不可变的饿,要求它的状态创建后不会改变,所有域都是final类型,并且,它被正确创建。 |
|