关于线程 thread 的一些知识

文章参考:

Java线程详解,写的很好

一 认识线程

1 什么是线程

线程是进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能够独立运行的基本单位。线程自己本身不拥有系统资源,只拥有一点在运行中必不可少的资源(比如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另外一个线程,同一进程中的多个线程可以进行并发执行。线程有就绪,阻塞和运行三种状态。

这里提到了一个叫进程的东西。尽管简单都知道,但是还是写一下吧! 进程指的是运行中的应用程序,每个进程都有自己的独立的地址空间,也就是内存空间。例如用户在桌面上点击了IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,就又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。

2 硬件线程与软件线程

http://www.uedsc.com/reading-thread.html

多核微处理器带有一个以上的物理内核,物理内核是真正独立的处理单元,多个物理内核使得多条指令能够并行的运行。为了充分发挥物理内核的功效,有必要运行多个进程,或者一个进程里运行多个线程,创建多线程的代码。

然而每个物理内核都可能会提供多个硬件线程,也就是逻辑内核或者逻辑处理器。使用了超线程技术的微处理器在每一个物理内核上提供了多分架构状态,比如很多带有4个物理内核并且使用了超线程技术的微处理器在每一个物理内核上提供两份架构状态,从而获得了8个硬件线程。这种技术称为对称多线程,他通过额外的架构状态在微处理器的指令级别对并行执行进行优化和增强,对称线程技术并不局限于每个物理内核两个硬件线程,有的是4个。所以可以看出,一个硬件线程并不代表一个物理内核。某些情况下,对称线程技术可以提升多线程代码的性能。

在操作系统,比如windows中,每个运行的程序都是进程。每一个进程中都运行了至少一个线程,这种线程被称为是软件线程,用于区分前面提到的硬件线程。一个进程至少有一个线程,而且必有一个主线程。操作系统的调度器在所有要运行的线程之间公平的分享可用的处理资源。Windows的调度器会给每一个软件线程分配处理时间。当windows调度器运行在多核微处理器上时,调度器必须从物理内核支持的硬件线程中分配时间给每一个需要运行指令的软件线程。打一个比方,可以吧每一个硬件线程想象成一个泳道,而软件线程则是上面的游泳者。并且有个调度器还专门负责这个游泳者要在什么时候在泳道上游泳,什么时候在泳道上停住。

Windows将每一个硬件线程识别为一个可调度的逻辑处理器。每一个逻辑处理器可以运行软件线程的代码。运行了多个软件线程的进程可以充分发挥硬件线程和物理内核的优势,并行的运行指令。


每一个线程都与其父进程分享一个私有的唯一的内存空间。但是每一个软件线程都有自己的栈, 寄存器 和 私有局部存储区域。

windows调度器可以决定将一个软件线程赋给另一个硬件线程,通过这种方式均衡每一个硬件线程的负载。由于通常都有其他软件线程在等待处理时间,因此,负载均衡机制可以合理的组合有效的资源,让其他软件也有机会执行各自的指令。

负载均衡简略说明:他指的是将软件线程的任务分发在多个硬件线程里去操作,通过负载均衡,工作负载可以公平的分配在硬件线程之间。然而是否能够完美的实现负载均衡取决于应用程序的并行程度,工作负载,软件线程数,可用的硬件线程以及负载均衡策略。

多线程的终极目的就是最大限度的利用CPU资源,记住是充分利用CPU资源,而达到的用户使用上,感觉很好很快。而不是提高cpu的运行效率。运行效率这东西,除非你换一个CPU吧,,估计没有多少折儿。  所以把能够把多线程用好,决定了你开发的程序,在同一个机子上,比其他人开发的是不是用起来更好使更快,起码我现在的水平就是这样理解的。

3 软件线程的调度

上面说的内容涉及到了线程调度这个词,严格的说是针对软件线程的调度。下面来看看软件调度是什么鬼。由上面硬件线程与软件线程的解释可以看出来,你甚至可以把硬件线程就当成一个实物吧(尽管不合理,但是好理解),cpu内核的四分之一或者是八分之一,甚至你可以把主板啥的拆开反正就是看得到就是啦,至于里面的原理,鬼才晓得反正现在我不想探究。他们由于操作系统的调度器的调度,可能有些时候有的部分会歇着不干活来省电。你可以把硬件线程当做是逻辑处理器。我们所说的软件线程就是靠这些逻辑处理器来运行的。而软件线程你就可以当成一个基本单位,也可以当成一个任务,反正我对这里单位的解释感觉很疑惑,怎么就是单位了疑问要使得cpu去运行这些逻辑起码你得是个线程,至少你有一个线程我才可以运行,否则,你连个整儿(都不算是一单位)的都不算,我运行个屁啊!嗯,一个硬件线程可以运行(处理线程中的逻辑)不止一个软件线程,并且硬件线程的调度器这位大管家还可以控制软件线程该在哪个硬件线程上执行。

那既然这样的话,一般一个进程里面会有1到n个不等的线程。假设一个进程开了32个线程,然后实际上的硬件线程也就8个,,要如何整,平均每个硬件线程要运行四个,要怎么搞?国家包分配哈哈哈,这就引出了线程调度了尴尬

一个进程里面32个线程,反正对于我这枚小白我是挺膈应这么多这么变态的线程数量的,但是事实上现实生活中真的有不少进程真的就有这么多线程。那么为什么要有多线程呢?想象一个场景吧,假设你的手机,而且渣渣处理器只有一个硬件线程,,然而额你有一个开发工作需求,让你实现看电视的同时还下载,,你总不能只用一个软件线程搞吧,如果一个软件线程,会有什么样的下场?要么先下载文件,要么先看电视?不符合需求啊这样!所以把如果只用一个线程就只有懵逼了。那么我就开两个线程吧,一个用来看视频,一个用来下载文件,那么这两个线程就得运行在同一个硬件线程上,试想,如果你不对这两个线程进行调度的话,,那不还是先运行完a线程的逻辑再运行b的逻辑么?这不还是相当于你先干这个后干那个么。这样很明显是不行的!所以一定要有一套东西来控制这两个线程,要怎么走,,能够模拟出我同时走着a的逻辑和b的逻辑这种假象。起码效果是两者都在进行,好在处理器的运行速度贼快,我要不就一会儿运行一下a, 然后再过一会儿再运行b,交替着运行,,这样在宏观上看起来就像是两个线程同时在运行了,反正我处理器运行速度快我就任性,我就频繁的切切切你们愚蠢的人类是绝对感知不到我这个障眼法的哈哈哈,这种策略可以说就是线程调度。网上给的线程调度太官方看着就头疼真是的。。

线程调度的模型(怎么个调度法儿):

1 分时调度模型 2 抢占式调度模型

分时调度:指让所有的线程轮流获得cpu的使用权,而且平均分配每个线程占用的cpu时间片。

抢占式调度:指有限让可运行池中优先级搞得线程占用cpu,如果可运行线程池的优先级相同的话,就随机选择一个线程,使其占用cpu。处于运行状态的线程会一直运行,直至它不得不放弃CPU。 (什么叫不得不放弃骂人这个说法让人无语啊!!)

“不得不放弃”的解释鄙视:一个线程会在一下原因放弃cpu

    1 java虚拟机让当前线程暂时放弃cpu,转到就绪状态,使其他线程获得运行的机会。  (额,,这不就像调度器么)

    2 当前线程因为某些原因而进入阻塞状态

    3 线程结束运行了。

    需要注意的是: 线程的调度不是跨平台的,它不仅仅取决于java虚拟机,还依赖于操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,就不会放弃cpu,在某些操作系统中,即使线程没有遇到阻塞,也会运行一段时间后放弃cpu,给其他线程机会。在java里面,他的线程调度采用的是抢占式调度模型,不是分时的,同时启动多个线程后,不能保证每个线程轮流并且均等的获得cpu时间片。如果希望明确的让一个线程给另外一个线程运行的机会,可以采取下面的方法之一:(这些现在不懂没关系,接下来会讲)

       1让处于运行状态的线程调用 Thread.sleep();

        2让处于运行状态的线程调用Thread.yield()方法

        3让处于运行状态的线程调用另一个线程的join()方法  

二 了解线程

1 第一要搞清楚的问题,如同程序和进程的区别,要了解线程,多线程,第一个要搞清楚的问题就是,线程对象与线程之间的区别。

多线程性能及效率问题

线程对象是可以产生线程的对象,比如java上的thread,Runnable对象。而线程,指的是正在执行的一个指令点序列。在java平台上的指的是从一个线程对象的start()开始,运行run()方法体中的那一段相对独立的逻辑的过程。

那么先从最简单的开始,咱们的main方法

public static void main(String[] args) {
for(int i = 0; i < 100; i ++) {
System.out.println(i + "");
}
}

如果成功编译了该java文件,然后在命令上敲入运行这个文件的指令。那么就会发生  JVM进程被启动,在同一个JVM进程中,有且只有一个进程,就是他自己。然后在这个JVM环境中,所有程序的运行都是以线程来运行的。JVM最先会产生一个主线程,由它来运行指定程序的入口点。在上方的代码中,这个入口就是main方法。当main方法结束后,主线程运行完成,JVM也就会随之退出。我们看到的是一个主线程在运行main方法,这样只有一个线程执行程序的流程我们称之为单线程。这是JVM提供给我们的单线程环境,事实上,JVM底层至少还有垃圾回收这样的后台线程以及其他的非java线程,但是这些线程对我们而言是不可访问的,所以我们就把它认为是单线程就可以。并且主线程是JVM自己开的,在这里并不是由线程对象产生的。在这个线程中,它运行了main方法里面的逻辑。

2 线程的生命周期(这是基础)

Java多线程学习(三)---线程的生命周期

线程是有周期的。当线程被创建并启动后,它既不是一启动就进入执行状态,也不是一直处于执行状态。线程是要经历一个生命周期的, 创建,就绪,运行,阻塞, 死亡。5种状态。尤其是当线程执行后,不可能一直霸占着cpu独自运行的,毕竟还有线程调度,就算线程想这么做,调度机制也不允许,通常进程中是有多线程的,所以cpu需要在多条线程之间切换,于是线程的状态也会多次在运行-->阻塞-->就绪-->运行-->阻塞-->就绪-->运行 中来回切换。下面简述一下这几种状态。

    1 新建状态:当程序使用new关键字创建了一个线程之后,他就立马进入了新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

    2 就绪状态:当线程对象调用了start方法之后,该线程就进入了就绪状态。java虚拟机会为其创建调用栈和程序计数器,等待调度运行。 (线程持有的三个东西, 调用栈,计数器,和寄存器)

    3 运行状态:如果处于就绪状态的线程获得了cpu,开始执行run方法的线程执行体,则该线程就处于运行状态。

    4 阻塞状态:当处于运行状态的线程失去所占用的资源后,就进入了阻塞状态。有一下几个原因会导致线程处于阻塞的状态

        1 线程自己调用了sleep(),进入了睡眠操作

        3 线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能恢复执行(这个 我不太理解)

        4 线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程的notify()或者notifyAll()

        5 线程执行了某些io操作,因为等待相关资源而进入了阻塞状态。比如监听system.in这个输入,但是尚且没有键盘输入,就进入了阻塞的状态。

    5 死亡状态:当run方法正常退出或者一个未捕获的异常终止了闰方法而使线程猝死。就进入死亡状态了

2.1 新建和就绪状态:

当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时他和其他的java对象一样,仅仅由java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现任何线程的动态特征,程序当然也不会执行线程。

当线程对象调用了start()方法之后,该线程就处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有被运行,只是表示该线程可以运行了,至于什么时候开始运行,那就得看JVM线程调度器的调度。就是听天由命吧。。

(注意:启动线程一定要用start()方法,而不是run()方法。永远不要调用线程的run方法,调用start方法,系统就会把run方法当成线程执行体来处理,但是如果直接调用线程对象的run方法,则run方法就相当于一个普通方法,立即执行,并且在run方法返回之前其他线程无法并发执行。需要指出的是,调用了线程的run方法之后,该线程就已经不是新建状态了,所以不要再师徒调用线程的start方法,start方法只是针对新建状态的线程用的,用于把它转为就绪状态,随时等待cpu。如果执意执行st则会爆出IllegalThreadStateException的异常。)

调用了线程对象的start方法之后,该线程立即进入就绪状态--就绪状态相当于等待执行,但是该线程并没有真正的进入运行状态。如果希望调用子线程start方法后子线程立即之情的话,程序可以使用Thread.sleep(1)方法,让当前运行的线程,也就是主线程睡眠1毫秒,很快的,但是这一毫秒之内cpu是不会闲着的,他会去执行另一个处于就绪状态的线程,那么很明显就是刚刚start的线程,这样子线程就能立即执行了。

2.2 运行和阻塞状态

如下情况下,线程将会进入阻塞状态:

1 自己调用了sleep自行阻塞的 2 线程想获得一个同步锁,但是这个锁被别的线程持有时时不释放 3 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。 4 线程在等某个通知 notify 5 程序调用了线程的suspend方法将线程挂起,但这个方法易引起死锁,应当尽量避免使用该方法。

当正在执行线程被阻塞之后,其他线程就可以获得执行机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也会是说,被阻塞的线程的阻塞解除后,必须等待线程调度器重新调用它。

2.3 解除阻塞

针对上面的几种情况当发生如下特定情况可以解除上面的阻塞,让线程重新进入就绪状态:

1 调用sleep导致的阻塞,经过了指定的时间之后就可以进入就绪状态

2 采用阻塞式IO方法导致的阻塞,等那个方法返回就解除阻塞了。

3 线程成功的获得了试图得到的同步锁。(同步监视器)

4 线程正在等待通知的时候,其他的线程发来了这个通知。

5 调用线程的suspend()方法将线程挂起导致的阻塞,被调用了resdme()方法。

从图中可以看出来,当一个线程处于阻塞状态的时候,他的走向只能是变成就绪状态。无法直接变成运行状态的。而就绪状态和运行状态之间的转换通常是不受程序控制的,而是由系统线程调度所决定。当处于就绪状态的线程获得了处理器处理权之后就进入运行状态,当运行状态的线程失去了处理器权(阻塞状态了),该线程就会进入就绪状态,但是有一个方法有例外,调用yield()方法的话,可以让运行状态的线程直接转到就绪状态。稍后将yield()这个方法。

2.4 线程死亡

2.4.1死亡状态

线程会以以下三种状态结束,结束后就处于死亡状态:

1 run() 方法 或者call() 方法执行完成,线程正常结束。

2 线程抛出了一个没有捕获的异常

3 直接调用该线程的stop()方法来结束该线程,该方法很容易导致死锁,通常不推荐使用。

(什么是死锁?)

2.4.2 程序设计

当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来之后,他就拥有和主线程相同的地位,他不会受主线程的影响。为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,阻塞,运行状态的时候就会返回true,当处于新建和死亡状态的时候就会返回false。0并且不要试图对一个已经死亡的线程调用start方法使他重新启动,死亡就是死亡,该线程将不可再次作为线程执行。强制start会抛异常。

        /**
	 * 测试当一个线程死亡的时候,如果再开start方法,可行吗?
	 */
	public void testRestarThread(){
		Thread1 thread1 = new Thread1();	//此时线程是新建状态
		for (int i = 0; i < 300; i++) {
			System.out.println(Thread.currentThread() + "------ i = " + i);
			if (i == 20) {
				thread1.start();//线程处于就绪状态
			}else if (i > 40){	//这个情况下,估计thread1 已经死掉了
				if (!thread1.isAlive()) {	//当线程为就绪,运行, 和阻塞状态的时候这个判断方法会返回true
					thread1.start();//如果线程死了,就试图用这个不合理的方法开
				}
			}
		}
	}
class Thread1 extends Thread{
	public void run() {
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread() + "");
		//运行完run里面的逻辑就算是走完了,线程结束,死亡
	}

}
运行结果:
Thread[main,5,main]------ i = 0
Thread[main,5,main]------ i = 1
Thread[main,5,main]------ i = 2
Thread[main,5,main]------ i = 3
Thread[main,5,main]------ i = 4
Thread[main,5,main]------ i = 5
Thread[main,5,main]------ i = 6
Thread[main,5,main]------ i = 7
Thread[main,5,main]------ i = 8
Thread[main,5,main]------ i = 9
Thread[main,5,main]------ i = 10
Thread[main,5,main]------ i = 11
Thread[main,5,main]------ i = 12
Thread[main,5,main]------ i = 13
Thread[main,5,main]------ i = 14
Thread[main,5,main]------ i = 15
Thread[main,5,main]------ i = 16
Thread[main,5,main]------ i = 17
Thread[main,5,main]------ i = 18
Thread[main,5,main]------ i = 19
Thread[main,5,main]------ i = 20
Thread[main,5,main]------ i = 21
Thread[main,5,main]------ i = 22
Thread[main,5,main]------ i = 23
Thread[main,5,main]------ i = 24
Thread[main,5,main]------ i = 25
Thread[main,5,main]------ i = 26
Thread[main,5,main]------ i = 27
Thread[main,5,main]------ i = 28
Thread[Thread-0,5,main]
Thread[main,5,main]------ i = 29
Thread[main,5,main]------ i = 30
Thread[main,5,main]------ i = 31
Thread[main,5,main]------ i = 32
Thread[main,5,main]------ i = 33
Thread[main,5,main]------ i = 34
Thread[main,5,main]------ i = 35
Thread[main,5,main]------ i = 36
Thread[main,5,main]------ i = 37
Thread[main,5,main]------ i = 38
Thread[main,5,main]------ i = 39
Thread[main,5,main]------ i = 40
Thread[main,5,main]------ i = 41
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Unknown Source)
	at Thread.ThreadDemo.testRestarThread(ThreadDemo.java:15)
	at Thread.ThreadDemoControlTool.main(ThreadDemoControlTool.java:10)

