今天是大年初四,新年里我感触最深的一天,刚刚写完一篇关于java网络编程基础的文章。我就马不停蹄的开始复习之前学习过的多线程。
多线程机制
目录
-
java中的线程
-
Thread类和线程的创建
-
线程的常用的方法
-
线程同步
-
协调同步的线程
-
线程联合
-
GUI线程
-
计时器线程
多线程是java的特点之一,掌握多线程,可以充分利用CPU的资源,更容易解决实际中的问题。1.进程与线程
分为操作系统与进程和进程与线程的辨析
1.1 操作系统与进程
进程是一次程序动态执行的过程,现代操作系统可以同时管理计算机系统中的多个进程,即让多个进程轮流使用CPU资源,甚至让多个进程共享操作系统所管理的资源,比如系统的剪切板。
1.2 进程与线程
线程不是进程,但是行为很像进程,线程是比进程更小的执行单位,一个进程在执行过程中,可能会产生多个线程,形成多条执行线索,每一个线索就是每个线程的产生,存在,消亡。线程之间也可以共享进程中的某些内存单元,比如代码或者数据,并利用这些共享单元实现数据交换,实时通信与必要的同步操作,但是和进程不一样的是,线程的中断与恢复可以更加节省系统的开销,具有多个线程的进程可以更好的解决实际问题。多线程是一项重要的使用技巧。
没有进程就没有线程,没有操作系统就没有进程。
2.java中的线程机制
内置对多线程的支持。计算机在任何一个时刻都只能执行线程中的一个,为了建立这些线程正在同步执行的感觉,java虚拟机需要快速的切换线程,这些线程将被轮流使用,都有机会使用CPU资源。
2.1 主线程
每一个Java应用程序都有一个缺省的主线程,当JVM加载代码时,发现main,就会启动一个线程,是主线程,负责执行main方法,如果在main中有其他的线程,那么jvm就要在主线程和其他线程中轮流切换,保证每个线程都有使用CPU资源的机会。main即使执行完最后的语句,jvm也要一直等到Java应用程序中所有的线程结束后,才会结束java应用程序。
2.2 线程的状态和生命周期
java语言使用Thread类及其子类的对象来表示线程,新建的线程在它的一个完整的生命周期通常要经历如下的4中状态。
-
新建
当一个Thread类的或者子类的对象被声明并创建时,新生的线程对象处于新建的状态,此时就有了相应的内存空间和资源。 -
运行
jvm将cpu使用权切换给该线程,这个线程就可以脱离创建它的的主线程开始自己的周期。
线程创建了仅仅只是占有了内存资源,在jvm管理的线程中还没有这个线程,此线程必须调用start()方法通知jvm,jvm才知道又有一个新线程排队等待切换。 -
中断
有四种原因的中断- jvm将cpu资源从当前线程切换到其他线程了,让本线程让出cpu的使用权,线程处于中断。
- 线程使用cpu资源期间,执行了sleep(int millseccond)方法,使得当前线程进入休眠状态。它是一个类方法,线程一旦执行了这个方法,就会立刻让出cpu的使用权,线程处于中断状态,过了参数指定的毫秒数,该线程就重新进到线程队列中排队等待cpu资源,以便从中断处继续运行。
- 线程使用cpu资源期间,执行了wait()方法,使得当前线程进入等待状态。等待的线程不会进入线程队列中排队等待cpu资源,必须有其他线程调用notify()方法通知它,使得它重新进到线程队列中排队等待cpu资源,以便从中断处继续运行。
- 线程使用cpu资源期间,执行某个操作进入堵塞状态,比如读写操作,进入堵塞的线程不能进入排队队列。只有当引起堵塞的原因消除了,线程才重新进到线程队列中排队等待cpu资源。以便从中断处重新开始运行。
-
死亡
处于死亡的线程不具有继续运行的能力,死亡的原因有两个:一是正常的线程完成了所有工作,结束了run()方法中的全部语句。二是线程被提前强制性的终止了,强制run方法结束。死亡状态就是线程释放了实体,即释放分配给线程对象的内存。2.3 线程调度与优先级
处于就绪状态的线程首先进入就绪队列排队等待cpu资源,jvm中的线程调度器负责管理线程,调度器把线程的优先级分为10个级别,分别使用Thread类中的类常量表示,如果没有明确的设置线程的优先级别,默认是5;
线程的优先级别可以通过setPriority(int grade)方法调整,该方法需要一个int类型参数。
jvm的线程调度器的任务是使高优先级的线程能始终运行,一旦有空闲,则使得具有同等优先级的线程以轮流的方式顺序使用时间片。
在实际的编程中,不提倡使用线程的优先级来保证算法的正确执行。
3 Thread类与线程的创建
分为使用Thread的子类和使用Thread类
3.1 使用Thread的子类
在java语言中,用Thread类或者子类创建线程对象,在编写Thread类的子类时,需要重写父类的run()方法,目的是规定线程的具体操作。
具体实现如下:
SpeakElephant.java
public class SpeakElephant extends Thread{
public void run()
{
操作
}
}
Example.java
speakElephant=new SpeakElephant();
speakElephant.start();
线程开始。
3.2 使用Thread类
使用Thread子类创建线程的优点:可以在子类中增加新的成员变量,有新增加的方法。
另一个创建线程的途径是用Thread类直接创建线程对象。使用Thread创建线程使用的构造方法:Thread(Runnable target)。该构造方法中一个参数是一个Runnable类型的接口。因此必须传递一个实现了接口的类所创建的对象(接口类的实例),该实例对象称作所创建线程的目标对象。当线程调用start()方法时,一旦轮到它来享受cpu资源,目标对象就会自动调用接口中的run方法。用户只需要让线程调用start方法,线程绑定于Runnable接口,也就是说当线程被调度并转入运行状态时,所执行就是run()方法中的操作。
Thread speakElephant;
ElephantTarget elephant;
elephantTarget =new ElephantTarget();
speakElephant=new Thread(elephant);
speakElephant.start();
public class ElephantTarget implements Runnable{
public void run()
{
for(int i=1;i<20;i++)
{
System.out.println("哈哈哈");
}
}
}
我们知道线程间可以共享相同的内存单元,并利用这些共享单元来实现数据交换,实时通信与必要的同步操作。创建目标对象的类在必要时还可以是某个特定对象类的子类。
下面例子是使用Thread类创建两个模拟对象猫和狗的线程,猫狗共享房屋里的一桶水,房屋就是线程额目标对象,猫狗在轮流喝水的过程中,主动休息片刻,而不是等到被强制中断喝水。
代码如下:
package Thread类和线程的创建__共享线程;
public class Example {
public static void main(String [] args)
{
House house=new House();
house.setWater(10);
Thread cat,dog;
dog=new Thread(house);
cat =new Thread(house);
dog.setName("狗");
cat.setName("猫");
dog.start();
cat.start();
}
}
package Thread类和线程的创建__共享线程;
public class House implements Runnable{
int water;
public void setWater(int w)
{
water=w;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
String name=Thread.currentThread().getName();
if(name.equals("狗"))
{
System.out.println(name+"喝水");
water=water-2;
}
else if(name.equals("猫"))
{
System.out.println(name+"喝水");
water=water-1;
}
System.out.println(" 剩 "+water);
try{
Thread.sleep(2000);
}
catch(Exception e)
{
}
if(water<=0)
return ;
}
}
}
这里要注意,一个线程的run方法的执行过程可能随时会被强制中断,理解jvm轮流执行线程的机制。
线程休息, sleep方法。
一个线程的run方法的执行过程中可能随时被强制中断
3.3 目标对象与线程的关系
从对象和对象之间的关系角度看,目标对象和线程的关系有以下的两种情景。
1. 目标对象和线程完全解藕
在上述例子中,创建目标对象的House类并没有组合cat和dog线程对象,也就是说House创建的目标对象不包含对cat和dog对象的引用,在这种情况下,目标对象经常需要通过获得线程的名字(因为无法获得线程对象的引用)
String name=Thread.currentThread().getName();
以便确定哪个线程正在占用cpu资源。
2. 目标对象组合线程
将线程作为目标对象的成员,当创建的目标对象组合线程对象时,目标对象可以通过获得线程对象的引用:Thread.currentThread();来确定哪个线程正在被占用cpu资源。
即就是在类House中,线程dog,cat定义为类House的成员变量,获取当前的线程可以采用这样的方法
Thread t=new Thread.currenThread();
3.4 关于run方法启动的次数
cat和dog是具有相同目标对象的,两个线程,当其中一个线程享用cpu资源时,目标对象自动调用接口中的run方法,当轮到另一个线程享用cpu资源时,目标对象会再次调用接口中的方法。也就是说run方法调用了两次。分别运行在不同的线程中。jvm随时会中断线程的运行。
4.线程常用的方法
- start()
线程调用方法启动线程,使得从新建状态进入就绪队列排队,一旦轮到他来享用cpu资源时,就可以脱离它的线程独立开始自己的生命周期。 - run()方法
Thread类的run方法与Runnable接口中的run方法的功能和作用相同,都用来定义线程对象被调度之后所执行的操作,都是系统自动调用而用户程序不得不引用的方法。系统的Thread类中,run方法没有具体的内容,所以用户需要自己创建自己的Thread类的子类,并重写父类的Run方法,当run方法执行完毕,线程就处于死亡状态。 - sleep(int millsecond)
线程的调度是按照其优先级的高低顺序进行。有时,优先级高的线程需要优先级低的线程做一些配合工作,或者优先级高的线程需要完成一些费时的操作,此时优先级高的的线程需要让出cpu资源,让低优先级的有机会执行,为了达到这个目的,优先级高的线程可以在run方法中调用sleep方法来放弃cpu资源,休眠,如果休眠时被打断,就会报错。因此必须在try catch中调用sleep方法。 - isAlive()
线程处于新建状态时,线程调用isAlive方法返回false。当一个线程调用start方法以后,并且占用cpu资源,该线程的run方法就开始运行了,在该线程run方法结束之前,线程调用isAlive返回true。进入死亡状态时,线程仍然可以调用方法isAlive(),此时返回false。
需要注意,一个已经运行的线程在没有进入死亡状态时,不要在给线程分配实体,由于线程只能引用最后分配的实体,先前的实体就会变成垃圾,并不会被垃圾收集器收集,例如
Thread thread=new Thread(target);
thread.start();
如果线程thread占用cpu资源进入运行,这时在执行
thread=new Thread(target);
那么先前的实体就会变成垃圾,并且不会被垃圾收集器收集,因为jvm认为那个垃圾正在运行状态,如果突然释放,可能会引起错误。
举例如下:
package 多次new实体线程的结果;
public class Example {
public static void main(String [] args)
{
Home home=new Home();
Thread showTime=new Thread(home);
showTime.start();
}
}
package 多次new实体线程的结果;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Home implements Runnable {
int time=0;
SimpleDateFormat m=new SimpleDateFormat("hh:mm:ss");
Date date;
@Override
public void run() {
// TODO Auto-generated method stub
while(true)
{
date=new Date();
System.out.println(m.format(date));
time++;
try
{
Thread.sleep(1000);
}
catch(Exception e)
{
}
if(time==3)
{
Thread thread=Thread.currentThread();
thread=new Thread(this);
thread.start();
}
}
}
}
- currentThread()
这个方法是Thread类中的类方法,可以使用类名调用,该方法返回当前正在使用CPU资源的线程。 - interrupt()
interrupt方法经常用来吵醒休眠的线程,当一些线程调用sleep方法处于休眠状态时,一个占有cpu资源的线程可以让休眠的线程调用这个方法吵醒自己,导致休眠的线程发生错误异常,结束休眠,重新排队等待cpu资源。
举例代码如下:
package interrupt方法实例;
public class Example {
public static void main(String args [])
{
ClassRoom room6501=new ClassRoom();
room6501.student.start();
room6501.teacher.start();
}
}
package interrupt方法实例;
public class ClassRoom implements Runnable {
Thread student,teacher;
public ClassRoom() {
teacher=new Thread(this);
student=new Thread(this);
teacher.setName("王教授");
student.setName("张三");
}
@Override
public void run() {
// TODO Auto-generated method stub
if(Thread.currentThread()==student)
{
try{
System.out.println(student.getName()+"正在睡觉,没听课");
Thread.sleep(1000*60*60); //这个的作用是为了模拟学生睡觉的情景。
}
catch(Exception e)
{
System.out.println(student.getName()+"被老师叫醒来了");
}
System.out.println(student.getName()+"开始听课");
}
else if(Thread.currentThread()==teacher)
{
for(int i=0;i<3;i++)
{
System.out.println("上课");
try{
Thread.sleep(500);
}
catch(Exception e)
{
}
}
student.interrupt();
}
}
}
5.线程同步
java程序中可以存在多个线程,但是在处理多线程问题时,必须注意:当两个或者多个线程同时访问同一个变量,并且一些线程需要修改这个变量,程序应对这样的问题作出处理,否则可能发生混乱。列举一个很简单的例子
一个工资管理人员正在修改雇员的工资,这个时候一些雇员正在领取工资,这样是不存在,雇员得等工资管理人员把工资表修改完毕,才能去领取工资。
线程同步机制:当一个线程A使用synchronized方法时,其他线程想使用这个方法时就必须等待,直到线程A使用synchronized方法。
6. 协调同步的线程
对于同步方法,有时涉及某些特殊情况,比如当一个人在一个售票窗口排队购买电影票,如果他给售票员的钱不是零钱,而售票员有没有零钱找,那么他就必须等待,并允许后面的人买票,以便售票员获得零钱给他。如果第二人还是没有零钱,那么两个人都必须等待,并允许后面的人买票。
也就是说,当一个线程使用的同步方法中用到某个变量,而此变量又需要其他线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()方法,wait()方法可以中断线程的执行,使得本线程等待,暂时让出cpu的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个同步方法的同时,应当使用notifyAll()方法通知所有由于使用这个同步方法而处于等待的线程结束等待,曾中断的线程就会从刚才的中断处继续执行这个同步方法,并遵循先中断先继续的原则。如果使用notify(),那就只是通知处于等待中的线程的某一个结束等待。
wait(),notify(),notifyAll()都是Object类中的final方法,被所有的类继承且不可以重写的方法,特别是在非同步方法中不可以使用这些方法。
7. 线程联合
一个线程A在占有cpu的资源期间,可以让其他线程调用join()和本线程联合,如:
B.join();
我们称作A在运行期间联合了B,如果A在占用cpu资源的期间一旦联合B线程,那么A线程将立刻中断执行,一直等到它联合的B执行完毕,A线程才会再重新排队等待cpu资源,以便恢复执行,如果A准备联合的B线程已经结束,那么B.join()不会有任何效果。
举例代码如下:
package 线程联合;
public class Example {
public static void main(String [] args)
{
ThreadJoin a=new ThreadJoin();
Thread customer=new Thread(a);
Thread cakemaker=new Thread(a);
customer.setName("顾客");
cakemaker.setName("蛋糕");
a.setJoinThread(cakemaker);
customer.start();
}
}
package 线程联合;
public class ThreadJoin implements Runnable {
Cake cake;
Thread joinThread;
public void setJoinThread(Thread joinThread) {
this.joinThread = joinThread;
}
@Override
public void run() {
// TODO Auto-generated method stub
if(Thread.currentThread().getName().equals("顾客"))
{
System.out.println(Thread.currentThread().getName()+"等待"+joinThread.getName()+"制作蛋糕");
try{
joinThread.start();
joinThread.join(); //当前线程开始等待joinThread结束。
}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"买了"+cake.name+"价钱:"+cake.price);
}
else if(Thread.currentThread()==joinThread){
System.out.println(joinThread.getName()+"开始制作生日蛋糕,请等。。。。");
try{
Thread.sleep(2000);
}
catch(Exception e){}
cake=new Cake("生日蛋糕",158);
System.out.println(joinThread.getName()+"制作完毕");
}
}
class Cake{
int price;
String name;
Cake(String name,int price)
{
this.name=name;
this.price=price;
}
}
}
8 GUI线程
当java程序包含图形用户界面时,jvm在运行应用程序时会自动启动更多的线程,其中有两个重要的线程:
AWT-EventQueue和AWT-Windows。
前者负责处理GUI事件,后者负责将窗体或者组件绘制到桌面。jvm会保证各个线程都有使用cpu资源的机会,比如
程序中发生GUI界面事件时,jvm就会将cpu资源切换给前者线程,比如点击了程序中的按钮,出发了ActionEvent事件,后者线程就会立刻排队等待执行处理事件的代码。
9.计时器线程
java提供了一个很方便的Time类,在javax.swing包中,当某些操作需要周期性的执行,就可以使用计时器,构造方法Time(int a ,Object b)创建一个计时器,其中参数a时毫秒,确定计时器每隔a毫秒振铃一次。b是计时器的监视器,计时器发生的振铃事件时ActionEvent事件,当振铃事件发生,监视器就会监视到,监视器就会回调接口中的actionPerformed(ActionEvent e)方法,因此每隔a毫秒振铃发生一次,接口就会回调一次。当我们只想让计时器只回调一次,振铃一次,可以让计时器调用setReapeats(boolean b)方法,b取值false。当我们使用 构造方法Time(int a ,Object b)时,b就自动成为了计时器的监视器,同时负责创建监视器的类必须实现接口Actionlistener。另外,计时器还可以调用setInitialDelay(int depay)设置首次振铃的延时,如果没有设置,默认是a毫秒。
计时器创建后,使用Time类的方法start()启动计时器,使用stop()停止计时器。restart()重启恢复线程。
需要注意的是,计时器的监视器必须是组件类的子类,否则计时器无法启动。
10 守护线程
线程默认是非守护线程,非守护线程也称作用户线程,线程调用void setDaemon(boolean on)可以将自己设置为一个守护线程。
thread.setDaemon(true);
当程序中的所有用户线程都已经结束运行,及时守护线程的run方法还有需要执行的语句,守护线程也会立刻结束运行,我们可以让守护线程做一些不是很重要的工作。
11 应用举例
12 小结
线程创建后仅仅是占有了内存资源,在jvm管理的线程中还没有这个线程,此时线程必须调用start()方法通知jvm。jvm才会知道又有一个新线程等待切换了。