(参考http://www.cnblogs.com/xdp-gacl/p/3633936.html 点击打开链接,以此为模板 自己做了整理、修改)
目录
3.2 sleep() / join() / yield()方法介绍
一. 线程的基本概念
线程理解:线程是一个程序里面不同的执行路径
每一个分支都叫做一个线程,main()叫做主分支,也叫主线程。
“记忆:线轻进静。一根很轻的线,进静同音”。进程只是一个静态的概念,机器上的一个.class文件,机器上的一个.exe文件,这个叫做一个进程。程序的执行过程都是这样的:首先把程序的代码放到内存的代码区里面,代码放到代码区后并没有马上开始执行,但这时候说明了一个进程准备开始,进程已经产生了,但还没有开始执行,这就是进程,所以进程其实是一个静态的概念,它本身就不能动。平常所说的进程的执行指的是进程里面主线程开始执行了,也就是main()方法开始执行了。进程是一个静态的概念,在我们机器里面实际上运行的都是线程。
Windows操作系统是支持多线程的,它可以同时执行很多个线程,也支持多进程,因此Windows操作系统是支持多线程多进程的操作系统。Linux和Uinux也是支持多线程和多进程的操作系统。DOS就不是支持多线程和多进程了,它只支持单进程,在同一个时间点只能有一个进程在执行,这就叫单线程。
CPU难道真的很神通广大,能够同时执行那么多程序吗?不是的,CPU的执行是这样的:CPU的速度很快,一秒钟可以算好几亿次,因此CPU把自己的时间分成一个个小时间片,我这个时间片执行你一会,下一个时间片执行他一会,再下一个时间片又执行其他人一会,虽然有几十个线程,但一样可以在很短的时间内把他们通通都执行一遍,但对我们人来说,CPU的执行速度太快了,因此看起来就像是在同时执行一样,但实际上在一个时间点上,CPU只有一个线程在运行。
学习线程首先要理清楚三个概念:
- 进程:进程是一个静态的概念
- 线程:一个进程里面有一个主线程叫main()方法,是一个程序里面的,一个进程里面不同的执行路径。
- 在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,因此我们看起来的感觉就像是多线程一样。什么才是真正的多线程?如果你的机器是双CPU,或者是双核,这确确实实是多线程。
二. 线程的创建和启动
在Java里面,Java的线程是通过java.lang.Thread类来实现的,每一个Thread对象代表一个新的线程。创建一个新线程出来有两种方法:一个是从Thread类继承,另一个是实现接口runnable。JVM启动时会有一个由主方法(public static void main())所定义的线程,这个线程叫主线程。可以通过创建Thread的实例来创建新的线程。你只要new一个Thread对象,一个新的线程也就出现了。每个线程都是通过某个特定的Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。
案例一,使用实现Runnable接口创建和启动新线程,开辟一个新的线程来调用run()方法。代码如下:
package cn.galc.test;
public class TestThread1{
public static void main(String args[]){
Runner1 r1 = new Runner1();//这里new了一个线程类的对象出来
//r1.run();//这个称为方法调用,方法调用的执行是等run()方法执行完之后才会继续执行main()方法
Thread t = new Thread(r1);//要启动一个新的线程就必须new一个Thread对象出来
//这里使用的是Thread(Runnable target) 这构造方法
t.start();//启动新开辟的线程,新线程执行的是run()方法,新线程与主线程会一起并行执行
for(int i=0;i<10;i++){
System.out.println("maintheod:"+i);
}
}
}
/*定义一个类用来实现Runnable接口,实现Runnable接口就表示这个类是一个线程类*/
class Runner1 implements Runnable{
public void run(){
for(int i=0;i<10;i++){
System.out.println("Runner1:"+i);
}
}
}
不开辟新线程直接调用run()方法
案例2,继承Thread类,并重写其run()方法创建和启动新的线程。代码如下:
package cn.galc.test;
/*线程创建与启动的第二种方法:定义Thread的子类并实现run()方法*/
public class TestThread2{
public static void main(String args[]){
Runner2 r2 = new Runner2();
r2.start();//调用start()方法启动新开辟的线程
for(int i=0;i<=10;i++){
System.out.println("mainMethod:"+i);
}
}
}
/*Runner2类从Thread类继承
通过实例化Runner2类的一个对象就可以开辟一个新的线程
调用从Thread类继承来的start()方法就可以启动新开辟的线程*/
class Runner2 extends Thread{
public void run(){//重写run()方法的实现
for(int i=0;i<=10;i++){
System.out.println("Runner2:"+i);
}
}
}
使用实现Runnable接口和继承Thread类这两种开辟新线程的方法的选择,应该优先选择实现Runnable接口这种方式去开辟一个新的线程。因为接口的实现可以实现多个,而类的继承只能是单继承。因此在开辟新线程时能够使用Runnable接口就尽量不要使用从Thread类继承的方式来开辟新的线程。
三. 线程状态转换
3.1 线程控制的基本方法
sleep()和wait()的区别:
1.)这两个方法来自不同的类分别是,sleep()来自Thread类,和wait()来自Object类。sleep()方法是Thread类的一个静态方法,也就是说他只对当前对象有效,谁调用的谁去睡觉,即使在a线程里调用b的sleep()方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep()。
2.) 锁: 最主要是sleep()方法没有释放锁,而wait()方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep()不出让系统资源;wait()是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait()不会加时间限制,因为如果wait()线程的运行资源不够,再出来也没用,要等待其他线程调用notify()/notifyAll()唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间。如果时间不到只能调用interrupt()强行打断,在sleep的过程中过程中如果被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。Thread.sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
3.)使用范围:wait(),notify()和notifyAll()只能在同步控制方法或者同步控制块里面使用,而sleep()可以在任何地方使用。
4.)sleep()必须捕获异常,而wait(),notify()和notifyAll()不需要捕获异常。
3.2 sleep()/join()/yield()方法介绍
sleep()方法的应用案例,代码如下:
package cn.galc.test;
import java.util.*;
public class TestThread3 {
public static void main(String args[]){
MyThread thread = new MyThread();
thread.start();//调用start()方法启动新开辟的线程
try {
/*Thread.sleep(10000);
sleep()方法是在Thread类里面声明的一个静态方法,因此可以使用Thread.sleep()的格式进行调用
*/
/*MyThread.sleep(10000);
MyThread类继承了Thread类,自然也继承了sleep()方法,所以也可以使用MyThread.sleep()的格式进行调用
*/
/*静态方法的调用可以直接使用“类名.静态方法名”
或者“对象的引用.静态方法名”的方式来调用*/
MyThread.sleep(10000);
System.out.println("主线程睡眠了10秒种后再次启动了");
//在main()方法里面调用另外一个类的静态方法时,需要使用“静态方法所在的类.静态方法名”这种方式来调用
/*
所以这里是让主线程睡眠10秒种
在哪个线程里面调用了sleep()方法就让哪个线程睡眠,所以现在是主线程睡眠了。
*/
} catch (InterruptedException e) {
e.printStackTrace();
}
//thread.interrupt();//使用interrupt()方法去结束掉一个线程的执行并不是一个很好的做法
thread.flag=false;//改变循环条件,结束死循环
/**
* 当发生InterruptedException时,直接把循环的条件设置为false即可退出死循环,
* 继而结束掉子线程的执行,这是一种比较好的结束子线程的做法
*/
/**
* 调用interrupt()方法把正在运行的线程打断
相当于是主线程一盆凉水泼上去把正在执行分线程打断了
分线程被打断之后就会抛InterruptedException异常,这样就会执行return语句返回,结束掉线程的执行
所以这里的分线程在执行完10秒钟之后就结束掉了线程的执行
*/
}
}
class MyThread extends Thread {
boolean flag = true;// 定义一个标记,用来控制循环的条件
public void run() {
/*
* 注意:这里不能在run()方法的后面直接写throw Exception来抛异常,
* 因为现在是要重写从Thread类继承而来的run()方法,重写方法不能抛出比被重写的方法的不同的异常。
* 所以这里只能写try……catch()来捕获异常
*/
while (flag) {
System.out.println("==========" + new Date().toLocaleString() + "===========");
try {
/*
* 静态方法的调用格式一般为“类名.方法名”的格式去调用 在本类中声明的静态方法时调用时直接写静态方法名即可。 当然使用“类名.方法名”的格式去调用也是没有错的
*/
// MyThread.sleep(1000);//使用“类名.方法名”的格式去调用属于本类的静态方法
sleep(1000);//睡眠的时如果被打断就会抛出InterruptedException异常
// 这里是让这个新开辟的线程每隔一秒睡眠一次,然后睡眠一秒钟后再次启动该线程
// 这里在一个死循环里面每隔一秒启动一次线程,每个一秒打印出当前的系统时间
} catch (InterruptedException e) {
/*
* 睡眠的时一盘冷水泼过来就有可能会打断睡眠
* 因此让正在运行线程被一些意外的原因中断的时候有可能会抛被打扰中断(InterruptedException)的异常
*/
return;
// 线程被中断后就返回,相当于是结束线程
}
}
}
}
运行结果:
join()方法的使用案例,代码如下:
package cn.galc.test;
public class TestThread4 {
public static void main(String args[]) {
MyThread2 thread2 = new MyThread2("mythread");
// 在创建一个新的线程对象的同时给这个线程对象命名为mythread
thread2.start();// 启动线程
try {
thread2.join();// 调用join()方法合并线程,将子线程mythread合并到主线程里面
// 合并线程后,程序的执行的过程就相当于是方法的调用的执行过程
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i <= 5; i++) {
System.out.println("I am main Thread");
}
}
}
class MyThread2 extends Thread {
MyThread2(String s) {
super(s);
/*
* 使用super关键字调用父类的构造方法
* 父类Thread的其中一个构造方法:“public Thread(String name)”
* 通过这样的构造方法可以给新开辟的线程命名,便于管理线程
*/
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("I am a\t" + getName());
// 使用父类Thread里面定义的
//public final String getName(),Returns this thread's name.
try {
sleep(1000);// 让子线程每执行一次就睡眠1秒钟
} catch (InterruptedException e) {
return;
}
}
}
}
运行结果:
yield()方法的使用范例,代码如下:
package cn.galc.test;
public class TestThread5 {
public static void main(String args[]) {
MyThread3 t1 = new MyThread3("t1");
/* 同时开辟了两条子线程t1和t2,t1和t2执行的都是run()方法 */
/* 这个程序的执行过程中总共有3个线程在并行执行,分别为子线程t1和t2以及主线程 */
MyThread3 t2 = new MyThread3("t2");
t1.start();// 启动子线程t1
t2.start();// 启动子线程t2
for (int i = 0; i <= 5; i++) {
System.out.println("I am main Thread");
}
}
}
class MyThread3 extends Thread {
MyThread3(String s) {
super(s);
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + ":" + i);
if (i % 2 == 0) {
yield();// 当执行到i能被2整除时,当前执行的线程就让出来让另一个在执行run()方法的线程来优先执行
/*
* 在程序的运行的过程中可以看到,
* 线程t1执行到(i%2==0)次时就会让出线程,让t2线程来优先执行
* 而线程t2执行到(i%2==0)次时也会让出线程,给t1线程优先执行
*/
}
}
}
}
运行结果如下:
sleep(),yield(),join(),notify(),wait(),notifyAll()的区别:
1)Thread.sleep(long) 和Thread.yield()都是Thread类的静态方法,在调用的时候都是Thread.sleep(long)/Thread.yield()的方式进行调用。而join()是由线程对象来调用。
2)Thread.sleep(long)是让当前运行的线程睡眠一会,这里说的睡眠的意思是让线程从运行状态进入阻塞状态,只有等阻塞时间过后才进入就绪状态(并不是直接进入运行状态),是否进入运行状态就要看JVM的内部调用机制了,通常是级别较高的处于就绪状态的线程会被调度进入就绪状态。
3)线程如果是通过继承Thread类,重写起run方法来实现的,那么在调用Thread.sleep(long)/Thread.yield()方法时当然可以不要Thread,直接使用sleep(long)/yield()。但如果是实现Runnable接口来实现的就必须有Thread。
4)如果Thread.sleep(long)/Thread.yield()使用在由synchronized锁定的代码块或者方法当中,那么在调用他们的过程当中,并不会释放锁。而是即使是睡眠也抱着这把锁,不让别人访问。
5)join()方法使调用该方法的线程在此之前执行完毕,也就是等待调用该方法的线程执行完毕后,再往下继续执行。注意该方法和sleep()方法都要捕获异常。
6)yield()方法是让当前线程直接由运行状态进入就绪状态,然后让JVM重新调度一次,但是这次调度只会让处于就绪队列中比当前线程优先级高或者相等的线程运行,很可能某个线程在调用了yield()方法后,又被JVM调度进来运行。
7)wait()和notify()、notifyAll() 这三个方法用于协调多个线程对共享数据的存取,所以必须在Synchronized语句块内使用这三个方法。前面说过Synchronized这个关键字用于保护共享数据,阻止其他线程对共享数据的存取。但是这样程序的流程就很不灵活了,如何才能在当前线程还没退出Synchronized数据块时让其他线程也有机会访问共享数据呢?此时就用这三个方法来灵活控制。wait()方法使当前线程暂停执行并释放对象锁标志,让其他线程可以进入Synchronized数据块,当前线程被放入对象等待池中。当调用 notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中的线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。 notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。
8)我对wait()的一点个人理解:将设线程为A线程、A线程持有锁(进入synchronized代码段)后执需要行同步代码,当执行到调用锁.wait()时,意思是让A线程进入阻塞状态(注意这个时候A线程的synchronized方法中锁.notify()这句代码后可能还有一大段代码没有执行就被阻塞在这里了)并且释放同步锁(释放锁的同时将自身线程放入到了该锁的等待队列当中);释放锁的瞬间其他的线程就已经获取到了该锁(这里姑且给这个其他线程起个名字叫做B线程)、当B线程调用了锁.notify()的时候其实B线程自己是一点都没有受到影响,因为这时B既没有阻塞也没有释放锁还是没事一样的执行完synchronized里面的代码;但是B线程刚刚的那句锁.notify()可以告诉虚拟机我B线程可能马上就要执行完了(当然具体多久还是程序员说了算,这个完全要看B线程的synchronized方法中锁.notify()这句代码后面还有多长没执行完),还告诉虚拟机你现在要立即从锁的等待队列中取出一个线程进入到就绪状态(这时候可能恰巧将A线程取出来了)。但这是A线程并不能立即就执行,因为B可能还没有执行完(没执行完就没有释放锁),A线程要想继续执行就必须再次获得锁;一旦B执行完,A就能获得锁了。但是A获得了锁是不是从synchronized方法重头再执行一遍呢?肯定不是!这时候A会接着上次notify()后的代码执行,这样来达到线程间通讯目的。
四. 线程的优先级
线程的优先级分为1-10这10个等级,默认是5。值越大优先级越高。如果小于1或大于10,则抛出异常throw new IllegalArgumentException()。
有时候,在自己的计算机上运行设置了线程优先级的几个线程的程序时,最终的运行结果并没有与预先估计的一致。不要误以为这是线程优先级失效,而是现在的计算机的多核处理器引起的。多核CPU是可以真正实现多个线程同时运行的,而不是简单的快速切换的假象,每个核心都能够负责一个线程。参考http://bbs.csdn.net/topics/390502387 链接
线程优先级别的使用案例,代码如下:
package cn.galc.test;
public class TestThread6 {
public static void main(String args[]) {
MyThread4 t4 = new MyThread4();
MyThread5 t5 = new MyThread5();
Thread t1 = new Thread(t4);
Thread t2 = new Thread(t5);
t1.setPriority(Thread.NORM_PRIORITY + 3);// 使用setPriority()方法设置线程的优先级别,这里把t1线程的优先级别进行设置
/*
* 把线程t1的优先级(priority)在正常优先级(NORM_PRIORITY)的基础上再提高3级 ,5+3=8
* 这样t1的执行一次的时间就会比t2的多很多
* 默认情况下NORM_PRIORITY的值为5
*/
t1.start();
t2.start();
System.out.println("t1线程的优先级是:" + t1.getPriority());
// 使用getPriority()方法取得线程的优先级别,打印出t1的优先级别为8
}
}
class MyThread4 implements Runnable {
public void run() { //run()方法一结束,这个线程也就结束了
for (int i = 0; i <= 1000; i++) {
System.out.println("T1:" + i);
}
}
}
class MyThread5 implements Runnable {
public void run() { //run()方法一结束,这个线程也就是结束了
for (int i = 0; i <= 1000; i++) {
System.out.println("===============T2:" + i);
}
}
}
五. 线程同步
每个对象对应一个互斥锁,使用关键字synchronized修饰某个对象之后,这个对象就是在任意时刻都只能由一个线程访问。
synchronized关键字的使用案例,代码如下:
package cn.galc.test;
public class TestSync implements Runnable {
Timer timer = new Timer();
public static void main(String args[]) {
TestSync test = new TestSync();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("t1");// 设置t1线程的名字
t2.setName("t2");// 设置t2线程的名字
t1.start();
t2.start();
}
public void run() {
timer.add(Thread.currentThread().getName());
}
}
class Timer {
private static int num = 0;
public/* synchronized */void add(String name) {// 在声明方法时加入synchronized时表示在执行这个方法的过程之中,当前对象被锁定
synchronized (this) {
/*
* 使用synchronized(this)来锁定当前对象,这样就不会再出现两个不同的线程同时访问同一个对象资源的问题了 只有当一个线程访问结束后才会轮到下一个线程来访问
*/
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + ":你是第" + num + "个使用timer的线程");
}
}
}
线程死锁的问题:
package cn.galc.test;
/*这个小程序模拟的是线程死锁的问题*/
public class TestDeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
public void run() {
System.out.println(Thread.currentThread().getName() + "的flag=" + flag);
/*
* 运行程序后发现程序执行到这里打印出flag以后就再也不往下执行后面的if语句了
* 程序也就死在了这里,既不往下执行也不退出
*/
/* 这是flag=1这个线程 */
if (flag == 1) {
synchronized (o1) {
/* 使用synchronized关键字把对象01锁定了 */
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
/*
* 前面已经锁住了对象o1,只要再能锁住o2,那么就能执行打印出1的操作了
* 可是这里无法锁定对象o2,因为在另外一个flag=0这个线程里面已经把对象o1给锁住了
* 尽管锁住o2这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o2不放的
*/
System.out.println("1");
}
}
}
/*
* 这里的两个if语句都将无法执行,因为已经造成了线程死锁的问题
* flag=1这个线程在等待flag=0这个线程把对象o2的锁解开,
* 而flag=0这个线程也在等待flag=1这个线程把对象o1的锁解开
* 然而这两个线程都不愿意解开锁住的对象,所以就造成了线程死锁的问题
*/
/* 这是flag=0这个线程 */
if (flag == 0) {
synchronized (o2) {
/* 这里先使用synchronized锁住对象o2 */
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
/*
* 前面已经锁住了对象o2,只要再能锁住o1,那么就能执行打印出0的操作了 可是这里无法锁定对象o1,因为在另外一个flag=1这个线程里面已经把对象o1给锁住了 尽管锁住o1这个对象的线程会每隔500毫秒睡眠一次,可是在睡眠的时候仍然是锁住o1不放的
*/
System.out.println("0");
}
}
}
}
public static void main(String args[]) {
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 1;
td2.flag = 0;
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.setName("线程td1");
t2.setName("线程td2");
t1.start();
t2.start();
}
}
解决线程死锁的问题,最好只锁定一个对象,不要同时锁定两个对象。
生产者消费者问题:
生产者消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
package cn.galc.test;
/* 范例名称:生产者--消费者问题
* 源文件名称:ProducerConsumer.java
* 要 点:
* 1. 共享数据的不一致性/临界资源的保护
* 2. Java对象锁的概念
* 3. synchronized关键字/wait()及notify()方法
*/
public class ProducerConsumer {
public static void main(String args[]){
SyncStack stack = new SyncStack();
Runnable p=new Producer(stack);
Runnable c = new Consumer(stack);
Thread p1 = new Thread(p);
Thread c1 = new Thread(c);
p1.start();
c1.start();
}
}
class SyncStack{ //支持多线程同步操作的堆栈的实现
private int index = 0;
private char []data = new char[6];
public synchronized void push(char c){ //入栈
if(index == data.length){
try{
this.wait(); //入栈时,栈满了就等待
}catch(InterruptedException e){}
}
this.notify(); //唤醒
data[index] = c;
index++; //下标自增
}
public synchronized char pop(){ //出栈
if(index ==0){
try{
this.wait(); //出栈时,栈空了就等待
}catch(InterruptedException e){}
}
this.notify(); //唤醒
index--; //下标自减
return data[index];
}
}
class Producer implements Runnable{ //生产者类
SyncStack stack;
public Producer(SyncStack s){
stack = s;
}
public void run(){
for(int i=0; i<20; i++){
char c =(char)(Math.random()*26+'A');
stack.push(c); //入栈
System.out.println("produced:"+c);
try{
Thread.sleep((int)(Math.random()*1000));
}catch(InterruptedException e){
}
}
}
}
class Consumer implements Runnable{ //消费者类
SyncStack stack;
public Consumer(SyncStack s){
stack = s;
}
public void run(){
for(int i=0;i<20;i++){
char c = stack.pop(); //出栈
System.out.println("消费:"+c);
try{
Thread.sleep((int)(Math.random()*1000));
}catch(InterruptedException e){
}
}
}
}
------------------------------------------------------------------ 我是低调的分隔线 ----------------------------------------------------------------------
吾欲之南海,一瓶一钵足矣...