抛异常了吧,start不能随便开的。

3 线程优先级

线程总是存在优先级,并且优先级的范围是在1到10之间,JVM线程调度采用的是基于优先级的抢占式调度机制。在大多数情况下,当前线运行的线程优先级将大于或者等于线程池的任何线程的优先级。但这仅仅是大多数的情况。

注意:当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作时没有保障的,只能把线程的优先级作为一种提高程序效率的方法,但是要保证程序不依赖于这种操作。

当线程池中的线程丢都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候的调用程序的操作方式可能有两种,一种是选择一个线程,直到它阻塞或者运行完成为止。二是分时间片,为线程池的每个线程提供均等的机会。

设置线程的优先级:线程默认的优先级是跟随创建它的线程的优先级,也就是谁带出来的就随谁。我们可以通过setPriority(int priority)更改线程的优先级。例如:

Thread t = new MyThread();
t.serPriority(8);
t.start();

线程的优先级是1到10之间的整数,JVM不会改变一个线程的优先级。然而1到10之间的值是没有保证的。一些JVM可能不能识别10个不同值,而将这些优先级进行每两个或者多个进行合并,变成少于10个优先级,则两个或多个优先级的线程肯能被映射为一个优先级。线程默认的优先级是5,Thread类中有三个常量,定义线程的优先级范围。

static int MAX_PRIORITY //线程可以具有最高的优先级
static int MIN_PRIORITY //线程具有最低的优先级
static int NORM_PRIORITY //分配给线程的默认优先级

4 线程相关代码

4.1线程的创建代码相关

一般有两种 1 直接写一个继承自Thread的子类并重写里面的run方法。 2 或者写一个Runnable, 用的时候作为Thread的初始化参数。 然后创建的时候直接new就行了。

/**
	 * 线程的创建示例
	 */
	public void createThread() {
		//用继承Thread类的那个类
		Thread2 thread1 = new Thread2();
		Thread2 thread2 = new Thread2();
		//用Thread 添加Runnable实现类的方式
		Runnable1 runnable1 = new Runnable1();
		Thread thread3 = new Thread(runnable1);
	
		thread1.start();
		thread2.start();
		thread3.start();
	}
class Thread2 extends Thread{
	public void run() {
		for (int i = 0; i < 30; i++) {
			System.out.println(Thread.currentThread() + "" + i);
		}
	
	}

}

class Runnable1 implements Runnable{
	public void run() {
		for (int i = 0; i < 30; i++) {
			System.out.println(Thread.currentThread() + "" + i);
		}
	}
}


一些常见的问题:

1 线程的名字,一个运行中的线程是有名字的,名字有两个来源,一个是虚拟机给自己的名字,另外一个是编码人员自己定义的名字。在没有指定名字的情况下,虚拟机总会给线程指名字,并且主线程的名字总是main。

2 线程都可以自己设置名字,也可以获取线程的名字。设置名字这方面都可以设置,包括主线程。

3 获取当前线程对象的方法是 Thread.currentThread();这段逻辑是在哪个线程里执行的,他就给哪个线程。

4 在上面的那堆代码中,只能保证每个线程都将启动,每个线程都运行直至完成。一系列线程以某种顺序启动,并不意味这将按照该顺序走他们各自的逻辑。对于任何一组启动的线程来说,调度程序不能保证他们的执行顺序,持续时间也无法保证。所以上面的log图会出现那个样子,一会儿0,一会儿1一会儿2.这个不是我们可以控制的。

