Java并发编程实战——基础知识

1 基础知识

1.3 线程带来的风险

1.3.1 安全性问题

1.3.2 活跃性问题

活跃性关注“某件正确的事情最终会发生”。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,最常见的活跃性问题就是死循环。死锁、饥饿和活锁等问题都是活跃性问题。

2 线程安全性

2.2 原子性

2.2.1 竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

最常见的竞竞态条件类型就是“先检查后执行(Check-Then-Act)”,即通过一个可能失效的观测结果来决定下一步的动作。
使用“先检查后执行”的一种常见情况就是延迟初始化。

另一种竞态条件就是“读取-修改-写入”。例如计数器的增长,计数器增长需要基于对象之前的状态来定义对象状态的转换。

2.2.1 数据竞争

如果在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争。 在Java内存模型中,如果在代码中出现数据竞争,那么这段代码就没有确定的语义。

2.3 加锁机制

要保持状态的一致性,就需要在单个院子操作中更新所有相关的状态变量。

2.3.1 内置锁

2.4 用锁来保护状态

当某个变量由锁来保护时,意味着在每次访问这个变量时都需要先获得锁,这样就确保在同一时刻只有一个线程可以访问这个变量。当类的不变性条件涉及多个状态变量时,那么还有另外一个需求:在不变性条件中的每个变量都由同一个锁来保护。因此可以在单个原子操作中访问或更新这些变量,从而确保不变性条件不被破坏。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

通常,在简单性和性能之间存在着互相制约因素。当实现某个同步策略时,,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。

当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或者控制台IO),一定不要持有锁。

3 对象的共享

3.1 可见性

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

3.1.1 失效数据

当线程读取变量的时候可能读到的是一个失效的值,称为失效数据。

3.1.2 非原子的64位操作

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前的某个线程设置的,而不是随机值。 这种安全性保证也称为最低安全性(out-of-thin-air-safety)。

最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(double和long)。Java内存模型要求,变量的读取和写入都必须是原子操作,但是对于非volatile类型的double和long变量,JVM将允许64位的读写操作分解为两个32位的读写操作。 当读取一个非volatie类型的long变量的时候,如果对该变量的读写在不同的线程中,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑数据失效问题,多线程中使用共享且可变的64位数据变量时也是不安全的,除非使用volatile或者加锁保护。

3.1.3 加锁与可见性

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

3.1.4 volatile变量

并不建议过度依赖volatile变量提供的可见性。如果在代码中依赖volatie变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难以理解。

仅当volatile变量能简化代码实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括: 确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或者关闭)。

volatile的一种典型用法:检查某个状态标记以判断是否退出循环。

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

volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。

加锁操作既可以保证原子性又可以保证可见性,而volatile变量只能确保可见性。

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

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

3.2 发布与逸出

发布(Publish)指对象能够在当前作用域之外的代码中使用。 当某个不应该发布的对象被发布时,这种情况被称为逸出(Escape)。

发布对象的最简单方法就是将对象标记为public staic。例如:

public static Set<Secret> kownSecrets;

public void initialize() {
	kownSecrets = new HashSet<Secret>();
}

当发布一个对象的时候可能会间接地发布其他对象。

如果从非私有方法中返回一个引用,那么同样会发布返回的对象。 例如:

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

发布一个对象时,在该对象非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。

发布一个内部的类实例也是一种发布对象或其内部状态的机制。例如如下案例,隐式地使this引用逸出。

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

不要在构造过程中使this引用逸出。
在构造过程中使得this逸出的一个常见错误就是在构造函数中启动线程。在构造函数中创建线程并没有错误,但是最好不要立即启动它。在构造函数中调用一个可改写的实例方法(既非private,也非final)时,同样会导致this引用在构造过程中逸出。
如果想在构造函数中注册一个事件监听器或启动线程,可以使用私有的构造器和public的工厂方法(Factory Method)。如下所示:

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;
	}
}

3.3 线程封闭

仅在线程内部使用数据的时候就可以不用考虑同步问题了,这种技术就被称为线程封闭(Thread Confinement)。即使一个对象不是线程安全的,只要被封闭在了线程内,会自动实现线程安全性。

3.3.1 Ad-hoc线程封闭

Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。 Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,能将对象封闭到目标线程上。

通常决定使用线程封闭技术的时候,通常将某个特定的子系统实现为一个单线程子系统。

在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取——修改——写入”的操作。这种情况下,相当于将修改操作封闭在单个线程中以防止发生静态条件,并且volatile变量的可见性还确保了其他线程能看到最新的值。

3.3.2 栈封闭

在栈封闭中只能通过局部变量才能访问对象。

3.3.3 ThreadLocal

程序清单 3-3-3-1 使用ThreadLocal维持线程封闭

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

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

开发人员经常滥用ThreadLocal,例如将所有的全局变量都作为ThreadLocal对象,或者作为一种“隐藏”方法参数的手段。ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要给外小心。

3.4 不可变性

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

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

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

程序清单 3-4-1 在可变对象基础上构建不可变类

public final 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);
	}
}

3.4.1 Final域

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

3.4.2 使用volatile类型发布不可变对象

一个例子, 这里是一个缓存分解的因数,OneValueCache 不可变,可以保证程序是线程安全的。

@Immutable 
class OneValueCache {
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors;

	public OneValueCache(BigInteger i, BigInteger[] factors) {
		this.lastNumber = i;
		this.lastFactors = factors;
	}

	public BigInteger[] getFactors(BigInteger i) {
		if (lastNumber == null || !lastNumber.equals(i)) {
			return null;
		} else {
			return Arrays.copyOf(lastFactors, lastFactors.length);
		}
	}
}
@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 = factors(i);
			cache = new OneValueCache(i, factors);
		}
		encodeIntoResponse(resp, factors);
	}
}

3.5 安全发布

3.5.1 不正确的发布:正确的对象被破坏

3.5.2 不可变对象与初始化安全性

3.5.3 安全发布的常用模式

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

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

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

pubilc static Holder holder = new Holder(42);

静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化任何对象都可以被安全地发布。

3.5.4 事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不再改变,那么把这种对象称为事实不可变对象(Effectively Immutable Object)。在这些对象发布后,程序只需要将它们视为不可变对象即可。通过使用事实不可变对象,不仅可以简化开发过程,而且还能因为减少了同步而提高性能。

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

3.5.5 可变对象

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

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

3.5.6 安全地共享对象

在并发程序中使用共享对象时,可以使用一些实用的策略,包括:

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

对一个URL接口进行压测。

for /l %%i in (1,1,4096) do (
	curl -X GET "http://localhost:8080/rest/xxxx" -H "accept: */*"
	timeout /t 1 /nobreak > NUL
)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值