线程&&多线程的实现

一、什么是线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。

一组并发线程运行在一个进程的上下文中,每个线程都有它自己独立的线程上下文,例如:栈、程序计数器、线程ID、条件码、寄存器集合等,每个线程和其它的线程一起共享除此之外的进程上下文的剩余部分。这里有一点要特别注意,就是寄存器是从不共享的,而虚拟存储器总是共享的。

线程和进程的区别:

  • 线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。
  • 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个进程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
  • 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
  • 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是TCB中所保存的线程状态比PCB表中少多了。
  • 进程是系统所有资源分配时候的一个基本单位,拥有一个完整的虚拟空间地址,并不依赖线程而独立存在。

二、实现线程

JAVA语言对线程的支持主要体现在Thread类和Runnable接口上面,因此JAVA实现多线程有两种方法:①实现Runnable接口 ②继承Thread类。

2.1 线程创建及启动

①线程创建

Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target,String name)

②线程启动

void start()

注意别把start()方法和run()方法搞混了。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。

1) start()
用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run()方法运行结束,此线程随即终止。

2) run()
run()方法只是类的一个普通方法而已,如果直接调用run()方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

2.2 获取线程的引用

static Thread currentThread():返回当前运行的线程引用

2.3 停止线程的方法
  如果我们想停止线程,不应该用stop(),因为stop()使得线程戛然而止,完成了什么工作,哪些工作还没有做,都不知道,且清理工作也没有做,所以stop()不是正确的停止线程方法,所以也被弃用了。
  之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run()或者call()方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile布尔变量在线程执行中设置状态标识来退出run()方法的循环或者是取消任务来中断线程。

//volatile是可见性的关键,保证了线程正确读取变量的值
volatile boolean keepRunning = true;

public void run(){
    while(keepRunning){
       //让线程"发动5连击"
       for(int i=0;i<5;i++){
         System.out.println(Thread.currentThread().getName() +"正在运行");
         //让出处理器时间,使得所有线程能同时再去竞争CPU资源从而获取运行的机会
         Thread.yield();
       }
    }
    //完成最后一次业务后跳出while循环后,之后进行一些清理工作
}

线程终止后,其生命周期结束了,即进入死亡态,终止后的线程不能再被调度执行。那么线程退出run()方法,是否代表该线程结束?还是释放完内存之后?答案就是:执行完run之后所有语句=该线程结束≠临时内存被回收。简单来说,就是线程结束就是run方法执行结束之后,线程进入死亡态,他占用的那部分就有可能被回收,但不一定是立即被回收。

2.4 测试线程状态的方法
  可以通过Thread 中的isAlive() 方法来获取线程是否处于活动状态。线程由start() 方法启动后,直到其被终止之间的任何时刻,都处于'Alive'状态。

2.5 线程的暂停和恢复
  有几种方法可以暂停一个线程的执行,在适当的时候再恢复其执行。

1.sleep() 方法
  当前线程睡眠(停止执行)若干毫秒,线程由运行中状态进入不可运行状态,停止执行时间到后线程进入可运行状态。

static void sleep(long millis)
static void sleep(long millis,int nanos)
try{
    Thread.sleep(500);
}catch(InterruptedException e){
   e.printStackTrace();
}

线程休眠要点
①线程休眠总是暂停当前线程
②在被唤醒并开始执行前,线程休眠的实际时间取决于系统计时器和调度器。对比较清闲的系统来说,实际休眠的时间十分接近于指定的休眠时间,但对于繁忙的系统,两者之间的差距就较大。
③线程休眠并不会释放当前线程已经获取的任何锁
④线程休眠并不会丢失当前线程已经获取的任何监视器。
⑤其他线程可以中断当前进程的休眠,但会抛出InterruptedException异常。

2.join()
  join()的作用是:“等待该线程终止”。这里需要理解的就是该线程是指的主线程等待子线程的终止,也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

void join()
void join(long millis)
void join(long millis,int nanos)
//加入join是为了让Stage线程最后停止,如果不加有可能Stage线程结束,actor线程还未停止
//好比导演喊停,演员还在演
public class Stage extends Thread(){
    public void run(){
          ActorRunnable actorTask = new ActorRunnable();
          Thread actor = new Thread(actorTask1,"演员1");
          //当前是Stage和actor两个线程同时竞争CPU资源
          actor.start();
          try{
               actor.join();
          }catch(InterruptedException e){
               e.printStackTrace();
          }
    }
}

3)yield()
  yield()使得当前运行线程释放处理器资源,但并不意味着退出和暂停,只是,告诉线程调度如果有人需要,可以先拿去,我过会再执行,没人需要,我继续执行。但是它的这种主动退出没有任何保障,就是在当前进入可运行状态时,还是有可能被JVM选中再回到运行状态的。

static void yield()
Thread.yield( ):使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。

2.6 使用线程代码示例