5 当目标线程的run()方法执行完后,该线程也就算完成了,进入死亡状态。

6 一旦线程启动,就永远不可能再重启同一个线程。只有一个新鲜的线程才具有被启动的条件,并且只有一次。

7 线程的调度室JVM的一部分,在一个cpu的机器上,实际上同时只能运行一个线程,一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中作为当前线程,可运行线程被选择运行的顺序是没有保障的,怎么个轮法儿要看具体的调度机制。

8 尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成一轮时,它移动到可运行队列的尾部等待,知道它最终排队到该队列的前端位置,他才被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不是以某种有保障的队列顺序来获得运行权这个事实。

4.2 让线程离开运行状态的代码相关

对于人为的阻止线程,在不考虑IO阻塞的情况下,大致有下面三个方面:睡眠,等待,因为需要一个对象的锁定而被阻塞。个人感觉大多是人为的操作,因为达到某种目的故意致使其阻塞,或者是操作不当没有考虑到,不小心致使发生了阻塞。所以想让它阻塞,从上述3个方面入手,

1 睡眠 sleep()

静态方法 Thread.sleep(), 它有两个重载的方法。作用是强制使当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,在苏醒之前他都不会转向就绪状态,而到睡眠时间到期的时候,才会转换为就绪状态。注意这是就绪状态(可运行状态),不是运行状态哈,它要不要执行还得看调度器进一步的分配。所以,睡眠的时间之后执行某些逻辑,这里的时间值在事实上其实是有可能小于真实执行的时间间隔的。

睡眠的实现方法:

try{
Thread.sleep(123);
}catch(InterrruptedException e) {
e.printStackTrace();
}

为了让其他的线程有机会执行,可以将Thread.sleep()的调用放到run()方法里,这样才能保证该线程执行过程中会睡眠。

注意:

1 线程睡眠时帮助所有线程获得运行机会的最好方法。

2 线程睡眠到自然苏醒,并返回到可运行状态,不是运行状态。sleep()中的时间值得是线程不会运行的最短时间。因此,sleep方法不能保证该线程睡眠到期后就一厘二不差的执行。

3 sleep是一个静态方法,只能控制当前正在运行的线程。所以把sleep写到run方法里面也是情有可原的。执行到run方法里面的逻辑就说明当前运行的线程一定是run方法所在的子线程里。

2 通过yield()让线程直接从运行状态回到可运行状态(就绪状态)

Thread.yield()是负责线程让步的。在这里我们先熟悉一下什么是线程让步!让步,很显然就是先让一下的意思。它主要是使分配器重新分配使用权,但是再次分配的使用权也有可能降到自己的头上。

要理解yield(),必须要了解线程优先级的概念。前面已经有了一些介绍。 Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield() 应该做的是让当前线程从运行状态变为就绪状态,以允许具有相同优先级的其他线程获得机会,因此使用yield()的目的就是让具有相同优先级的线程之间能够适当的轮转执行。但是实际中是无法保证yield()达到目的的,因为让步的线程还有可能会被调度机制再次选中。

结论:yield()从没有导致线程转到等待/睡眠/阻塞状况,在大多数情况下,yield()将导致线程从运行状态变为可运行状态,但是有可能没有效果。

3 join() 方法

Thread的静态方法Join()让一个线程B加入到线程A,,或者也可以说A中强行加入B, 等到B执行完之后(注意,即使B里面就算是sleep了,那也得到B执行完!),A就继续走。

thread t = new MyiTread();
t.start();
t.join();

示例:

public void  testJoin() {
		Thread2 thread2 = new Thread2();
		thread2.start();
	}
class Thread2 extends Thread{
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread() + "" + i);
		}
		Thread3 thread3 = new Thread3();
		thread3.start();
		try {
			thread3.join();
		} catch (Exception e) {
			// TODO: handle exception
		}
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread() + " go on.." + i);
		}
	}

}
class Thread3 extends Thread{
	public void run() {
		for (int i = 0; i < 100; i ++) {
			System.out.println(Thread.currentThread() + " 张三 " + i);
		}
	}
}

运行结果: (可以看出两个线程因为这样操作有了顺序)


另外 jion()还有一个带超时限制的重载。jion(int t)。什么意思呢? 就是线程B jion进来了,就立马执行线程B的逻辑,那么线程A的逻辑也就此打断,等到t毫秒以后,反正时间到了,管他B执行没执行完呢,继续从A上次暂停的逻辑处走。

/**
	 * 不好意思,我先占你个300毫秒的时间,做一些事情哈。你要搞什么我不管,但你从现在开始务必给我停上300毫秒!时间一到咱们公平竞争!
	 * @throws Exception
	 */
	public void testJionTime() throws Exception{
		System.out.println("第一行代码时间 == " + System.currentTimeMillis());
		Thread3 thread3 = new Thread3();
		thread3.start();
		//线程里面一执行就让他睡了299毫秒
		//这样就可以看出来,过了这个300毫秒之后,,两个线程的执行,是不是就是按着正常的调度走了。
		thread3.join(300);
		//没猜错的话,这一行估计就是等了300毫秒之后,开始走的
		System.out.println("jion的下一行代码时间 == " +System.currentTimeMillis());
		
		for (int i = 0; i < 30; i++) {
			System.out.println(Thread.currentThread() + " time == " +  System.currentTimeMillis() + " 主线程循环 " + i);
		}
	}



class Thread3 extends Thread{
	public void run() {
		try{
			Thread.sleep(299);	//此处睡了299毫秒,然后主线程设置睡300毫秒。  那么底下的怎么着也得执行个1毫秒吧。
			for (int i = 0; i < 200; i ++) {
				System.out.println(Thread.currentThread() + " time == " + System.currentTimeMillis() + " 张三 " + i);
			}
		}catch(Exception e) {
			
		}
		
	}
}



我非常好奇什么情况下会用join(),,哎呀反正到时候就会用啦,

到目前为止,介绍了线程离开运行状态的3种方法:

1 调用Thread.sleep() 使当前线程睡眠至少指定的毫秒数,尽管他可能在指定的时间之前被中断

2 调用Thread,yield() 不能保障太多事情,尽管通常他会让当前线程回到可运行状态,使得有相同优先级的线程有机会执行。

3 调用 join(): 保证当前线程停止执行,直到该线程所加入的线程回到就绪状态,或者线程执行完成为止。然而如果它加入的线程没有存活,则当前线程不需要停止。

除了以上3种方式,还有下面几种特殊状况可能使线程离开状态:

1 线程的run方法完成

2 在对象上调用wait()方法,(不是在线程上调用)这个稍后有。

3 线程不能在对象上获得锁定,它正视图运行该对象的方法代码。

4 线程调度程序可以决定将当前运行状态移动到可运行状态,以便于让另外一个线程获得运行的机会,而不需要任何理由。

4.3 线程的同步

4.3.1 同步问题的提出。

线程同步主要是为了防止多个线程访问同一个数据对象的时候,对数据造成破坏。因为从一开始我们就讲了,线程自身有的资源很少,大部分是和其他线程共享进程中的资源。

例如两个线程 A 和 B,都操作同一个对象,并且修改这个对象里面的数据。假设有两个线程,想改同一个字符串的内容。看看会有什么后果。

package Thread;

public class ThreadDemo2 implements Runnable {
	Foo foo = new Foo();
	
	public static void main(String[] args) {
		ThreadDemo2 demo2 = new ThreadDemo2();
		Thread t1 = new Thread(demo2);
		Thread t2 = new Thread(demo2);
		t1.start();
		t2.start();
	}
	
	public void run() {
		for (int i = 0; i < 30; i++) {
			foo.fix(30);
			try {
				System.out.println(Thread.currentThread() + " : x = " + foo.getX());
				Thread.sleep(1);
			} catch (Exception e) {
				// TODO: handle exception
			}
			
			
		}
	}
}

class Foo{
	int x = 100;
	public int getX(){
		return x;
	}
	
	public int fix(int y){
		x -= y;
		return x;
	}
}

看见了吧,,,事实上总是超乎你的想象尴尬,,40, -20, -80,怪有规律,还TM双双出现。 想象中的应该是我不管两个线程到底先走谁,,怎么着也得是  30递减啊?!我的目的就是30递减啊!!怎么会是这样的结果?脑子里模拟代码怎么走就明白了,首先要清楚一点,线程这玩意儿,不做专门的控制的话,它可能在任何代码行的执行后,切换!因为这样超乎我们正常思维的执行,所以就有了各种超乎我们判断的运行结果。上面的结果很明显是线程0首先走了一下,但是还没有来得及走到打印log的那行代码,sleep了一下,切了线程,然后线程1就走到给减掉了30, 但是还是没有走到打印线程1log 的时候,又切换线程了,于是线程0就继续走,走到打印log的那行的时候,数据不是减掉30,事实上已经被减掉60了!!所以打印出了这么一波。。

刚刚我们说到了里面有sleep方法的调用。导致切切切。。。那么把这个函数去掉呢??会以30递减吗???我走了一下,结果也挺惨的。。。


反正就是乱了乱了,,针对这种多线程对共享数据操作的,,不好好控制真的容易出五花八门的bug。。对于我这种水平的来说,,真的就是只有我想不出来的,,没有他不产生的...哎!!!

那如果想要保持结果的合理性,我们要怎么做呢?只需要达到一个目的,就是将foo的访问加以限制,每次只有一个线程能访问。这样就能保证foo对象中数据的合理性了。synchronized,熟悉的关键词就是用来解决这种问题的。

在具体的java代码中需要完成以下两个操作:1 把竞争访问的资源 foo.x标示为private 2 同步那些修改变量的代码,使用 synchronize关键字同步方法或代码。

解释:同步和异步。 同步指的是两件事请有先后顺序,必须前者返回的时候我才能进行下去。异步与之相反,就是管你返回来没返回来呢,我继续我的其他事情,你要是返回来了,就给我说一声,我这时候再根据返回的结果做相应的逻辑处理。 这个在安卓编程里面经常用到的,例如 根据从服务端拉下来的数据做手机界面上的更改。  主线程会直接发一个请求消息,然后这个请求消息直至接收数据这堆庞大还可能耗时的逻辑,就直接交给子线程处理了,主线程做的不过就是调了一个发消息的方法而已,,然后管你子线程来没来返回结果的,我继续走我生命周期中该做的其他事情。然后子线程来了消息,就会告知结果返回来了(设置监听器),主线程得知,立马对其进行处理。这样谁都不耽误。。

4.3.2 锁!用来解决上节问题的噩梦级工具!

关于java中的锁的理解(通俗易懂)

