Java之旅--多线程进阶

本文详细探讨了Java中的多线程概念,包括线程安全、原子操作、互斥锁与自旋锁,以及ThreadLocal和synchronized的区别。通过实例分析了线程间协作机制,如wait/notify、条件变量和同步队列。还讨论了Java线程死锁的预防策略,以及在实际应用中如何获取异步线程的返回结果。文章还对比了ReentrantLock和synchronized的性能,提供了解决线程安全的各种场景下的实践方案。
摘要由CSDN通过智能技术生成


先说点别的,为什么要逐渐学会读英文书籍


解释一个名词:上下文切换、Context switch


多任务系统中,上下文切换是指CPU的控制权由运行任务转移到另外一个就绪任务时所发生的事件。

When one thread’s execution is suspended and swapped off the processor, and another thread is swapped onto the processor and its execution is resumed, this is called a context switch.


为什么读英文版的计算机书籍,能理解的透彻和深刻。我们使用的语言,比如Java,是用英语开发的,其JDK中的类的命名、方法的命名,都是英文的,所以,当用英文解释一个名词、场景时,基于对这门编程语言的了解,我们马上就理解了,是非常形象的,简直就是图解,而用中文解释,虽然是我们的母语,但是对于计算机语言而言,却是外语,却是抽象的,反而不容易理解。

推荐书籍:Java Thread Programming ,但是要注意,这本书挺古老的,JDK1.1、1.2时代的产物,所以书中的思想OK,有些代码例子,可能得不出想演示的结果。


前言


关于多线程的知识,有非常多的资料可以参考。这里稍微总结一下,以求加深记忆。

关于多线程在日常工作中的使用:对于大多数的日常应用系统,比如各种管理系统,可能根本不需要深入了解,仅仅知道Thread/Runnable就够了;如果是需要很多计算任务的系统,比如推荐系统中各种中间数据的计算,对多线程的使用就较为频繁,也需要进行一下稍微深入的研究。


几篇实战分析线程问题的好文章:

怎样分析 JAVA 的 Thread Dumps

各种 Java Thread State 第一分析法则

数据库死锁及解决死锁问题

全面解决五大数据库死锁问题

关于线程池的几篇文章:

http://blog.csdn.net/wangpeng047/article/details/7748457

http://www.oschina.net/question/12_11255

http://jamie-wang.iteye.com/blog/1554927


基本知识


JVM最多支持多少个线程:http://www.importnew.com/10780.html

在一次真实的案例中,8G内存(Java应用分配了4G堆内存),4核,虚拟机,JVM 1.7,在应用down掉之前,大约开了12000个线程:http://blog.csdn.net/puma_dong/article/details/46669499


线程安全


如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。

如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者这样说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。


或者我们这样来简单理解,同一段程序块,从某一个时间点同时操作某个数据,对于这个数据来说,分叉了,则这就不是线程安全;如果对这段数据保护起来,保证顺序执行,则就是线程安全。


原子操作


原子操作(atomic operation):是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

原子操作(atomic operation):如果一个操作所处的层(layer)的更高层不能发现其内部实现与结构,则这个操作就是原子的。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。

在多进程(线程)访问资源时,原子操作能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。

原子操作时不需要synchronized,这是Java多线程编程的老生常谈,但是,这是真的吗?我们通过测试发现(return i),当对象处于不稳定状态时,仍旧很有可能使用原子操作来访问他们,所以,对于java中的多线程,要遵循两个原则:

a、Brian Goetz的同步规则,如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步;

b、Brain Goetz测试:如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以避免使用同步

通常所说的原子操作包括对非long和double型的primitive进行赋值,以及返回这两者之外的primitive。之所以要把它们排除在外是因为它们都比较大,而JVM的设计规范又没有要求读操作和赋值操作必须是原子操作(JVM可以试着去这么作,但并不保证)。


错误理解


import java.util.Hashtable;
class Test
{
	public static void main(String[] args) throws Exception {
		final Hashtable<String,Integer> h = new Hashtable<String,Integer>();
		long l1 = System.currentTimeMillis();
		for(int i=0;i<10000;i++) {
			new Thread(new Runnable(){
				@Override
				public void run() {
					h.put("test",1);
					Integer i1 = h.get("test");
					h.put("test",2);
					Integer i2 = h.get("test");
					if(i1 == i2) {
						System.out.println(i1 + ":" + i2);
					}
				}
			}).start();
		}
		long l2 = System.currentTimeMillis();

		System.out.println((l2-l1)/1000);
	}
}

有人觉得:既然Hashtable是线程安全的,那么以上代码的run()方法中的代码应该是线程安全的,这是错误理解。

线程安全的对象,指的是其内部操作(内部方法)是线程安全的,外部对其进行的操作,如果是一个序列,还是需要自己来保证其同步的,针对以上代码,在run方法的内部使用synchronized就可以了。


互斥锁和自旋锁


自旋锁:不睡觉,循环等待获取锁的方式成为自旋锁,在ConcurrentHashMap的实现中使用了自旋锁,一般自旋锁实现会有一个参数限定最多持续尝试次数,超出后,自旋锁放弃当前time slice,等下一次机会,自旋锁比较适用于锁使用者保持锁时间比较短的情况。

正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

CAS乐观锁适用的场景:http://www.tuicool.com/articles/zuui6z


ThreadLocal与synchronized


区别ThreadLocal 与 synchronized


ThreadLocal是一个线程隔离(或者说是线程安全)的变量存储的管理实体(注意:不是存储用的),它以Java类方式表现; 

synchronized是Java的一个保留字,只是一个代码标识符,它依靠JVM的锁机制来实现临界区的函数、变量在CPU运行访问中的原子性。 

两者的性质、表现及设计初衷不同,因此没有可比较性。

synchronized对块使用,用的是Object对象锁,对于方法使用,用的是this锁,对于静态方法使用,用的是Class对象的锁,只有使用同一个锁的代码,才是同步的。


理解ThreadLocal中提到的变量副本


事实上,我们向ThreadLocal中set的变量不是由ThreadLocal来存储的,而是Thread线程对象自身保存。

当用户调用ThreadLocal对象的set(Object o)时,该方法则通过Thread.currentThread()获取当前线程,将变量存入Thread中的一个Map内,而Map的Key就是当前的ThreadLocal实例。


Runnable与Thread

实现多线程,Runnable接口和Thread类是最常用的了,实现Runnable接口比继承Thread类会更有优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。


Java线程互斥和协作


阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java 提供了大量方法来支持阻塞,下面让对它们逐一分析。


1、sleep()方法:sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。

典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。


2、(Java 5已经不推荐使用,易造成死锁!!) suspend()和resume()方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用resume()使其恢复。

stop()方法,原用于停止线程,也已经不推荐使用,因为stop时会解锁,可能造成不可预料的后果;推荐设置一个flag标记变量,结合interrupt()方法来让线程终止。


3.、yield() 方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值