public class ActorRunnable implements Runnable{
    volatile boolean keepRunning = true;
    public void run(){
       while(keepRunning){
       //让线程"发动5连击"
       for(int i=0;i<5;i++){
         System.out.println(Thread.currentThread().getName() +"正在运行");
         //让出处理器时间,使得所有线程能同时再去竞争CPU资源从而获取运行的机会
         Thread.yield();
       }
    }
}

public class Stage extends Thread(){
    public void run(){
          //演员1的任务
          ActorRunnable actorTask1 = new ActorRunnable();
          //演员2的任务
          ActorRunnable actorTask2 = new ActorRunnable();
          //Thread中构造方法里面的runnable对象用于提供线程业务接口run方法
          Thread actor1 = new Thread(actorTask1,"演员1");
          Thread actor2 = new Thread(actorTask2,"演员2");
          //actor1、actor2两个线程由本线程启动
          actor1.start();
          actor2.start();
          //让舞台线程暂时休眠。因为在本实例中是actor1、actor2和stage三个线程共同竞争CPU资源的
          //由于当stage竞争到了cpu时就会终止另外两个线程,所以在stage线程中调用了sleep方法来给actor1和actor2方法提供最少50ms的运行时间。
          try{
               Thread.sleep(500);
          }catch(InterruptedException e){
               e.printStackTrace();
          }
          //只有当Thread.yield()的时候,选中的是stage线程,才能执行到下面停止actor的方法。
          //但是actor1停止了并不意味着actor2也会停止,因为执行完actor1.keepRunning = false后,可能stage线程的cpu资源被抢走了
          actor1.keepRunning = false;          
          actor2.keepRunning = false;
    }
}

public void main(String[] args){
     new stage().start();
}

三、Thread VS Runnable

我们刚接触的时候可能会迷糊继承Thread类和实现Runnable接口实现多线程,其实在接触后我们会发现这完全是两个不同的实现多线程。

  • Runnable方式可以避免Thread方式由于java单继承特性带来的缺陷
  • Runnable的代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况;而Thread方式是多个线程分别完成自己的任务。

第二个是什么意思呢?我们用代码来说明。假设到年关了,现在还剩下五张火车票,那现在有三个窗口去卖这五张火车票,我们用三个线程去模拟这三个窗口去同时卖这五张火车票。

用Thread实现卖票系统
class MyThread extends Thread{
   private int ticketsCont = 5; //5张火车票
   private String name ; //窗口,即线程名字

   public MyThread(String name){
       this.name = name;
   }
 
   public void run(){
      while(ticketCont>0){
        ticketCont--;
        System.out.println(name+"卖掉了一张票,剩余票数为:"+ticketCont);
      }
   }
}

public class TicketThread{
  public void main(String[] args){
    //创建三个线程,模拟三个窗口卖票
    MyThread mt1 = new MyThread("窗口1");
    MyThread mt2 = new MyThread("窗口2");
    MyThread mt3 = new MyThread("窗口3");

    //启动三个线程,也即是开始卖票
    mt1.start();
    mt2.start();
    mt3.start();
  }
}

运行结果:总共有5张票,但是却卖了15张票


线程&&多线程的实现_第1张图片
用Runnable实现卖票系统
class MyThread implements Runnable{
   private int ticketsCont = 5; //5张火车票

   public void run(){
      while(ticketCont>0){
        ticketCont--;
        System.out.println(Thread.currentThread().getName()+"卖掉了一张票,剩余票数为:"+ticketCont);
      }
   }
}

public class TicketThread{
  public void main(String[] args){
    MyThread mt = new MyThread();
    //创建三个线程,模拟三个窗口卖票
    Thread th1 = new Thread(mt,"窗口2");
    Thread th2 = new Thread(mt,"窗口2");
    Thread th3 = new Thread(mt,"窗口3");

    //启动三个线程,也即是开始卖票
    th1.start();
    th2.start();
    th3.start();
  }
}

运行结果:正常卖出五张票


线程&&多线程的实现_第2张图片

  在Runnable模拟的方法中,因为MyThread实现Runnable接口的。MyThread mt = new MyThread();之后我们把mt传给三个线程对象,也就是说三个线程传递的对象是同一个Runnable对象,所以三个线程对象用的都是同一个Runnable对象里面的代码,自然而然资源也是共享的,所以它们三个加在一起总共卖了五张火车票。
  但是为什么打印出来剩余票数是4、1、3、0、2呢?因为线程的执行时间是随机的。首先线程1启动,获取到CPU资源,然后转到run方法去执行。执行完之后又把CPU资源让出来,票数剩余4张。这个时候主线程又继续执行,然后线程1没有获取了CPU资源,就在等待,线程2获取到了CPU资源就去执行run方法,剩余票数变成3张......

四、守护线程

JAVA线程有两类:
①用户线程

  • 运行在前台,执行具体的任务
  • 程序的主线程、连接网络的子线程等都是用户线程

②守护线程

  • 运行在后台,为其他前台线程服务
  • 一旦所有用户线程都运行结束,守护线程会随JVM一起结束工作
  • 最常见的守护线程就是垃圾回收线程、数据库连接池中的监测线程、JVM虚拟机启动后的监测线程。

如何设置守护线程?可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程。要注意的是,setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常

public class DeamonThredemo{
  public static void main(String[] args){
    //这里的主线程就是main
    System.out.println("进入主线程"+Thread.currentThread().getName());
    //deamonThread实现了Runnable接口
    DeamonThread deamonThread = new DeamonThread();
    Thread thread = new Thread(deamonThread);
    //将thread设置为守护线程
    thread.setDaemon(true)
    //启动守护线程
    thread.start();
    //主线程工作。在主线程工作的同时守护线程会一直工作,直到主线程退出
    System.out.println("程序退出了主线程"+Thread.currentThread().getName());
  }
}

但不是所有的任务都可以分配给守护线程来运行,比如读写操作或者计算逻辑为什么呢?
  一旦所有的用户线程都退出运行了,守护线程也觉得自己的存在没有必要了,因为它没有守护的对象了,这时守护线程也会结束掉工作。如果我在守护线程里面做了一些读写操作,而当这个读写操作做到一半的时候,所有的用户线程都退出来了,这个时候守护线程也没有存在的必要了,他也会结束掉自己。但是这个时候读写操作还没进行完,守护线程就退出来了,那程序就崩溃了。

你可能感兴趣的:(线程&&多线程的实现)