java中,每个对象都有一个内置锁!一段synchronized的代码被一个线程执行之前,他首先要拿到执行这段代码的权限,在java里面就是拿到了某个同步对象的锁,如果这个时候同步对象锁被其他线程拿走了,它就只能等待了(线程被阻塞到锁池等待队列中)。取到锁后,他就开始执行同步代码,然后这段代码执行完之后就马上把锁还给被同步的对象,在其他锁池中等待的线程就可以拿到锁执行他们的同步代码了,这样就保证了代码在同一时刻只有一个线程执行。

众所周知:在java多线程编程中,有一个非常重要的问题就是线程的同步问题。关于线程的同步通常有以下几种方法:

1 在需要同步的方法的方法签名中加入synchronized关键字,

2 使用synchronized块对需要同步的代码进行同步。

3 使用JDK 5 中提供的java.util.concurrent,lock 包中的Lock对象

另外为了解决多个线程对同一白能量进行访问时可能发生的安全性问题,我们不仅可以采用同步机制,更可以通过JDK 1.2中加入的ThreradLocal来保持更好的并发性。

1 线程的先来后到

线程的同步机制是靠锁来的概念来控制的,那么在java中,锁是如何体现出来的呢?

这里有必要说一下JVM的内存分配,因为接下来会涉及到这波常识,用来判断什么是属于线程自己资源其他线程不能访问,怎样才确定不同线程请求的是同一把锁:


参考来源:

Java内存图以及堆、栈、常量区、静态区、方法区的区别   

请输入关键词 浅谈java+内存分配及变量存储位置的区别

从几个sample来学习Java堆,方法区,Java栈和本地方法栈

:主要存放在运行期用到的一些局部变量(基本数据类型变量)或者指向其他对象的一些引用,因为方法执行时,被分配的内存就在栈中,所以当然存储的变量就在栈中喽。当一段代码或者一个方法调用完毕后,涉及到的基本数据类型或者对象的引用就会被立即释放。不知道大家有没有记起来前面的一些内容,,好像一个线程在start()的时候,会被分配它拥有的少量资源, 调用栈,寄存器,程序计数器。。如果调用栈和 栈 是同一个东西的话,,,额,,我好想发现了不得了的事情。。

:主要存放java在运行过程中new出来的对象,凡是通过new生成的对象都放在堆中,对于堆中的对象生命周期的管理主要是java虚拟机的垃圾回收机制GC进行回收和统一管理。类的非静态成员变量也存放在堆中,其中基本数据类型是直接保存值,而复杂的类型保存指向对象的引用,非静态成员变量在类的实例化时开辟空间并且初始化。所以要知道类的几个时机, 加载-连接-初始化-实例化 详解请看 Java中类的加载概述和加载时机

静态域:位于方法区的一块内存,存放类中以static声明的静态变量

常量池:位于方法区的一部分内存。存放常量,常量是变量的对立面,意指不可改变的量,说白了就是 final 修饰的那一波。final修饰的基本类型不可改值,final修饰的对象不可改指向它地址的引用,也就是有一个确定的唯一并且不可改的引用。常量池在编译期间就将一部分数据存放于该区域了,包含基本数据类型如 int , long, short, double等以final声明的常量值,和String字符串,特别注意的是对于方法运行期间位于栈中的局部变量String常量的值可以通过String.intern()方法将该值放到常量池中的! 记住String 存到常量池里!

方法区:是各个线程的共享的内存区域,它用于存储class二进制文件,包含了虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。他有个名字叫Non-Heap(非堆),目的是与java堆区分开。

注意,静态域和常量池,被包含于 方法区里。 对于方法区,看下 方法区和常量池 这个文章,尤其是代码例子,挺有意思。

好吧讲完了内存方面,我们把思维从上面的内容里抽回来哈,看下正经的东西。。。让我们从JVM的角度来看看锁的概念,在java运行时的环境中,JVM需要对两类线程共享的数据进行协调。一种是保存在堆中的实例变量,二是保存在方法区中的类变量。这两种数据都是被所有线程共享的,程序不需要协调保存在java栈中的数据,因为这些数据是属于该栈的线程所私有的。

在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的,对于对象来说,相关联的监视器保护对象的实例变量,对于类来说,监视器是保护类的类变量。(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不会监视。)为了实现监视器的排他性监视能力,java虚拟机会为每个对象和类都关联一个锁,代表任何时候只允许一个线程拥有的特权,线程访问实例变量或者类变量不需要锁。但是如果线程获取了锁,那么在他释放这个锁之前就没有其他线程可以获取相同数据的锁了。锁住一个对象就是获取对象相关联的监视器。

锁类实际上用对象锁来实现的,当虚拟机装载一个class文件的时候,他就会创建一个java.lang.Class类的实例。当锁住任何一个对象的时候,实际上就是锁住那个类的class对象。一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每次获得一个锁,就加1,释放一个就减1,当计数器为0的时候,锁就被完全释放了。

编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。在java程序中,我们只需要用上synchronized或者synchronized方法就可以标志一个监视区域,当每次进入一个监视区域时,java虚拟机就会自动锁上类或对象。

当一个有限的资源被多个线程方向的时候,为了保证共享资源的互斥访问,我们一定要给他们哦爱出一个先来后到的规则,而做到这一点,对象锁起到了一个非常重要的作用。

我们对上面的代码demo做一下修改,那种不堪入目的结果就可以改好了。

package Thread;

public class ThreadDemo2 implements Runnable {
	Foo foo = new Foo();
	
	public static void main(String[] args) {
		ThreadDemo2 demo2 = new ThreadDemo2();
		Thread t1 = new Thread(demo2);
		Thread t2 = new Thread(demo2);
		t1.start();
		t2.start();
	}
	
	//用 synchronized 修饰方法,只是个例子,这里修饰的是run方法,实际开发中可别这样用哈
	public synchronized void run() {
			for (int i = 0; i < 30; i++) {
				foo.fix(30);
				try {
					System.out.println(Thread.currentThread() + " : x = " + foo.getX());
					Thread.sleep(1);
				} catch (Exception e) {
					// TODO: handle exception
				}
			}	
	}
}

class Foo{
	int x = 100;
	public int getX(){
		return x;
	}
	
	public int fix(int y){
		x -= y;
		return x;
	}
}

上面的代码只是对run方法进行了synchronized修饰了一下,就改了问题,运行结果如图:


结果终于呈30递减了,那么里面究竟发生了什么呢?首先synchronized如果修饰方法的话,就会把该方法所在的类的对象中的锁要过来,,然而main方法中,虽然是两个线程,但是在其初始化的时候,往里面塞的Runnable实现类对象是同一个对象!这就意味着,这两个线程执行run方法的时候,其所在的对象即使可能有不一样的引用,但是指向的地址是同一个对象,那么锁就不言而喻了,同一把锁!而且是Runnable实现类的锁!注意了啊,要是线程同步,你必须得保证锁是同一把!然而一把锁就对应了一个唯一的确定的对象,所以用synchronized的时候,你得确定不同的线程要获取的是同一把锁才有效!

还有第二种方法,看看:

package Thread;

public class ThreadDemo2 implements Runnable {
	Foo foo = new Foo();
	
	public static void main(String[] args) {
		ThreadDemo2 demo2 = new ThreadDemo2();
		Thread t1 = new Thread(demo2);
		Thread t2 = new Thread(demo2);
		t1.start();
		t2.start();
	}
	
	public void run() {
		synchronized (foo) {
			for (int i = 0; i < 30; i++) {
				foo.fix(30);
				try {
					System.out.println(Thread.currentThread() + " : x = " + foo.getX());
					Thread.sleep(1);
				} catch (Exception e) {
					// TODO: handle exception
				}
			}	
		}					
	}
}

class Foo{
	int x = 100;
	public int getX(){
		return x;
	}
	
	public int fix(int y){
		x -= y;
		return x;
	}
}

看出把synchronized的修饰位置变了,改为了run方法里面。看下运行结果:


也是一种理想的结果哈!但变了一个位置,是否就表示和上面修饰方法一个含义呢?其实是不一样了,,两个线程需要的锁还是同一把,但不是Runnable实现类的,而是Foo的!

那么Synchronized的修饰范围如何控制呢?我把synchronized修饰的范围再缩小一波,搞到for循环里面看看有什么结果。我感觉也可以。。看看哈:

package Thread;

public class ThreadDemo2 implements Runnable {
	Foo foo = new Foo();
	
	public static void main(String[] args) {
		ThreadDemo2 demo2 = new ThreadDemo2();
		Thread t1 = new Thread(demo2);
		Thread t2 = new Thread(demo2);
		t1.start();
		t2.start();
	}
	
	public void run() {
			for (int i = 0; i < 30; i++) {
				//for循环里面加同步代码块
				synchronized (foo) {
					foo.fix(30);
					try {
						System.out.println(Thread.currentThread() + " : x = " + foo.getX());
						Thread.sleep(1);
					} catch (Exception e) {
						// TODO: handle exception
					}
				}					
			}
	}
}

class Foo{
	int x = 100;
	public int getX(){
		return x;
	}
	
	public int fix(int y){
		x -= y;
		return x;
	}
}

运行结果:


正常的。这时候你可能会好奇了,,synchronized修饰的是for循环里面的数据,,,那for循环里面不是有个i = 0; i    < 30; i ++ 么?这个不在synchronized修饰的范围内啊??多线程怎么没有把i给乱改啊?原因是i 它被存到了哪里!它是不是线程之间的共享数据!这个for循环里面的 i 显然就是运行时这个方法时临时产生的值,既不属于类的成员变量,也不是常量,静态变量,,这种显然就是栈里存的数据。那就好说了,栈里的数据对于线程是私有的,相当于各个线程人手一份互不影响。所以i这个数据绝对是线程安全。所以这样改的结果,打印出来也是对的。

关于锁和同步,有以下几个要点:

1)每个对象只有一个锁,当提到同步时,应该清楚在什么上同步,也就是说要的是谁的锁。

2)一个类里,可以有同步方法 和 非同步方法,非同步方法是可以被多个线程自由执行而不受锁的限制的。

3)线程睡眠时,它所持有的锁不会被释放!

4)线程可以获得多个锁。比如,在一个对象的同步方法里面调用了另外一个对象的同步方法,这样就获得了两把锁。

5)同步损害并发性,应该尽量的缩小同步的范围,同步不但可以同步整个方法,还可以同步方法中的一部分代码,到底要拿谁的锁要搞清。

4.3.3 静态方法同步

要同步静态方法,需要一个用于整个类对象的锁,这个对象就是这个类 (xxx.class),上了锁,程序中所有这个类的对象,,都会同步。

