快速鸟瞰并发编程, 呕心沥血整理的架构技术【2】

做者:享学课堂James老师java

转载请声明出处!编程

接着第1篇后,咱们继续来跟进一下并发编程的其它内容,以下:安全

第5节 安全发布

要安全的发布一个对象,对象的引用和对象的状态必须同时对其余线程可见。通常一个正确构造的对象(构造函数不发生this操做),能够经过以下方式来正确发布:bash

  • 在静态初始化函数中初始化一个对象引用。只有一个线程能够初始化静态变量,由于类的初始化是在独占锁下完成的。
class  JamesStaticInitializer {
	// 在不进行其余初始化的状况下发布不可变对象
	public  static  final  Year year = Year.of(2017);
	public  static  final  Set<String> keywords;
	// 使用静态static来构造复杂对象
	static {
		// 建立可变集
		Set<String> keywordsSet = new  HashSet<>();
		// 初始化状态
		keywordsSet.add("james");
		keywordsSet.add("13号技师");
		// 使集合不可修改
		keywords = Collections.unmodifiableSet(keywordsSet);
	}
}
复制代码
  • 将一个对象引用保存在volatile类型的域或者是AtomicReference对象中。
class  JamesVolatile {
	private  volatile  String state;
	void setState(String state) {
		this.state = state;
	}
	String getState() {
		return state;
	}
}
复制代码
  • AtomicInteger将值存储在volatile字段中,所以volatile变量的相同规则适用于此处。
class  JamesAtomics {
	private  final  AtomicInteger state = new  AtomicInteger();
	void initializeState(int state) {
		this.state.compareAndSet(0, state);
	}
	int getState() {
		return state.get();
	}
}
复制代码
  • final域, 将对象的引用保存到某个正确构造对象的final类型的域中。
class  JamesFinal {
	private  final  String state;
	JamesFinal(String state) {
		this.state = state;
	}
	String getState() {
		return state;
	}
}
复制代码

确保此引用在构造期间不逃逸。多线程

this引用逃逸("this"escape)是指对象尚未构造完成,它的this引用就被发布出去了。这是危及到线程安全的,由于其余线程有可能经过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另一些线程看到的倒是已经初始化完的状态,这种不一致性是不肯定的,程序也会所以而产生一些没法预知的并发错误。在说明并发编程中如何避免this引用逸出以前并发

class  JamesThisEscapes {
	private  final  String name;
	ThisEscapes(String name) {
		JamesCache.putIntoCache(this);
		this.name = name;
	}
	String getName() {
		return name;
	}
}
class  JamesCache {
	private  static  final  Map<String, ThisEscapes> CACHE = new  ConcurrentHashMap<>();
	static  void putIntoCache(JamesThisEscapes thisEscapes) {
		//“this”引用在对象彻底构造以前逃逸
		CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes);
	}
}
复制代码
  • 正确同步成员变量。
class  JamesSynchronization {
	private  String state;
	synchronized  String getState() {
		if (state == null)
		state = "Initial";
		return state;
	}
}
复制代码

第6节 不可变的对象

不可变对象具有执行安全的特性。此外,相较于可变对象,不可变对象一般也较合理,易于了解,并且提供较高的安全性。不可变对象的一个重要特性是它们都是线程安全的,所以不须要同步。固然对象不可变的是有以下要求滴:函数

  • 全部变量都是 final.
  • 全部变量必须是可变对象或不可变对象。
  • this 在构造方法执行期间引用不会逃脱。
  • 该类是final,所以没法在子类中覆盖此行为。

不可变对象的示例:post

// 声明为final类
public  final  class  JamesArtist {
	// 不可变对象, 字段为final
	private  final  String name;
	//用于保存不可变对象, final类型
	private  final  List<JamesTrack> tracks;
	public  JamesArtist(String name, List<JamesTrack> tracks) {
		this.name = name;
		//防止拷贝
		List<JamesTrack> copy = new  ArrayList<>(tracks);
		//标记为不可更改
		this.tracks = Collections.unmodifiableList(copy);
		// “this”在构造期间不会传递到任何地方。
	}
}
// 同上声明为final类
public  final  class  JamesTrack {
	// 不可变对象, 字段为final
	private  final  String title;
	public  JamesTrack(String title) {
		this.title = title;
	}
}
复制代码

