java基础——详解多线程机制

今天是大年初四,新年里我感触最深的一天,刚刚写完一篇关于java网络编程基础的文章。我就马不停蹄的开始复习之前学习过的多线程。

			多线程机制

目录

  1. java中的线程

  2. Thread类和线程的创建

  3. 线程的常用的方法

  4. 线程同步

  5. 协调同步的线程

  6. 线程联合

  7. GUI线程

  8. 计时器线程
    多线程是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中状态。

  1. 新建
    当一个Thread类的或者子类的对象被声明并创建时,新生的线程对象处于新建的状态,此时就有了相应的内存空间和资源。

  2. 运行
    jvm将cpu使用权切换给该线程,这个线程就可以脱离创建它的的主线程开始自己的周期。
    线程创建了仅仅只是占有了内存资源,在jvm管理的线程中还没有这个线程,此线程必须调用start()方法通知jvm,jvm才知道又有一个新线程排队等待切换。

  3. 中断
    有四种原因的中断

    1. jvm将cpu资源从当前线程切换到其他线程了,让本线程让出cpu的使用权,线程处于中断。
    2. 线程使用cpu资源期间,执行了sleep(int millseccond)方法,使得当前线程进入休眠状态。它是一个类方法,线程一旦执行了这个方法,就会立刻让出cpu的使用权,线程处于中断状态,过了参数指定的毫秒数,该线程就重新进到线程队列中排队等待cpu资源,以便从中断处继续运行。
    3. 线程使用cpu资源期间,执行了wait()方法,使得当前线程进入等待状态。等待的线程不会进入线程队列中排队等待cpu资源,必须有其他线程调用notify()方法通知它,使得它重新进到线程队列中排队等待cpu资源,以便从中断处继续运行。
    4. 线程使用cpu资源期间,执行某个操作进入堵塞状态,比如读写操作,进入堵塞的线程不能进入排队队列。只有当引起堵塞的原因消除了,线程才重新进到线程队列中排队等待cpu资源。以便从中断处重新开始运行。
  4. 死亡
    处于死亡的线程不具有继续运行的能力,死亡的原因有两个:一是正常的线程完成了所有工作,结束了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才会知道又有一个新线程等待切换了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Devin Dever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值