例如:

public static synchronized void setName (String name) {
Xxx.name = name;
}

//等价于
public static void setName (String name) {
synchronized(Xxx.class) {
Xxx.name = name;
}
}
4.3.4 如果线程不能获得锁会怎么样

如果线程试图进入同步方法,但是其锁已经被占用,则线程在该对象上被阻塞。实质上,线程对象进入该对象的一种池中,锁池,必须在那里等着,直到锁被释放,该线程才会变成可运行状态。

当考虑阻塞时,一定要注意用的是哪个对象的锁:

1 调用同一个对象中的非静态同步方法的线程将被阻塞,但是如果输不同对象的话,则他们执行同步方法的时候,持有的锁也是不同对象的,线程之间就不会互斥。

2 调用同一个类的静态同步方法的不同线程将会阻塞,因为锁是同一个,都是class对象

3 静态同步方法和非静态同步方法调用,永远不会彼此阻塞,因为锁不同!

4 对于同步代码块,要清楚什么对象被锁定,即synchronized后面括号里面的参数,在同一个对象上进行同步的线程将彼此阻塞,不同对象锁定的线程将永远不会阻塞。

4.3.5 何时需要同步

在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改它。

对于非静态字段中可更改的数据,通常使用非静态方法访问。

对于静态字段中可更改的数据,通常使用静态方法访问,仔细想也明白,针对这类数据如果用平常的方法同步的话,假设该类同时有好几个对象,再来个不同的线程一个操作一个对象,,这样的话他们就能同时篡改这个静态变量了。。白瞎。。所以真正的保护是写个同步的静态方法去操作这个静态数据,,因为拿到的锁是针对这个类的锁,这样在同一个进程中,所有该类的对象在运行到持有类锁的同步代码时,就可以全部被封死了。

如果需要在非静态方法中使用静态字段,或者静态字段中使用非静态方法,问题就变得非常复杂了。。

4.3.6 线程安全的类

当一个类已经很好的同步以保护它的数据时,这个类就称为线程安全的。但是即使是线程类,使用起来就一定线程安全吗?不一定,应该特别小心的用,因为操作的线程间仍然不一定安全。

举个例子,其实这种例子在开发工作中也遇到过类似的:

比如一个集合是线程安全的,有两个线程操作同一个集合对象,当一个线程查询集合非空后,删除集合中所有的元素,第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出了集合非空,但是当第一个线程执行清除后,第二个再执行删除明显是不对的,因为此时集合已经为空了。。这种现象,可以说是即使你保证了原子级操作的线程安全性,但是原子与原子间究竟发生了什么是不可得知的,更何谈其之间多线程操作的安全性。  那么这种情况解决方法是 线程对集合操作的时候,加同步,锁是集合的锁。就可以解决。

4.3.7 死锁

死锁对于java程序来说是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程的时候就会发生死锁。前面我们有提到一个事情, 同步代码块中的逻辑,即使一个线程sleep了,也不会释放它持有的锁。

举一个牵强的例子:

package Thread;

public class SiSuoDemo {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		A a = new A();
		a.age = 0;
		a.name = "name";
		a.sex = 1;
		
		B b = new B();
		
		A2BThread a2bThread = new A2BThread(a, b);
		B2AThread b2aThread = new B2AThread(a, b);
		
		a2bThread.start();
		b2aThread.start();
	}
}

class A {
	public String name = "";
	public int age = 0;
	public int sex = 0;
	
	public String toString() {
		return "now is A and name = " + name + " age = " + age + " sex = " + (sex == 0 ? "男" : "女");
	}
}

class B extends A {
	public String adress = "";
	public String phoneNumber = "";
	
	public String toString() {
		return "now is B and name = " + name + " age = " + age + " sex = " + (sex == 0 ? "男" : "女") 
				+ " adress = " + adress + " phonenumber = " + phoneNumber;
	}
}

class B2AThread extends Thread{
	private A a = null;
	private B b = null;
	public B2AThread(A a, B b) {
		this.a = a;
		this.b = b;
	}
	