第7节 线程Thread类

java.lang.Thread类用于表示应用程序或JVM线程。代码老是在某些Thread类的上下文中执行(用于 Thread#currentThread()获取本身的Thread)。ui

线程状态以下this

线程协调方法以下

怎么处理 InterruptedException异常?

  • 在从新抛出 InterruptedException 以前执行特定于任务的清理工做。
  • 声明当前方法抛出 InterruptedException.
  • 若是未声明某个方法抛出 InterruptedException,则应经过调用将中断的标志恢复为true, Thread.currentThread().interrupt()而且应该抛出一个更合适的异常。将标志设置为true很是重要,以便有机会处理更高级别的中断。

不可预知的异常处理

线程能够指定 UncaughtExceptionHandler将接收任何致使线程忽然终止的未捕获异常的通知。

Thread thread = new  Thread(runnable);
thread.setUncaughtExceptionHandler((failedThread,exception)->
{
	logger.error("Caught unexpected exception in thread ‘{}’.", failedThread.getName(), exception);
}
);
thread.start();
复制代码

第8节 线程的活跃度

死锁

当存在多个线程时会发生死锁,每一个线程等待另外一个线程持有的资源,从而造成资源循环和获取线程。

潜在的死锁示例:

class  JamesAccount {
	private  long amount;
	void plus(long amount) {
		this.amount += amount;
	}
	void minus(long amount) {
		if (this.amount < amount)
		throw  new  IllegalArgumentException(); else
		this.amount -= amount;
	}
	static  void transferWithDeadlock(long amount, JamesAccount first, JamesAccount second) {
		synchronized (first) {
			synchronized (second) {
				first.minus(amount);
				second.plus(amount);
			}
		}
	}
}
复制代码

若是同时发生死锁:

  • 一个线程正在尝试从第一个账户转移到第二个账户,而且已经得到了第一个账户的锁。
  • 另外一个线程正在尝试从第二个账户转移到第一个账户,而且已经得到了第二个账户的锁。

避免死锁的技巧:

  • 锁定顺序 - 始终以相同的顺序获取锁。
class  JamesAccount {
	private  long id;
	private  long amount;
	// 此处省略了一些方法
	static  void transferWithLockOrdering(long amount, JamesAccount first, JamesAccount second) {
		Boolean lockOnFirstAccountFirst = first.id < second.id;
		Account firstLock = lockOnFirstAccountFirst ? first : second;
		Account secondLock = lockOnFirstAccountFirst ? second : first;
		synchronized (firstLock) {
			synchronized (secondLock) {
				first.minus(amount);
				second.plus(amount);
			}
		}
	}
}
复制代码
  • 锁定超时 - 在获取锁时不要无限制地阻塞,应该释放全部锁后再尝试。
class  JamesAccount {
	private  long amount;
	//省略了一些方法
	static  void transferWithTimeout(long amount, JamesAccount first, JamesAccount second, int retries, long timeoutMillis)
	throws  InterruptedException {
		for (int attempt = 0; attempt < retries; attempt++) {
			if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
				try {
					if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
						try {
							first.minus(amount);
							second.plus(amount);
						}
						finally {
							second.lock.unlock();
						}
					}
				}
				finally {
					first.lock.unlock();
				}
			}
		}
	}
}
复制代码

JVM能够检测监视器死锁,并在线程转储中打印死锁信息。

活锁和线程饥饿

活锁偏偏与死锁的概念相反,死锁的意思是全部线程都拿不到资源且都占用着对方的资源,而活锁的意思是拿到资源后各线程却又相互释放不执行。

若是多线程中出现了相互谦让,至关于13号技师得病后你们都不要她了,都主动将资源释放给其它的线程来使用,那么这个资源会不断在多个线程之间跳动但又得不到明确执行,这就是活锁;并且多线程执行中有线程的优先级,有的线程优先级高它是可以插队并优先执行的,但若是这些优先级高的线程一直抢占优先级低线程的资源,最终会致使低优先级线程它是没法获得执行的,这就是饥饿。

(未完待续......)

关注我,等待更新,还有更多技术干货分享~

相关文章
相关标签/搜索