------<ahref="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------

现实生活中很多对象处理的事物都是并发的,比如说一个人可以一边听歌一边看书,脑子里还一边在思考,皮肤感知到痒了还能调动手去挠一挠,这些都是并发完成的。

而计算机就是对现实的模拟,因此也会碰到这种需求,比如说一台个人电脑,内部同时运行着很多的软件,我们打开windows的任务管理器会发现许多系统的进程,此外还有QQ,迅雷,杀毒软件,游戏,浏览器,视频播放器等等,这些任务都是同步运行的,在使用其中一个的时候并不需要中止另外一个。微观来说,我们单独拆开视频播放器也会发现,它其实是包含着视频播放和音频播放的,而这两个内部功能也是同时的,否则将只有声音没有图像或者倒过来。

我们知道计算机的运算核心是CPU,在早期的时候一台计算机只有一个CPU,它不可能做到同时处理多个任务,它能做的就是飞快地在任务与任务之间做着切换,这样几十个任务做一圈下来甚至不到一毫秒,于是使用者就会产生一种错觉好像这些任务是同步进行的,其实内部一个任务运行的时候其他任务是需要等待CPU执行的,只不过等待的时间远远小于人类能察觉到的时间所以让我们感觉每一个任务的处理都是不间断的。早在2005年的时候CPU的切换速率就已经能达到4GHZ,也就是每秒钟切换超过40亿次,而现在又有了多核处理器,真正实现多个线程同步处理,使得处理速度变得更快。

那么我们在实际开发中也会遇到这种需求:我们开发的产品需要同时具有多条线程,从而可以被多个对象使用。试想如果火车站售票大厅只开了一个窗口,这个用户不买完票别的用户就没法买,那么用户的等待时间就会超级长。因此我们需要设计尽量多一些的售票窗口,开启多个线程给所有的用户。

这就是多线程的概念,这里面有两个关键词:进程线程

进程是指正在执行的程序。每一个进程执行都有一个执行顺序。该顺序就是一个执行路径或者叫一个控制单元。

线程是进程中的一个独立的控制单元。线程在控制着进程的执行。

一个进程中至少有一个线程。

Java虚拟机JVM启动的时候会有一个进程java.exe

根据上面的知识我们知道该进程中至少一个线程负责java程序的执行。

而且这个线程会自动在代码里搜索main关键字,所以它运行的代码存在于main方法中。该线程称为主线程。

就我们所学的知识范围内来说,虚拟机JVM都不止一个线程,除了运行主函数的主线程外,至少还有负责垃圾回收机制的线程。

多线程存在的意义:允许多个任务同时处理(如果主线程在走,垃圾不回收,那么会导致内存超负荷,这时主线程必须停,先处理垃圾),顺带还能提高效率。

其他最典型的例如下载程序都是一个进程,里面好几个下载可以同时进行,那每一个就是一个线程。

如何执行进程和线程是windows的任务,java只是调用,调用的方法已经写好封装在Thread类里。因此我们查阅API文档,知道想要创建自定义线程,可以按如下操作:

创建线程的第一种方式:继承Thread类。

步骤:

1.      定义一个类继承Thread类。

2.      复写Thread类中的run方法。

目的:将要运行的自定义代码存储在run方法中,复写父类方法,并继承Thread父类其他所有功能,包括调用系统底层多线程的功能,让线程运行起来。

3.      创建该类的对象,并调用线程的start方法,该方法有两个作用:启动线程,调用run方法。

【注意】如果主函数里调用的不是start方法而是run方法的话那么线程不会启动(线程创建了但是没有运行),还是单线程,执行run方法的时候不会执行其他代码,因为线程没有开启,还是主线程在调用run方法,仅仅是对象调用方法,和一般函数没有区别。

Start和run的区别一定要清楚,面试常考。

下面是一个卖票的小例子:

class Ticket extends Thread
{int ticket = 100;//设置初始票数为100张public void run(){for (; ticket>0 ;ticket-- ){System.out.println(Thread.currentThread().getName()+"...selling: "+ticket);//打印第几个线程卖出的票,卖的是第几张。}}
}
//定义一个类继承Thread类并复写其中的run方法。
class TicketDemo//第一种创建方式
{public static void main(String[] args) {Ticket t1 = new Ticket();Ticket t2 = new Ticket();Ticket t3 = new Ticket();Ticket t4 = new Ticket();//创建该类的对象。t1.start();t2.start();t3.start();t4.start();//启动线程,调动它们的run方法。//四个线程火力全开地在同步卖票。}
}

通过分析运行结果发现,发现每一次都不同。因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行。(当然多核除外) CPU在做着快速切换以达到看上去同时运行的效果。我们可以形象地把多线程的运行形容为在互相抢夺CPU的资源,或者叫做执行权。

这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长时间,CPU说的算。

为什么要覆盖run方法?

Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储区域就是在run方法里。(就好像主线程要运行的代码存在main里面,虚拟机从main方法执行,这是虚拟机定义好的,而在run方法里存储需要多线程运行的代码也是Thread这个类定义好的)。

线程的五种状态:被创建、运行、冻结、临时、消亡。

控制这几种状态转化的关键词:start,stop, sleep(time) wait notify。

其中stop已经停止使用;wait和notify已经过时,它们分别被Condition的await和signal取代。

它们的关系如图所示:

线程对象在创建好之后进入被创建状态,它里面需要执行的代码不会启动,需要通过start关键字才能过渡到运行状态,真正开始多线程运行。我们可以通过sleep(time)关键字让该线程进入冻结状态一段所设定的时间,时间单位是毫秒,到时间了自动回到运行状态,也可以通过wait关键字让运行状态中的线程进入冻结状态,并通过notify关键字让其返回到运行状态来。临时状态的进出不是我们能控制的,而是系统控制的,CPU同一时间只能执行一个线程,所有其他有资格获得CPU执行权的线程都处于这个状态。而当一个线程使用完毕需要终结它时用stop关键字让其进入消亡状态等待垃圾回收器回收?

对象通过start运行后不会立即执行,而是先进入临时状态,虽然获得了运行资格,但是要等CPU分配资源才能真正开始执行。

执行权和运行资格的概念很容易混淆,需要明确的是:执行权是CPU控制的,同一时刻只能有一个进程的一个线程拥有执行权。而运行资格是JAVA定义的,只有有运行资格的线程才有资格等待CPU的资源。

另外上面的sleep是静态方法,属于类Thread,它会抛出异常InterruptedException异常。

另外上面售票的小例子我们会发现几乎每张票都被卖了4次,这时因为我们建立了4个该类的对象,相当于产生了4个火车站同时售票,每个火车站售100张票,这明显与我们的设计初衷不符。因此需要把这个票从一个线程一份变为共享资源。一个方法是可以把票数变成静态的,这样每个对象就不会各自创建一个新票了,而是共享,但是静态生命周期长,占用系统资源。

但是这里注意下面这个解决方案是不可以的,已经从创建状态到了运行状态了,再开启start没有意义。运行会出提示:线程状态异常。已经在运行的程序不需要再次开启。

因此,这里推荐第二种创建线程的方式。

创建线程的第二种方式:实现Runnable接口

1.      定义类实现Runnable接口。

2.      在这个类中覆盖Runnable接口中的run方法。并将线程要运行的代码存放在该run方法中。

3.      通过Thread类建立线程对象:将我们定义好的Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。(多态)

自定义run方法的所属对象是我们创建的Runnable接口的子类对象。所以要让线程去执行指定对象的run方法,就必须告诉线程这个对象是谁(明确该run方法所属对象)。

调用Thread类的start方法开启线程并调用Runnable接口子类对象的run方法。

【注意】:子类定义时,方法的权限要设置为public否则无法覆盖。因为接口里的变量和方法全都是public,而要想继承或者实现,子类权限要大于等于父类。

上例按照第二种方式改造后的代码:

class Ticket implements Runnable
{private int ticket = 100;
public void run()
{while(ticket>0)
System.out.println(Thread.currentThread().getName()+"...selling: "+ticket--);}
class TicketDemo2
{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);Thread t4 = new Thread(t);t1.start();t2.start();t3.start();t4.start();}
}

可以看出Ticket这个类只建立了一个对象t,而t对象里面有了一个私有的ticket参数,run方法操作这个参数。同时t对象作为参数把run方法传递给了Thread类的4个对象,相当于开启了4个线程,把一部分需要多线程执行的代码给了这4个对象让他们处理,而这4个对象处理的代码调用的都是同一个私有的private参数,这个参数在t对象的堆内存里的成员变量位置,没有封装在run函数里面供每个Thread的对象人手一份,保证了所操作数据的唯一性。

除了上述这两种方法外,把票数单独封装在一个类里面,从外部进行调用也可以。

实现方式和继承方式区别:

继承Thread:线程代码存放Thread子类的run方法中。

实现Runnable:线程代码存在接口的子类的run方法。

实现方式好处:

避免了单继承的局限性。

在定义线程时,建议使用实现方式。这种方式也最常用。

因为JAVA不支持多继承,所以当一个类有自己的父类,它就不能再继承Thread,但是该类里面还有一部分代码需要多线程执行。当然如果一个类本身没有自己的父类,也可以用继承方式。

获取线程和线程名:

上述两个例子中都打印了自己的线程名,那是因为线程都有自己默认的名称,默认是:Thread-编号,该编号从0开始。通过getName()就可以获取通过setName设置名称,但也可以通过构造函数往里面传string类型来自定义设置。

currentThread方法能返回当前执行的线程对象的引用(获取当前线程对象),该方法是静态的,写为 static Thread currentThread();

因此this.getName()和Thread.currentThread().getName()是等价的。

但是两个稍微有点差别,Thread.currentThread()更通用一些,无论谁调用都能获取当前CPU正在处理的线程的对象,而this是获得这个类的对象:第一种创建线程的方式因为自定义的类继承了Thread,同时继承了getName方法,可以使用没问题。但如果用第二种创建线程的方式,我们自己建的类实现Runnable接口,它里面并没有getName方法,这时this.getName()就会编译报错。所以一般写Thread.currentThread()。

多线程的安全问题

分析上面那个售票的例子发现,打印出0 -1-2等错票,这是因为当某条或者某几条(A,B,C)线程判断完票数大于0之后CPU就切换到别的线程(D),而别的线程可能完成了ticke--的操作,这样票数就为0了,而当CPU切换到A后,A就打印0票并--,再切换到B线程就会打印-1票并继续--,最后切换到C打印出来-2票。

这样,多线程运行出现安全问题,这个是实际开发中需要特别注意的,因为测试环节可能怎么测都测不出来,而一到真实环境里运行一段时间后就可能出错。

问题的原因在于:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致了共享数据的错误。

解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方案。同步代码块:

synchronized(对象)
{
}

可以把括号里的对象视为一个锁,同步锁,有的也翻译成监视器,只有开关两个状态,一个线程读到这个锁先判断开关,开就进来,同时把锁关上这样别的线程不管我这个线程在锁里面墨迹多久有没有执行权都进不来,然后等该线程出去了又执行一个动作:开锁,这样别的线程才能进来,这些都是自动处理的。火车上的卫生间就是一个经典的现实生活中的例子。

同步代码块的括号里需要放入一个对象,以这个对象作为锁的标记,对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使获取CPU的执行权,也进不去,因为没有获取锁。该对象随便用什么都可以,但是为了方便起见都用的是上帝Object的对象。

因为同步需要判断,费时,而且同步的代码不处理完了某一条线程就不允许其他线程进来,实际违背了多线程的宗旨,降低整个系统的效率,因此对于同步需要慎重,能不放进同步里的就不放进去,但是出于安全考虑有些代码不得不同步。

如何判断哪些代码要放到同步代码块中:看看哪些语句在操作共享数据。

必须是多个线程使用同一个锁。几个线程运行不同的互不相关的代码不用锁。

好处:解决了多线程的安全问题。

弊端:多个线程都需要判断锁,较为消耗资源。(有得必有失,但在允许范围内)

如何判断是否应该同步:

1.      明确哪些代码是多线程运行代码。

2.      明确共享数据。

3.      明确多线程运行代码中哪些语句是操作共享数据的。

如果只有单独一条语句,不需要加同步代码。如果操作共同数据的代码有两条或者以上,或者分散到几个地方,这时候也视为多条语句,要用同一个锁并用同步包起来。

同步两种表现形式:除了上面所介绍的同步代码块外还有同步函数(直接在函数前加上synchronized修饰符)。

同步函数没有提供传入对象作为锁的参数,因此就不需要创建Object的对象了,而锁却依赖对象,怎么破?

答曰:非静态函数需要被对象调用,也就是说这些函数其实都有一个对象所属引用,即this. 所以同步函数使用的锁就是this也就是调用它的对象本身。

【需要明确的是】这里所说的调用它的对象是该函数直接所属类的对象(这个对象可以是那个Runnable的子类对象,也就是作为参数传递给Thread类对象的那个对象,往往只创建一个),而不是间接调用该方法的Thread类的对象即线程对象,如果是那样的话,相当于每个线程各自拥有一把锁,就好比每个人各自有一个卫生间,与我们要解决的问题明显不符。

见下例:

class Ticket implements Runnable
{private int ticket = 100;Object obj = new Object();boolean flag = true;public void run(){if (flag){while (true){synchronized(this)//写this就是t对象,和下面的alternativefunction方法共用一个锁,如果写obj就是另一个对象,两个锁。{if (ticket>0){try{Thread.sleep(10);}catch (Exception e){System.out.println("fuck me");}System.out.println(Thread.currentThread().getName()+"code: "+ticket--);}}}}elsealternativefunction();}public synchronized void alternativefunction(){while (true){if (ticket>0){try{Thread.sleep(10);}catch (Exception e){System.out.println("fuck me");}System.out.println(Thread.currentThread().getName()+" function...........: "+ticket--);}}}
}
class TicketDemo3
{public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();try{Thread.sleep(10);}catch (Exception e){System.out.println("fuck me");}t.flag = false;t2.start();}
}

同步方法块里的对象写this就是t对象,和下面的alternativefunction方法共用一个锁,如果写obj就是另一个对象,两个锁。

而如果同步函数被静态修饰后,使用的锁不再是this。因为静态方法中也不可以定义this,静态加载的时候对象还没有进堆内存。静态对应的锁是该类对应的字节码文件对象(类名.class)。

静态进内存时,内存中尚未建立本类对象,但是一定有该类对应的字节码文件对象。对象是:类名.class,该对象的类型是:Class,可以用getClass方法获取到某一类的相应对象。这一点会在反射部分细讲。

因为根据Java创建对象的顺序,在程序运行的时候,类要先封装成一个class类型的对象(字节码文件对象),然后才能调用里面的静态资源。此时内存中有一个对象,就是Ticket.class

总结:静态的同步方法使用的锁是该方法所在类的字节码文件对象,也就是类名.class,这个对象在内存里是唯一的。

死锁

同步中有同步,两个同步如果形成了嵌套就容易死锁。A占着锁1要锁2,而B占着锁2要锁1,二者都因为无法获得所需要的锁,无法释放自己手里的锁,这就是死锁。

比如下例:

class Ticket implements Runnable
{private int ticket = 1000;Object obj = new Object();boolean flag = true;public void run(){if (flag){while (true){synchronized(obj)//占着obj的锁要alternativefunction的this锁{alternativefunction();}}}elsewhile (true)alternativefunction();}public synchronized void alternativefunction()//占着alternativefunction的this锁要obj的锁{synchronized(obj){if (ticket>0){try{Thread.sleep(10);}catch (Exception e){System.out.println("fuck me");}System.out.println(Thread.currentThread().getName()+"code: "+ticket--);}}}
}

线程间通信

多个线程在操作同一个资源,但是操作的动作不同。比如说两台大卡车共同操作同一个煤堆,一个输入新煤,一个拉走煤堆里的煤。这就需要两个卡车协同好了,因为CPU只能在线程间切换的特性,相当于同一时间只能有一个大卡车在作业,当一个运出一次煤它需要让另一个打开车知道并运入煤,它自己在旁等待,这样交替进行,就叫线程间通信。

内部原理:

线程运行的时候内存中会建立一个线程池,这个线程池每个锁各一份,所有在该锁上处于冻结状态的线程都存在线程池当中。调用notify的时候唤醒的都是相应锁上线程池中的线程。当内存池中有多个线程时,唤醒的通常是在该锁上第一个等待的。

wait方法是放弃执行资格,将占用锁的线程冻结,并让该线程在这个锁上等待,同时把线程所占用的锁给释放掉,相当于后面学的unlock。

【注意】wait与sleep的区别:二者都会释放执行权,但wait释放锁,而sleep不释放锁。另外wait属于Object上帝类,而sleep属于Thread类;wait是非静态的,sleep是静态的。

notify()只唤醒某一个锁上等待的一个线程。notifyAll()是唤醒线程池里面所有线程的指令。notify和notifyAll在底层相当于wait(0);

wait notifynotifyAll都只能用在同步锁里面,因为要对持有监视器(锁)的线程操作。wait和notify方法必须要标识出所操作的那个线程所用的锁才能让其冻结或者解锁(当前线程必须拥有此对象监视器)。这时操作的相当于是持有那个锁的线程。用法是:对象名.wait(); 对象名.notify();这里的对象名就是锁。当这些方法前面没有【对象名.】时默认使用调用这个方法的对象,相当于【this.xxx()】,一个线程wait之后会释放它当时持有的锁,并在这个锁上等待,而x.notify只能唤醒x这个锁上的等待的线程,相当于标识。

线程被唤醒后,从当初被冻结wait();的地方开始往下走,而不是从同步代码的顶端。

wait notifynotifyAll这些方法都是操作线程的,却定义在Thread类的父类,也就是上帝Object中,是因为需要通过被对象调用来确定要冻结或者解冻的线程,而可以用作同步锁的对象是任意的,因此可以被任意对象调用的方法须定义在上帝Object中,所有类都能继承。

线程间通信的例子详见下面代码:

class Resource
{private String name;private int NO;private boolean flag = false;public synchronized void set(String name){if (!flag){this.name = name + "--" + NO++;System.out.println(Thread.currentThread().getName()+ "...生产者..." +this.name);flag = true;notifyAll();//为啥用notifyAll?因为需要唤醒对方线程,notify仅仅是该锁上线程池中第一个等待的线程,可是该锁是this对象,也就是唯一的Resource r这个对象,消费者和生产者都使用同一个锁。那么所唤醒的线程有可能是己方线程,而此时flag置为true,那么!flag就为假,就会执行else里面的语句,造成己方线程进来也被冻结的情况。而永远运行不到代码里的notify语句从而无法唤醒下一个线程,造成所有线程都冻结,整个程序挂起。}else{try{wait();}catch (Exception e){}}}public synchronized void get(){if (flag){System.out.println(Thread.currentThread().getName()+ "...消费者..........." +this.name);flag = false;notifyAll();}else{try{wait();}catch (Exception e){}}}
}
class Producer implements Runnable
{private Resource r;Producer (Resource r){this.r = r;}public void run(){while (true){r.set("商品");}}
}
class Consumer implements Runnable
{private Resource r;Consumer (Resource r){this.r = r;}public void run(){while (true){r.get();}}
}
class ProducerConsumerDemo
{public static void main(String[] args) {Resource r = new Resource();Producer p = new Producer(r);Consumer c = new Consumer(r);Thread t1 = new Thread(p);Thread t2 = new Thread(p);Thread t3 = new Thread(p);Thread t4 = new Thread(c);Thread t5 = new Thread(c);Thread t6 = new Thread(c);t1.start();t2.start();t3.start();t4.start();t5.start();t6.start();}
}

JDK1.5新特性

由上面的例子可以看出每次唤醒都需要notifyAll指向不明确的做法很不严谨,而且浪费系统资源,所以考虑能不能指向性地唤醒某一个线程或者某一类线程。这就引出了JDK1.5的升级。

JDK1.5(也叫JDK5.0因为是里程碑式的升级所以版本号都改了)提供了多线程升级解决方案。将同步synchronized替换成显示的Lock操作。以前所学的synchronized同步锁在程序运行到同步代码块或者同步函数的时候判断锁并加锁,在运行完同步代码块或者同步函数后释放锁,这一切都是自动完成的不需要程序猿手动添加。而有了Lock以后我们可以手动上锁及解锁(把隐式变成显式),虽然稍微不太方便了些,但是提高了程序的灵活性。

我们可以查阅API文档中的java.util.concurrent.locks包了解通过Lock建立锁并实现多线程间通信的具体过程。

发现Lock是一个接口,我们先得创建它的对象,所以这里可以用多态的方式用Lock类型指向它的一个子类对象,比如说ReentrantLock。代码如:Lock l =new ReentrantLock();

有了这个对象我们就可以使用里面的加锁lock与解锁unlock方法来替代之前版本的同步功能。

还有一个方法newCondition,这个方法能返回一个返回绑定到此 Lock对象的新Condition对象。这个Condition对象我们可以理解为一个标记,像上面例子里面的许多条线程我们其实可以分为两类,生产者和消费者,我们可以让生产者使用l锁里的c1标记,消费者使用l锁里的c2标记。这样的话,我们既能保证线程安全,同一时间只能有一个线程对共享数据进行操作,又能让我们在唤醒的时候为了避免唤醒同类线程(比如生产者A线程唤醒生产者B线程)而使用notifyAll这种笨办法。而是改良为让生产者线程用c2.signal()方法(替代notify)唤醒对方线程,自己用c1.await()方法(替代wait)进入冻结状态等待被对方线程唤醒。

一个Lock的子类对象可以支持多个相关的 Condition 对象。

Condition中也有与notifyAll相对应的方法signalAll,除此以外还有很多其他方法,详情请查阅API文档。

总结来说,JDK1.5的升级就是将同步Synchronized的锁替换为了Lock接口的子类对象。并将锁上的监视器,也就是继承自Object类中的wait, notify, notifyAll方法,封装成了Condition对象中的await和signal方法。这个Condition对象可以通过Lock锁的newCondition方法行获取。

对上面生产者消费者示例的JDK1.5升级详见下例:

import java.util.concurrent.locks.*;class Resource
{private String name;private int NO;private boolean flag = false;private Lock l = new ReentrantLock();private Condition c1 = l.newCondition();//【注意】这里不是l.new Condition(),这是内部类的写法,而newCondition是Lock里面的一个方法名。private Condition c2 = l.newCondition();public void set(String name) throws InterruptedException{l.lock();try{if (!flag){this.name = name + "--" + NO++;System.out.println(Thread.currentThread().getName()+ "...生产者..." +this.name);flag = true;c2.signal();}else{c1.await();}}finally{l.unlock();//释放锁的动作一定要执行。}}public void get() throws InterruptedException{l.lock();try{if (flag){System.out.println(Thread.currentThread().getName()+ "...消费者..........." +this.name);flag = false;c1.signal();}else{c2.await();}}finally{l.unlock();}}
}
class Producer implements Runnable
{private Resource r;Producer (Resource r){this.r = r;}public void run(){while (true){try{r.set("商品");}catch (InterruptedException e){}}}
}
class Consumer implements Runnable
{private Resource r;Consumer (Resource r){this.r = r;}public void run(){while (true){try{r.get();}catch (InterruptedException e){}}}
}
class ProducerConsumerDemoforJDK5
{public static void main(String[] args) {Resource r = new Resource();Producer p = new Producer(r);Consumer c = new Consumer(r);Thread t1 = new Thread(p);Thread t2 = new Thread(p);Thread t3 = new Thread(p);Thread t4 = new Thread(c);Thread t5 = new Thread(c);Thread t6 = new Thread(c);t1.start();t2.start();t3.start();t4.start();t5.start();t6.start();}
}

在该示例中,实现了本方线程只唤醒对方线程的操作。

多线程的停止

stop方法已经过时。要停止线程只有一种方法,那就是使run方法结束。我们在开启多线程的时候,运行代码通常都是循环结构(如果多线程里面每个run方法里的代码只执行一次的话就没有开启多线程的意义了,单线程也能完成),那么我们只要控制住循环,就可以让run方法结束,也就是线程结束。

具体做法可以设计一个标记,当执行一定次数之后改变标记,再判断标记的时候不满足条件,循环跳出。

特殊情况:

当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。

解决方案:Thread类提供的interrupt();方法,可用于清除因为sleepwait 以及后面出现的join让线程所处于的冻结状态,将处于冻结状态中的线程强制恢复到运行状态,(但是会产生一个interruptedException的异常)这样就可以操作标记让线程结束。

例子详见下面的代码:

class StopThread implements Runnable
{private boolean flag = true;public void run(){synchronized(this){while(flag){try{wait();}catch (InterruptedException e){System.out.println(Thread.currentThread().getName() + "......Exception");flag = false;}System.out.println(Thread.currentThread().getName() + "...... is runing");}}}public void changeflag(){flag = false;}
}class StopThreadDemo
{public static void main(String[] args) {StopThread st = new StopThread();Thread t1 = new Thread(st);Thread t2 = new Thread(st);t1.start();t2.start();int x = 0;while (true){if (x++ == 100){
//              st.changeflag();t1.interrupt();t2.interrupt();break;}System.out.println(Thread.currentThread().getName() + "------" + x);}System.out.println(Thread.currentThread().getName() + " over");}
}

该例子中同时存在着t1线程,t2线程以及主线程main,t1与t2一进线程就进入了wait状态无法读到改变flag标记的代码,无法跳出循环。但是主线程里的【线程.interrupt】方法可以强制让冻结状态的线程恢复到运行状态,同时会抛出一个interruptedException异常,在解决异常的catch代码里面操作flag标记让线程运行完毕。

守护线程(用户线程)

也就是后台线程。我们所看到的线程都是前台线程,当把某些线程标记为后台线程后他们就具备了一些特点:后台线程开启后和前台线程共同抢夺CPU的执行权,当所有前台线程都结束后,后台线程会自动结束。(后台依赖前台)因此省去了interrupt,它自己就能结束。

【注意】结束的意思仅仅就是让守护线程结束,即使后台线程冻结了也能结束,但它里面不蕴含interrupt方法(强制让冻结的线程回到运行状态),因此在结束后台线程的时候不会抛异常。

守护线程的开启方法:调用Thread类的voidsetDaemon(boolean on)方法(传入true参数)。这个方法会产生两个异常(IllegalThreadStateException- 如果该线程处于活动状态。SecurityException - 如果当前线程无法修改该线程。)但他们都是RuntimeException的子类,不用声明。

【注意】守护线程这个方法必须在启动线程前调用。

例详见:

class StopThread implements Runnable
{private boolean flag = true;public void run(){while(flag){System.out.println(Thread.currentThread().getName() + "...... is runing");synchronized(this){try{wait();}catch (Exception e){System.out.println("见鬼了");}}}}
}class StopThreadDaemonDemo
{public static void main(String[] args) {StopThread st = new StopThread();Thread t1 = new Thread(st);Thread t2 = new Thread(st);t1.setDaemon(true);//如果没有这两行守护线程,该线程结束不了。t2.setDaemon(true);t1.start();t2.start();int x = 0;while (true){if (x++ == 100){break;}System.out.println(Thread.currentThread().getName() + "------" + x);}System.out.println(Thread.currentThread().getName() + " over");}
}

join方法

线程之间也有轻重缓急之分,因此在实际开发中会有这样一种需求:让某个不重要的,或者需要等待另一条线程处理结果的线程等待那条线程执行完它才能执行。这就是Thread类中的join方法。

当A线程执行到了B线程的join方法时(B.join();),A就会等待。等B线程都执行完,A才会执行。相当于把自己的执行资格让了出去,而如果此时还有线程C也在执行,那么C不会受到影响,也就是会出现BC交替运行的状态,但只有B运行完A才能运行,与C是否运行完无关。可以理解为一个线程碰到谁的join他就等谁死。

join可以用来临时加入线程执行。但是总体来说Join方法用的频率不太高。

例详见:

class Demo implements Runnable
{int x = 0;public void run(){for (; x<100 ;x++ ){System.out.println(Thread.currentThread().getName()+ "......" +x);}
//      try
//      {
//          wait();
//      }
//      catch (Exception e)
//      {
//          System.out.println("Exception fixed:  "+Thread.currentThread().getName());
//      }}
}
class JoinDemo
{public static void main(String[] args) throws Exception{Demo d = new Demo();Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start();t1.join();//这行代表当前线程(这里是主线程)将执行资格让了出去等待t1线程结束,这个线程才会继续执行。如果注释此行的话,会发现主线程和t1,t2线程交替执行,谁也不让谁。for (int x = 0; x<100 ;x++ ){System.out.println(Thread.currentThread().getName()+ "......" +x);}System.out.println(Thread.currentThread().getName()+ " over");}
}

还有一种情况作为了解:还是上面AB的例子,如果B线程wait了此时整个程序会处于冻结状态,这时要用interrupt强制清除A的冻结状态,但A上会抛出一个异常。

线程的toString()方法把Object类里面的同名方法复写了,打印某个线程的toString()方法输出如下:

Thread[Thread-1,5,main],

解释:

Thread-1:表示线程的名称,这里是默认形式,数字从0开始。

5:含义是优先级,抢资源的频率。在线程类中有方法setPriority (int newPriority)可以设置线程的优先级。与设置守护线程不同,设置优先级可以等到开启线程之后。

【注意】所有线程包括主线程,默认优先级是5.优先级档次:1-10.

在所有优先级中只有1,5,10最明显(4或6与5基本没区别),(其实优先级即使1和10也要运行次数多了才明显)。因此起名为MIN_PRIORITY, NORM_PRIORITY以及MAX_PRIORITY(常量字母全大写)。这里主要是为了阅读性考虑。

【记住】要养成习惯,凡是数据固定的要定义成常量,数据共享的要静态。

main: 含义是该线程所属的线程组:一般情况下,线程被谁开启的,它就属于哪个组。或者可以用ThreadGroup类新建一个对象,把创建好的线程存到这个新组里面去。但是线程组用的概率很低,而且这个类用起来很麻烦。

这个toString方法是打印某个线程的默认方法(也就是说我们打印线程对象默认调用这个方法),它与线程对象的getName()方法是有区别的,后者仅仅打印线程名称。

yield方法

线程类里的yield()方法是静态方法,不需要通过线程对象调用,直接Thread.yield();类名调用即可。

该方法可以稍微暂停下当前正在执行的线程对象,并执行其他线程。使得所有线程能够较为平均地获得CPU执行权。并稍微减缓线程执行的频率(稍微暂停的时间很短,运行完其他线程马上就会恢复)。

toString, Priority, join和 yield的实际应用例子详见:

class Demo implements Runnable
{private int x = 0;public void run(){for (; x<70 ;x++ ){System.out.println(Thread.currentThread().toString()+ "......" +x);Thread.yield();}}
}
class ThreadExample
{public static void main(String[] args) throws Exception{Demo d = new Demo();Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();Thread.sleep(10);t1.setPriority(Thread.MAX_PRIORITY);//设置优先级可以等到开启线程之后,为了看出效果让t1线程开启一会再设置。t2.start();t1.join();for (int x = 0; x<100 ;x++ ){System.out.println(Thread.currentThread()+ "......" +x);}System.out.println(Thread.currentThread()+ " over");}
}

在实际开发中,多线程用的比较多的情境是

主函数里面有许多互相独立的循环,如果依次运行效率比较低,想同时运行,因此创建多线程。但是为了这种情况单独写个类创建对象然后再start太麻烦,因此就用匿名内部类的方式,将需要同步运行的代码封装起来,避免按顺序执行的低效。例如下面:

class ThreadTest
{public static void main(String[] args) {new Thread()//继承Thread+匿名内部类方法{public void run(){for (int x=0; x<90; x++){System.out.println(Thread.currentThread().getName()+".........."+x);}}}.start();new Thread(new Runnable()//实现Runnable+匿名内部类方法{public void run(){for (int x=0; x<90; x++){System.out.println(Thread.currentThread().getName()+".........."+x);}}}).start();for (int x=0; x<90; x++){System.out.println(Thread.currentThread().getName()+".........."+x);}}
}

【最后提一下】在使用多线程的时候我们会发现,有的时候尽管我们的代码没有问题,打印的结果却会有小小的异常。这里我们不用太在意,因为现在许多计算机都是多核处理器,能真正实现多个线程的并发处理,另外DOS中的打印也是一个线程,但是我们却没有控制它,相当于运算和结果的显示不是同步的,因此顺序会有误差。

黑马程序员——多线程相关推荐

  1. [置顶] 黑马程序员 -- 多线程

    黑马程序员 -- 多线程 polk601001 星期五, 15/06/2012 - 17:14 发布 什么是多线程? 多线程就是使程序并发(同时)执行几个操作. .NET 框架类库在System.Th ...

  2. 黑马程序员—————— 多线程

    java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例.每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码).java使用线程执行体来代表这段 ...

  3. 黑马程序员—多线程,单线程

    ------- Windows Phone 7手机开发..Net培训.期待与您交流! ------- 单线程与多线程比较: 单线程只能做一件事情,多线程可以做多种事情.多线程的应用,可以让一个程序同时 ...

  4. 黑马程序员 多线程

    进程:是一个正在执行中的程序. 每一个进程执行都有一个执行顺序.该顺序是一个执行路径,或者叫一个控制单元. 线程:就是进程中的一个独立的控制单元. 线程在控制着进程的执行. 一个进程中至少有一个线程. ...

  5. 黑马程序员——多线程的实现(2+1)详解

    ------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- 方法1:继承Thread类 继承Thread类,重写run方法,把需要被线程执行的代码写到run ...

  6. 黑马程序员——总集篇

    -----------android培训.java培训.java学习型技术博客.期待与您交流!------------ 本人编写技术博客的时候只是针对章节的一些比较重要的知识点来编写的: 个人感觉质量 ...

  7. 黑马程序员,黑马论坛-----多线程知识点总结

    来源:黑马程序员,黑马论坛 创建: 方式一:继承Thread类 步骤: 继承Thread覆写run( )方法 通过Thread子类创建线程对象 调用start( );方法开启线程执行run( ) 方式 ...

  8. 【C++学习汇总】【黑马程序员】

    [C++学习汇总] 1 黑马程序员 2 深蓝学院 3 自发式收集学习 1 黑马程序员 [C++][第一篇][黑马 p84 - p105 ][引用][重载][类和对象-struct.class] [C+ ...

  9. 黑马程序员入学Java知识——精华总结

    黑马程序员入学Java知识--精华总结 J2SE部分,Java高新技术部分,7K面试题部分等黑马入学要求的知识点总结! 一.黑马程序员-java概述与基础知识 6 1.何为编程? 6 2.Java语言 ...

最新文章

  1. 并查集hdu1232
  2. YAML中多行字符串的配置方法总结
  3. 【错误记录】Android NDK 错误排查记录 ( java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader )
  4. python魔术方法由谁定义_Python的魔术方法
  5. Java中dao层、service层、controller层、entity层和view层的概述
  6. 数据可视化怎么完成的_完成期望后会发生什么:可视化育儿数据
  7. java的关键字和保留字_Java关键字和保留字及其含义
  8. Ajax Beta 2.0 中 AtlasToolKit Library 控件 Accordion 后台添加
  9. 4.7清明考试(完蛋)
  10. BERT加速 | 预训练模型参数量越来越大?这里有你需要的BERT推理加速技术指南...
  11. PMP®考试通过率多少
  12. 2022淘宝双十一优惠券如何叠加使用?淘宝双十一优惠券叠加规则介绍
  13. 《那些年啊,那些事——一个程序员的奋斗史》六
  14. 基于labVIEW的学习(一)函数信号发生器
  15. Python 将汉字转为拼音
  16. Ambarella : 一家伟大的视频压缩处理芯片厂商
  17. oracle11gwin8,win8_oracle11g_64位连接32位PLSQL_Developer
  18. 陆奇: 机会是留给广结良友并且时刻有准备的人。
  19. 今年北京将新增城市公园31处 让市民享受高品质绿化
  20. 新西兰java程序员_在新西兰做程序员是一种什么体验?解析新西兰计算机专业...

热门文章

  1. 2019世界区块链大会•乌镇第2日金句集锦
  2. 电商实时交易风控系统
  3. 数据集处理之python生成.lst文件
  4. MPLS 配置远端LDP会话实验 详解
  5. zdm各命令的功能和作用_ZDM命令 注释
  6. MuMu模拟器是干什么用的?MuMu模拟器Mac版对电脑配置要求是什么?
  7. 全局数据共享——MobX(微信小程序)
  8. 微信小程序文本输入<textarea/> 详解
  9. 青少年CTF-弱口令实验室招新赛部分wp复现步骤
  10. uestudio自动补全html代码,UEStudio Suite,强大的代码编辑工具套件