	public void run(){
		try {
			synchronized (b) {
				Thread.sleep(1);	//此时强制睡了1毫秒,意在使其切换另一个线程,要记得即使sleep了,该线程持有的锁是不会释放的
				synchronized (a) {
					if (b != null && a != null) {
						a.name = b.name;
						a.age = b.age;
						a.sex = b.sex;
						System.out.println(a);
					}
				}
			}
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

class A2BThread extends Thread{
	private A a = null;
	private B b = null;
	public A2BThread(A a, B b) {
		this.a = a;
		this.b = b;
	}
	
	public void run() {
		try {
			synchronized (a) {
				Thread.sleep(1); //此时强制睡了1毫秒,意在使其切换另一个线程,要记得即使sleep了,该线程持有的锁是不会释放的
				synchronized (b) {
					if (b != null && a != null) {
						b.adress = "xxx";
						b.phoneNumber = "xxx";
						b.age = a.age;
						b.name = a.name;
						b.sex = a.sex;
						System.out.println(b);
					}
				}
			}
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

运行结果是什么都没打印出来。因为死锁了。两个线程都因为得不到自己期待的锁,都阻塞着不往下走。有没有注意到 里面很牵强的加上了sleep(), 这句来模拟一种很特殊的情况,即,恰好一方刚获取了两个锁的第一个锁的时候,切换线程了!

让两个 A2BThread 获得了a的锁之后,就立马睡眠使其阻塞,这样就会很快的切到另一个线程了,然后另一个线程,也就是B2AThread,在执行run方法的时候,首先获取了b的锁,然后又睡,,这样就继续执行 A2BThread,,下一行,A2BThread就要索要a的锁了,,但是刚才B2A线程已经获取了a的锁了,,所以 A2BThread一定会获取失败的,那就阻塞吧,等别的线程把a的锁还回来就可以继续运行了,于是阻塞又切回了 B2A,,但是B2A的下一行就是获取 a 的锁,但是此时a这个锁正在被A2B这个阻塞线程持有着呢,是不会得到这个锁的,于是B2A也采取阻塞的方式以求另一个线程释放。。。于是就进入了一个死循环中,,二者永远也得不到自己想要的锁,因为二者都正在持有着他们需要的锁。死锁就出现了。

但是如果不用sleep方法导致且线程的话,,实际情况下是不太容易出现这种一行都打印不出来的结果的,,,当然,去掉sleep()后,可能输出的结果是对的,也可能是不对的。都有可能。我咋感觉有点像量子力学的某些理论,,随机啊随机。。。。

但是无论代码中发生死锁的概率有多小,一旦发生死锁,程序就会死掉。有一些方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。自行百度吧,,困死我了。。

4.3.8 线程同步小结

1 线程同步的目的是为了保护多个线程访问同一个资源时对资源的破坏。

2 线程同步方法是通过锁来实现的,每个对象有且只有一个锁,这个锁与其对应的对象关联,线程一旦获得了对象锁,其他访问该对象的线程就因为拿不到锁无法再访问该对象其他的同步方法。

3 对于静态同步方法,线程所获取的锁是针对这个类的锁,锁对象是class对象。静态和非静态 同步方法的锁互不干预。一个线程获得锁,当在一个同步方法中方位另外一个对象的同步方法的时候,就会顺带获取后者对象的锁。于是就有两个锁。

4 对于同步,要清楚是在哪个对象上同步的,这个十分关键。

5 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全作出正确的判断,对原子操作作出分析,并保证原子操作期间别的线程无法访问竞争资源。

6 当多个线程等待一个对象锁时,没有获得到锁的线程将会发生阻塞。

7 死锁是线程间相互等待锁造成的,在实际情况下发生的概率很小,真让你写一个死锁的程序,也不一定会好使,但是一旦程序发生了死锁,程序会挂掉。

4.4 线程的交互

4.4.1 线程交互的基础知识

首先要从java.lang.object的类的三个方法学习

void notify():唤醒此对象监视器上等待的单个线程

void notifyAll(): 唤醒此监视器上等待的所有线程。

void  wait():导致当前的线程等,并立马放弃同步代码块被同步对象的锁,直到其他线程调用此对象的notify() 或者 notifyAll(). 并且wait的调用对象也得是被锁住的那个对象。(这个是后来发现的规则)

另外 wait还有两个很重要的重载方法:

void wait(long timeout) :导致当前的线程等待,直到其他线程调用此线程此对象的notify()方法或者notifyAll()方法,或者超过指定的时间量。

void wait(long timeout, int nanos): 导致当前的线程等待直到其他线程调用此对象的notify() 或者 notifyAll() 方法,或者超过某个实际时间量,或者其他线程中断当前线程。

线程不能在平常的情况下调用对象上等待或者通知的方法,除非这个线程持有该对象的锁。所以 wait , notify, notifyAll 只能在同步环境中用!

wait, notify, notifyall 都是object 的实例方法。正如每个对象的锁一样,每个对象也有一个线程列表,他们等待来自该信号通知。线程通过执行对象上的wait方法获得这个等待列表,从那时起,他就不再执行任何其他指令,直到调用对象的notify方法位置。如果多个线程在同一对象上等待,则只选择一个线程继续执行。如果没有线程等待,则不采取任何特殊操作。调用它的对象一定要一致。

注意哈,wait 和 notify 搭配使用的话,可以定要搞清锁住的对象。

例子:

package Thread;

public class ThreadWaitDemo {
	public static void main(String[] args) {
		Object s = "";	//为了测试执行wait对象的时候,导致当前线程等待之外,,放弃的那把锁到底是谁的锁,
		MyThread thread = new MyThread(s);
		synchronized (thread) {		//锁住了thread对象的锁
			try {
				System.out.println("开始等待计算");
				thread.start();
//				s.wait();
				thread.wait();
				System.out.println("等待结束, total = " + thread.total);
			} catch (Exception e) {
			}
			
		}
		
	}

}

class MyThread extends Thread {
	Object string = null;
	MyThread(Object s){
		string = s;
	}
	public int total = 0;
	public  void run() {
		synchronized (this) {
			for (int i = 0; i < 101; i++) {
				total += i;
				System.out.println(i+"");
			}
			notify();
//			string.notify();
		}	
	}
}



哎,,,写代码的时候,,感觉到坑了!!果然实践很重要,会带出很多思维。什么坑呢??我写的时候,搞不清main 方法里的synchronized 括号里要的那把锁,,和 这一块的同步代码里 thread.wait(),,,为什么是thread调用wait??反正就是一个令当前线程放弃锁的操作,为什么偏偏调用thread的?和 synchronized括号里面的参数难道有什么规矩必须保持一致?还有好多其他的问题,主要集中于 notify 和 wait 之间调用,是不是有一个潜规则,不写就报错。。事实上我就这点试了一下,,的确报错了。 比如,我把main方法里面的thread.wait()方法注掉改为了 s.wait(),  同时把 MyThread 的run方法里面的notify改成了同一个对象的notify(),要凑凑一对,想着反正wait就是暂停当前线程并还线程所在锁的,,即使我用s调出wait(),那也得放弃锁。。运行了一下,可以确定 s.wait() 方法执行了之后,主线程的确放弃了synchronized所持有的那把 thread 的锁,并且执行了 MyThread的run方法,但是但是,,string.notify()执行之后,,就仿佛没了消息!!最后一句“等待结束,,”死活打印不出来。所以推断出 wait 和 notify 肯定有一套规则!但是我这里看不到源码我的天。。咋整。


好吧,native的。。。

使用wait方法和notify方法用于线程间通信的正确姿势 这篇文章,可以说是条条命中!

嗷嗷我明白了,经过查阅,,wait, notify 这类方法, 所在的同步代码块,锁了谁,就应该用谁调!!!当在对象上调用wait()方法时,执行该代码的线程立即放弃这个线程持有的锁,然而调用notify时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在移除之前都不会放弃锁。因此,notify并不意味着这时的锁变得可用。

例子:

/**
 * 要求:用wait 和 notify 确保两个线程一定先递加加后递减! 拒绝使用jion解决
 * @author forev
 *
 */
public class ThreadWaitDemo {
	public static boolean isAddOver = false;	//用于标志递加线程是否执行完毕。
	
	public static void main(String[] args) {
		NumberGame game = new NumberGame();
		game.setAddTimes(100);
		game.setSubtractTimes(100);
		SubtractThread sThread = new SubtractThread(game);
		AddThread aThread = new AddThread(game);
		
		sThread.start();
		aThread.start();
		
	}

}

//递减线程
class SubtractThread extends Thread {
	private NumberGame game= null;
	public SubtractThread(NumberGame g) {
		this.game = g;
	}
	public void run() {
		synchronized (game) {
			try {
				//这里需要有个大大的细节要注意!!!一定要在while循环里面判断,不要用if!不要用if!不要用if!重要的事情说。。
				while (!ThreadWaitDemo.isAddOver){	
					game.wait();
				}
				game.subtract();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
			
	}
}

//递加线程
class AddThread extends Thread {
	private NumberGame game= null;
	public AddThread(NumberGame g) {
		this.game = g;
	}
	public void run() {
		synchronized (game) {
			game.add();
			game.notify();
			ThreadWaitDemo.isAddOver = true;
		}
			
	}
}

class NumberGame {
	private int total = 0;
	private int addTimes = 0;
	private int subtractTimes = 0;
	
	public void setAddTimes(int addTime) {
		this.addTimes = addTime;
	}
	
	public void setSubtractTimes(int st) {
		this.subtractTimes = st;
	}
	
	public int getTotal(){
		return total;
	}
	
	// 递加  线程安全
	public void add(){
		synchronized (this) {
			for (int i = 0; i <= addTimes; i++) {
				total += i;
				System.out.println("add......" + total);
			}
		}
	}
	//递减 线程安全
	public void subtract() {
		synchronized (this) {
			for (int i = subtractTimes; i >= 0; i--) {
				total -= i;
				System.out.println("subtract......" + total);
			}
		}		
	}
	
}

多次的运行结果:


4.4.2 多个线程等待一个对象锁时要用notifyAll()

多数情况下,最好通知某个对象的所有线程,如果这样做,可以在对象上使用 notifyAll()让所有在此对象上等待的线程冲出等待区,回到可运行状态。

4.4.5 线程的调度-休眠

Java线程调度室java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。这里要明确一点,不管程序员如何编写调度,只能最大限度的影响执行的次序,而不能做到精准控制。

线程休眠是使线程让出cpu的最简单方法之一,线程休眠的时候,会将CPU资源交给其他的线程,以便能轮换执行,当休眠一定时间之后,线程就会苏醒,进入准备状态等待。例子前面好像有。

4.4.6 线程的调度-合并

线程合并的含义是将几个线程合并为一个单线程执行,应用场景是当一个线程必须等另一个线程执行完毕才能执行时可以使用join方法。前面有例子。

4.4.7 守护线程:

守护线程基本上与普通线程没有啥区别调用线程的setDaemon(true),则可以将其设置为守护线程,守护线程的使用情况比较少,但并非无用,举例来说,JVM的垃圾回收,内存管理等线程都是守护线程。还有就是做数据库应用的时候,使用的是数据库连接池,连接池本身也包含很多后台线程,监控连接的个数,超时时间,状态等。

setDaemon方法详细说明: 

public final void setDaemon(boolean on)将线程标记为守护线程,当正在运行的线程都是守护线程的时候,JVM就会退出了。

public class DaemonThreadDemo {
	public static void main(String[] args) {
		
		DaemonThread thread = new DaemonThread();
		thread.setDaemon(true);
		thread.start();
		
		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread() + " i=" + i);
			try {
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}

}

class DaemonThread extends Thread {
	public void run() {
		for (int i = 0; i < 1000; i ++) {
			System.out.println(Thread.currentThread() + " i=" + i);
			try {
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

运行结果:


注意看,,当主线程执行完之后, 守护线程没执行几句就结束了。因为剩下的只有守护线程了,JVM会退出的。

4.4.8 生产者消费者模型

对于多线程的程序来说,不管任何语言,生产者和消费者模型都是最经典的。就像学习每一门语言一样,hello world, 都是最经典的例子。

实际上,准确的来说应该是 生产者-消费者-仓储, 离开了仓储,生产者消费者模型就显得没有说服力了。

1 生产者仅仅在仓储未满的时候进行生产,仓满则停止生产。

2 消费者仅仅在仓储有产品的时候才会消费,仓空则等待。

3 当消费者发现仓储没产品可以消费的时候,就会通知生产者生产

4 生产者在生产出可消费产品的时候,应该通知等待的消费者去消费。

例子:

// 模拟生产者消费者模式
public class ProduceConsumeDemo {
	public static void main(String[] args) {
		ZhiZaoChang zhi = new ZhiZaoChang(80);
		ProduceThread produceThread = new ProduceThread(zhi);
		produceThread.setDaemon(true);	//设置为守护线程,当消费者买完了东西,就不生产了
				
		ConsumeThread consume1 = new ConsumeThread(zhi, 50);
		ConsumeThread consume2 = new ConsumeThread(zhi, 80);
		ConsumeThread consume3 = new ConsumeThread(zhi, 60);
		ConsumeThread consume4 = new ConsumeThread(zhi, 10);
		ConsumeThread consume5 = new ConsumeThread(zhi, 20);
		ConsumeThread consume6 = new ConsumeThread(zhi, 90);
		ConsumeThread consume7 = new ConsumeThread(zhi, 50);
		ConsumeThread consume8 = new ConsumeThread(zhi, 90);
		ConsumeThread consume9 = new ConsumeThread(zhi, 40);
		ConsumeThread consume10 = new ConsumeThread(zhi, 35);
		ConsumeThread consume11 = new ConsumeThread(zhi, 99);
		ConsumeThread consume12 = new ConsumeThread(zhi, 79);
		ConsumeThread consume13 = new ConsumeThread(zhi, 45);
		
		produceThread.start();
		
		consume1.start();
		consume2.start();
		consume3.start();
		consume4.start();
		consume5.start();
		consume6.start();
		consume7.start();
		consume8.start();
		consume9.start();
		consume10.start();
		consume11.start();
		consume12.start();
		consume13.start();
	}
}

class ZhiZaoChang{
	public static final int MAX = 100;
	private int mProductNumber = 0;
	
	public ZhiZaoChang(int nowNumber) {
		this.mProductNumber = nowNumber;
	}
	
	public void produce() {
		while (true) {	//加一个死循环,以便于可以随时等待时间片的分配
			synchronized (this) {
				while (mProductNumber < MAX) {
					mProductNumber = MAX; 	//补满货
					System.out.println(Thread.currentThread() + "   生产完了,补满货了");
					notifyAll();
				}
			}
			try {
				Thread.currentThread().sleep(1);	//强制睡眠,以便于切换到其他的线程
			} catch (Exception e) {
				// TODO: handle exception
			}			
		}
	}
	
	public void consumer (int needNumber) {
		synchronized (this) {
			try {
				while (mProductNumber < needNumber) {
					System.out.println(Thread.currentThread() + "   货不够了, 需要消费" + needNumber 
							+ "......余货还有:" + mProductNumber);
					//缺货了,赶紧腾出来锁,让其他的线程用,如果此时恰好有一个其他的消费者获得了这把锁,但是他也发现缺货了,
					//那么它也会走同样的逻辑,把锁甩出,让其他的线程用,如果有一个线程也是消费者线程,但是它购买的量比较少,余货
					//正好够,他就会再消费一波。然后再甩出锁,来回几次剩下的只有都甩出过锁只等notify的消费者线程,和
					//那个生产者线程了,反正总会轮到它!它进行补货,补完就发个通知告诉大家,有货了,继续排队买哈。
					wait();		
				}
				mProductNumber -= needNumber;
				System.out.println(Thread.currentThread() + "   消费完了, 消费了" + needNumber 
						+ "......余货还有:" + mProductNumber);
			} catch (Exception e) {
				// TODO: handle exception
			}	
		}
	}
}

class ConsumeThread extends Thread{
	private ZhiZaoChang zhiZaoChang = null;
	private int needNumber = 0;
	
	public ConsumeThread(ZhiZaoChang zzc, int number) {
		this.zhiZaoChang = zzc;
		this.needNumber = number;
	}
	public void run() {
		zhiZaoChang.consumer(needNumber);
	}
}

class ProduceThread extends Thread {
	private ZhiZaoChang zhiZaoChang = null;
	
	public ProduceThread(ZhiZaoChang zhi) {
		this.zhiZaoChang = zhi;
	}
	
	public void run() {
		zhiZaoChang.produce();
	}
}
	
	

运行结果:



这样下来,所有的消费者线程都要到货了,并且数据还有条不紊。

需要注意的是:notifyAll 起到。的作用就是一个通知作用,不释放锁!得等同步块执行完后才会释放锁,或者粗暴点儿,直接wait,但是上面代码生产者可别用wait哈!所以notifyAll也就是告诉所有线程,你们wait那么久终于有结果了,都醒醒来等锁吧!!

4.4.9 volatile关键字

java 语言包含两种内在的同步机制,同步块(或方法)和 volatile变量。这两种机制的提出都是为了实现代码的安全性。其中Volatile变量的同步性比较差,但是有时候它更简单并且开销更低,并且使用的时候更容易出错。

谈及到volatile关键字,不得不提一篇文章, 《Java理论与实践:正确使用volatile变量》,这篇文章对volatile关键字的用法做出了相当精辟的阐述。

之所以要单独提出volatile这个冷门关键字原因是它在高性能的多线程程序中有很重要的用途,只是这个关键字用不好会出很多问题。

首先考虑一个问题,为什么变量需要volatile来修饰呢?

首先要搞清楚这个问题,首先应该凝白计算机内部做了什么,比如做了一个i ++ 操作,计算机内部处理了3次, 读取-修改-写入。

同样,对于一个long型数据,做了个赋值操作,在32位系统下需要两步才能完成,先改低32位,然后改高32位。

假想一下,当将以上的操作放到多线程环境下操作,就可能出现的问题是,这些步骤执行了一部分,而另外一个线程已经引用了变量值,这样就导致了读取脏数据的问题。

通过这样的设想下,就不难理解volatile关键字了。 volatile可以用在任何变量的面前,但是不能用于final变量的前面,因为final型的变量是禁止修改的,也不存在线程安全的问题。

4.4.10 线程池及新特性:

sun在java5中,对java线程中的类库做了大量的扩展,其中线程池就是java5的新特性之一,除了线程池之外还有很多线多线程相关的内容,为多多线程的编程带来了极大的便利,为了编写搞笑稳定可靠的多线程程序,线程部分的新增内容显得尤为重要。

有关java5线程新特性的内容全部在java.util.concurrent下面,里面包含了数目众多的接口和类,熟悉这部分API特性是一项艰难的学习过程。

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面放了众多未死亡的线程,池中线程执行调度由池管理器来处理。当有线程任务时, 从池中取出一个,执行完后线程对象归池,这样就可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

在java5之前,要实现一个线程池是相当有难度的,现在java5位我们做好了一切,我们只需要按照提供的API来使用,即可享受线程池给我们带来的极大便利。

线程池分好多种,固定尺寸线程池,可变尺寸线程池

在使用线程池之前,必须知道如何去创建一个线程池,需要了解的是java.util.concurrent.Executors类的API,这个类提供了大量的创建线程池的静态方法。

1) 固定大小的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class ThreadPoolTest {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(2);
		
		MyThread3 t1 = new MyThread3();
		MyThread3 t2 = new MyThread3();
		MyThread3 t3 = new MyThread3();
		MyThread3 t4 = new MyThread3();
		MyThread3 t5 = new MyThread3();
		MyThread3 t6 = new MyThread3();
		MyThread3 t7 = new MyThread3();
		
		pool.execute(t1);
		pool.execute(t2);
		pool.execute(t3);
		pool.execute(t4);
		pool.execute(t5);
		pool.execute(t6);
		pool.execute(t7);
	}
}

class MyThread3 extends Thread{
	public void run() {
		System.out.println(Thread.currentThread() + "......");
	}
}


2)单任务线程池

 改革方法就行了



对于以上两种线程池,大小都是固定的,当要加入的池的线程或者任务超过池的最大尺寸的时候,则入次线程需要排队等待。一旦线程池中有线程完毕,则排队等待的某个线程就会入池执行。

3 可变尺寸的线程池


4 延迟线程池



5 单任务延时连接池




额,,上面的例子发现忘了调用 pool.shutdown(); 了,,不调用的话程序是不会结束的。

6 新定义,有返回值的线程

在java5之前,线程是没有返回值的,常常为了有返回值而费尽周折,并且代码很不好写,或者干脆绕过这道坎,走别的路了。现在java终于有可返回值得任务了。可返回值的任务必须实现callable接口,类似的,无返回值的任务必须Runnable接口,执行完Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到callable任务返回的object了。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class CallableDemo {
	public static void main(String[] args) {
		ExecutorService service = Executors.newFixedThreadPool(2);
		
		MyCallable callable1 = new MyCallable("A");
		MyCallable callable2 = new MyCallable("B");
		
		Future fu1 = service.submit(callable1);
		Future fu2 = service.submit(callable2);
		
		try {
			System.out.println(fu1.get().toString());
			System.out.println(fu2.get().toString());
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ExecutionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		service.shutdown();
		
	}
}

class MyCallable implements Callable{
	private String s = "";
	
	public MyCallable (String s) {
		this.s = s;
	}

	public Object call() throws Exception {
		return s;
	}
	
}



7 锁类

在java5中,专门提供了锁对象,利用锁可以方便的实现资源的封锁,(我靠它要锁谁,封锁的是谁啊?),用来控制对竞争资源并发的访问的控制,这些内容主要集中在Java.util.concurrent.lock包下面,里面有三个重要的接口,Condition, Lock, ReadWriteLock。

Condition:它将object类的监视器方法(wait(), notify(), notifyAll())分解成截然不同的对象,以便通过这些对象与任意Lock实现组合使用, 为每个对象提供多个等待,set(wait-ser)。

Lock:Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作。

ReadWriteLock: 维护了一对相关的锁定,一个用于只读操作,一个用于写入操作。


例子:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class LockDemo1 {
	public static void main(String[] args) {
		Money m = new Money();
		Lock lock = new ReentrantLock();
		ControlMoneyThread thread1 = new ControlMoneyThread(m, 100, lock);
		ControlMoneyThread thread2 = new ControlMoneyThread(m, 1000, lock);
		ControlMoneyThread thread3 = new ControlMoneyThread(m, -345, lock);
		ControlMoneyThread thread4 = new ControlMoneyThread(m, -456, lock);
		ControlMoneyThread thread5 = new ControlMoneyThread(m, 321, lock);
		ControlMoneyThread thread6 = new ControlMoneyThread(m, 100, lock);
		
		ExecutorService pool = Executors.newCachedThreadPool();
		pool.execute(thread1);
		pool.execute(thread2);
		pool.execute(thread3);
		pool.execute(thread4);
		pool.execute(thread5);
		pool.execute(thread6);
		pool.shutdown();
	}
}

class Money{
	private int money = 0;
	
	private int getMoney(){
		return money;
	}
	
	public void addMoney(int m) {
		money += m;
		System.out.println(Thread.currentThread() + " 涉及金额=" + m + " 当前余额是: " + money);
	}
}

class ControlMoneyThread extends Thread {
	private Money m = null;
	private int change = 0;
	private Lock lock = null;
	
	public ControlMoneyThread(Money m, int changeCount, Lock lock) {
		// TODO Auto-generated constructor stub
		this.m = m;
		change = changeCount;
		this.lock = lock;
	}
	
	public void run() {
		// TODO Auto-generated method stub
		lock.lock();
		m.addMoney(change);
		lock.unlock();
	}
}


一定要注意的是,在获取了锁的对象之后,用完之后一定要尽快的释放锁!以便于等待该锁的线程有机会执行。

在上文中提到了Lock接口及对象,使用它,可以很优雅的控制竞争资源的安全访问,但是这种锁是不区分读写的,这种锁我们称之为普通锁。为了提高性能,java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,在一定程度上提高了程序的执行效率。

java并发编程系列之ReadWriteLock读写锁的使用

java中有读写锁有个接口java.util.concurrent.locks.ReadWriteLock, 也有具体的实现ReentrantReadWriteLock,详细的API可以查看API文档

public class ReadWriteLockDemo1 {
	
	public static void main(String[] args) {
		final ReadWriteLockDemo1 demo = new ReadWriteLockDemo1();
		Thread thread = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		Thread thread1 = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		thread.start();
		thread1.start();
	}
	
	public synchronized void get (Thread thread) {
		System.out.println("start time = " + System.currentTimeMillis());
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(20);
			} catch (Exception e) {
				// TODO: handle exception
			}
			System.out.println(thread.getName() + "正在进行读操作......");
		}
		System.out.println(thread.getName() + "读操作完毕");
		System.out.println("end time:" + System.currentTimeMillis());
	}
	
}

我们可以看到,即使是在读取文件,加了synchronized关键字之后,读与读之间也是互斥的,也就是说必须等到Thread-1读完以后,才会轮到Thread-0执行,而无法做到同时读文件,这种情况下如果存在大量的线程需要同时读文件的话,读写锁的效率就体现出来了,它的效率要明显高于synchronized关键字的实现。

import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReadWriteLockDemo1 {
	
	//添加读写锁
	ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	
	public static void main(String[] args) {
		final ReadWriteLockDemo1 demo = new ReadWriteLockDemo1();
		Thread thread = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		Thread thread1 = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		thread.start();
		thread1.start();
	}
	
	public void get (Thread thread) {
		//添加锁
		lock.readLock().lock();
		try {
			System.out.println("start time = " + System.currentTimeMillis());
			for (int i = 0; i < 5; i++) {
				try {
					Thread.sleep(20);
				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println(thread.getName() + "正在进行读操作......");
			}
			System.out.println(thread.getName() + "读操作完毕");
			System.out.println("end time:" + System.currentTimeMillis());
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			//释放锁
			lock.readLock().unlock();
		}
		
	}
	
}

可以看到效率高了近一倍

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他的线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,如果一个线程已经占用了写锁,则其他的线程如果申请写锁或者读锁,则申请的线程会一直等待当前写锁的释放,读锁和写锁是互斥的,写锁和写锁也是互斥的,但是读锁和读锁不是互斥的,

import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReadWriteLockDemo1 {
	
	//添加读写锁
	ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	
	public static void main(String[] args) {
		final ReadWriteLockDemo1 demo = new ReadWriteLockDemo1();
		Thread thread = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		Thread thread1 = new Thread(){
			public void run() {
				demo.get(Thread.currentThread());
			};
		};
		
		Thread thread3 = new Thread() {
			public void run() {
				demo.add(Thread.currentThread());
			}
		};
		
		thread.start();
		thread1.start();
		thread3.start();
	}
	
	public void get (Thread thread) {
		//添加锁
		lock.readLock().lock();
		try {
			System.out.println("read start time = " + System.currentTimeMillis());
			for (int i = 0; i < 5; i++) {
				try {
					Thread.sleep(20);
				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println(thread.getName() + "正在进行读操作......");
			}
			System.out.println(thread.getName() + "读操作完毕");
			System.out.println("read end time:" + System.currentTimeMillis());
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			//释放锁
			lock.readLock().unlock();
		}
		
	}
	
	
	public void add (Thread thread) {
		//添加锁
				lock.writeLock().lock();
				try {
					System.out.println("write start time = " + System.currentTimeMillis());
					for (int i = 0; i < 5; i++) {
						try {
							Thread.sleep(20);
						} catch (Exception e) {
							// TODO: handle exception
						}
						System.out.println(thread.getName() + "正在进行写操作......");
					}
					System.out.println(thread.getName() + "写操作完毕");
					System.out.println("write end time:" + System.currentTimeMillis());
				} catch (Exception e) {
					// TODO: handle exception
				} finally {
					//释放锁
					lock.writeLock().unlock();
				}
	}
}

 可重入锁(Reentrant):

如果锁具备可重入性,则称之为可重入锁。像syncheonized和ReenTranLock都是可重入锁,可重入性在我们看来实际上表明了锁的分配机制,基于线程的分配,而不是基于方法调用的分配。举个例子,当一个线程执行到synchronized方法时,比如method1,而在method1中会调用同一个类的另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2,,因为如果不这样搞得话,逻辑根本执行不下去,如果不具有可重入性,这样走,不相当于死锁么。

可中断锁:

可中断锁,顾名思义,就是可以相应中断的锁。在java中,synchronized就不是可中断的,但是Lock是可中断的锁。

如果某一线程A在执行同步代码,另一线程B正在等待正在获取锁,可能由于等待时间太长,线程B不想再等待了,想先处理其他的事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁:

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待最久的线程(也就是最先请求的线程)会获得该锁,这就是公平锁。

那么与之对立的就是非公平锁,非公平锁既无法保证锁的获取是按照请求顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。 在java中,synchronized就是非公平锁,它无法保证等待的线程获取到锁的顺序。而对于,ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁,设置方法如下 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);

读写锁:前面有。

8 新特性-信号量

java的信号量实际上是一个功能完毕的计数器,对控制一定资源的消费与回收有着重要的意义,信号量常常用于多线程的代码中,并能监控有多少数目的线程等待获取资源,并且通过信号量可以得知可用资源的数目等,这里总是在强调数目二字,但是不能指出是那、哪些在等待,哪些资源可用。如果信号量类能够返回数目还能知道哪些对象在等待,哪些资源可用,那就非常完美了,仅仅拿到这些概括性的数字,对精确控制的意义并不是很大。

9 新特性-阻塞队列

阻塞队列是java5线程新特性中的内容,java定义了阻塞队列的接口,java.util.concurrent,BlockingQueue,阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位位置。同样,当队列为空的时候,请求队列元素的操作同样会阻塞等待,直到有元素可用为止。

有了这样的功能,就为多线程的排队等候的模型开辟了便捷通道,非常有用。java.util.concurrent.BlockingQueue继承了java.Queue接口

下面请看:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;


public class BlockingQueueDemo1 {
	public static void main(String[] args) {
		BlockingQueue queque = new ArrayBlockingQueue(8);
		for (int i = 0; i < 30; i++) {
			try {
				queque.put(new Integer(i));
				System.out.println("向阻塞队列加了一个新元素:" + i);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		System.out.println("全部添加完毕。。。");
	}
}

添加了8个之后,线程就阻塞了!因为阻塞队列撑死能盛8个元素。

另外,阻塞队列还有更多实现类,来满足各种复杂的需求。ArrayBlockingQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,SunchronousQueue,具体的API差别也很小。

10 新特性-阻塞栈

对于阻塞栈,与阻塞队列相似,不同点是栈是后进先出的结构。每次操作的是栈顶,而队列是先进先出的结构,每次操作的是队列头。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;


public class BlockingQueueDemo1 {
	public static void main(String[] args) {
		BlockingDeque deque = new LinkedBlockingDeque(8);	//也就改了这一处而已
		for (int i = 0; i < 30; i++) {
			try {
				deque.put(new Integer(i));
				System.out.println("向阻塞队列加了一个新元素:" + i);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		
		System.out.println("全部添加完毕。。。");
	}
}

从上面结果可以看出,程序并没有结束,二是线程阻塞了,原因是栈满了,后面添加元素的操作就被阻塞了。

11 新特性-条件变量

条件变量是java5线程中很重要的一个概念,顾名思义,条件变量就是表示条件的一种变量。但是必须说明,这里的条件是没有实际含义的,仅仅是个标志而已,并且条件的含义旺旺通过代码来赋予含义。

这里的条件和普通的条件表达式有着天壤之别:条件变量都实现了java.util.concurrent.lock.Condition接口,条件变量的实例化是通过对一个Lock对象上调用newCondition()方法来获取的,这样,条件就和一个锁对象绑起来了,因此,java中的条件变量只能配合锁使用,并控制并发程序访问竞争资源的安全。

条件变量的出现是为了更精细控制线程等待与唤醒,在java5之前,线程的等待与唤醒依靠的是object对象的wait() 和 notify()/notifyAll()方法,这样处理不够精细。

而在java5中,一个锁可以有多个条件,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待,当调用signalAll()方法,又可以唤醒该条件下等待的线程。

条件变量是比较抽象的,原因是他不是自然语言中的条件概念,而是程序控制的一种手段。

以下以银行存取款的模拟程序来揭开java多线程条件变量的神秘面纱:

有一个账户,多个用户在同时操作这个账户,有的存款有的取款,存款随便存,取款有限制,不能透支,任何试图透支的操作都将等待里面有足够的存款时才执行操作。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class TiaojianBianLiangDemo {
	public static void main(String[] args) {
		MyCount count = new MyCount("123456788947243746", 1000);
		
		ControlThread thread1 = new ControlThread(count, "张三", 10000, ControlThread.TYPE_SAVE);
		ControlThread thread2 = new ControlThread(count, "张三的老婆", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread3 = new ControlThread(count, "张三的儿子", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread4 = new ControlThread(count, "张三的闺女", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread5 = new ControlThread(count, "张三的父亲", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread6 = new ControlThread(count, "张三的母亲", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread7 = new ControlThread(count, "张三的岳父", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread8 = new ControlThread(count, "张三的岳母", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread9 = new ControlThread(count, "张三的哥哥", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread10 = new ControlThread(count, "张三的弟弟", 1000, ControlThread.TYPE_DRAW);
		ControlThread thread11 = new ControlThread(count, "张三的妹妹", 1000, ControlThread.TYPE_DRAW);
		
		//张三好惨。。。
		
		ExecutorService pool = Executors.newFixedThreadPool(11);
		pool.execute(thread11);
		pool.execute(thread10);
		pool.execute(thread9);
		pool.execute(thread8);
		pool.execute(thread7);
		pool.execute(thread6);
		pool.execute(thread5);
		pool.execute(thread4);
		pool.execute(thread3);
		pool.execute(thread2);
		pool.execute(thread1);
		
		pool.shutdown();



	}
}

class MyCount{
	private String mOid;	//账号
	private int mCash;	//账户余额
	
	private Lock mLock = new ReentrantLock();	//账户锁
	private Condition mSave_Condition = mLock.newCondition();	//存款条件
	private Condition mDraw_Condition = mLock.newCondition();	//取款条件
	
	public MyCount(String id, int cash) {
		this.mOid = id;
		this.mCash = cash;
	}
	
	/**
	 * 存款
	 */
	
	public void saving(int x, String name) {
		mLock.lock();
		if (x > 0) {
			mCash += x;
			System.out.println("存款完毕! 执行人:" + name + " 金额:" + x + "目前余额为:" + mCash);
		}
		mDraw_Condition.signalAll(); //唤醒所有取款线程
		mLock.unlock();
	}
	
	public void drawing (int x, String name) {
		mLock.lock();
		try {
			if (mCash - x < 0) {
				System.out.println("余额不足," + name + "要取" + x + "元, 余额为:" + mCash + "元!");
				mDraw_Condition.await(); //等待
			}
			// 进行取款处理
			mCash -= x;
			System.out.println("取款完毕! 执行人:" + name + " 金额:" + x);
			
//			mSave_Condition.signalAll(); //为什么有这一步??
				
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			mLock.unlock();
		}
				
	}
	
}


class ControlThread extends Thread {
	public static final int TYPE_SAVE = 0x01;
	public static final int TYPE_DRAW = 0x02;
	
	private MyCount mMyCount;
	private String mName;
	private int mMoney = 0;
	private int mType = 0;
	
	public ControlThread (MyCount count, String name, int number, int type) {
		this.mMyCount = count;
		this.mName = name;
		this.mMoney = number;
		this.mType = type;
	}
	
	public void run() {
		if (mType == TYPE_SAVE) {
			mMyCount.saving(mMoney, mName);
		} else if (mType == TYPE_DRAW) {
			mMyCount.drawing(mMoney, mName);
		}
	}
}



11 新特性-原子量

所谓的原子量就是操作变量的操作时原子的,该操作不可再分,因此是线程安全的。

为什么要使用原子量呢?原因是多个线程对单个变量操作也会引起一些问题,在java5之前,可以通过volatile, synchronized关键字来解决并发访问的安全问题,但是这样太麻烦。

java5之后,专门提供了用来进行多线程并发访问的工具包 java.util,concurrent.atomic,其中的类也很简单。

原子量吧,我感觉就像是一个线程安全的类,对这个类作出某些操作的时候能够保证同时只有一个线程能访问,,但是这不代表写代码的时候就可以高枕无忧了,因为这种控制是原子级的,不能保证这个过程之外的整段逻辑是否是线程安全,所以用的时候还是得做一下整体的并发处理。

具体代码不写了。百度一下。

12 新特性-障碍器

哎,一堆生词。Java5中,添加了障碍器,为了适应一种新的设计需求,比如一个大型任务,常常需要分配好多子任务去执行,只有当所有的子任务完成之后,才能执行主任务,这时候就可以用障碍器了,不明觉厉!感觉很有用!

import java.util.concurrent.CyclicBarrier;


public class CylicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier cb = new CyclicBarrier(7, new MainTask());
		
		new HuluwaThread("红娃大力士", cb).start();
		new HuluwaThread("橙娃千里眼", cb).start();
		new HuluwaThread("黄娃铜头铁臂", cb).start();
		new HuluwaThread("绿娃喷火娃", cb).start();
		new HuluwaThread("青娃水娃", cb).start();
		new HuluwaThread("蓝娃隐身娃", cb).start();
		new HuluwaThread("紫娃吸妖娃", cb).start();
	}
	
	
}

class MainTask implements Runnable{
	public void run() {
		System.out.println("妖怪,还我爷爷,饶你不死!");
	}
}

class HuluwaThread extends Thread {
	
	private String name;
	private CyclicBarrier cb;
	
	public HuluwaThread(String name, CyclicBarrier cb) {
		this.name = name;
		this.cb = cb;
	}
	
	public void run() {
		try {
			System.out.println(name + "合体完毕!");
			cb.await();
		} catch (Exception e) {
			// TODO: handle exception
		}	finally {
			
		}
		
	}
}


猜你喜欢

转载自blog.csdn.net/weixin_28774815/article/details/80220023
今日推荐