第9次文章:线程

  • 2019 年 10 月 8 日
  • 笔记

终于加班儿赶完了这周的总结,拖延症是要改改了

一、概念:程序 线程 进程

程序:属于我们所写的指令集,是一个静态概念

进程:操作系统调度我们的程序,是一个动态概念

线程:在进程内开辟多条执行路径,每一个线程都可以类似于一个小的进程

二、创建线程

模拟多线程,首先需要创建线程。创建线程的方法主要有以下几种方法:

方法一:继承Thread + run()

启动:创建子类对象 +对象.start()

下面我们模拟一下龟兔赛跑进行分析:

1)首先建立对应的兔子和乌龟类

public class Rabbit extends Thread{  @Override  public void run() {  for (int i=0; i<100; i++) {  System.out.println("兔子跑了"+i+"步");  }  }  }  class Torrit extends Thread{  @Override  public void run() {  for (int i=0; i<100; i++) {  System.out.println("乌龟跑了"+i+"步");  }  }  }

2)下一步我们创建对应的子类对象,并使用对象.start()进行启动对象的线程

public class RabbitApp {  public static void main(String[] args) {  Rabbit rab = new Rabbit();  Torrit tor = new Torrit();          rab.start();//启动兔子线程  tor.start();//启动乌龟线程  for (int i=0; i<100; i++) {  System.out.println("main=====>"+i);  }  }  }

分析:在这个main方法中,我们开启了3条线程,分别对应的是兔子对象rabbit,乌龟对象torrit,以及程序的主线程main方法。在启动对象的线程的时候,一定不能调用我们重写的run()方法!!!如果调用run()方法,就属于普通的方法调用,那么整个cpu会根据程序中的顺序,依次执行每一个线程,并且会等待上一个线程全部执行完之后,才会执行下一段线程。那么我们模拟的就不是多线程了,而是单线程。所以我们在启动的时候需要调用父类对象Thread的start方法。这时,计算机的cpu会按照时间片的分配,同时进行3条线程。这才是我们模拟的多线程。

方法二:实现Runnable + run()

主要步骤有以下几步:

1、类 :实现Runable接口 + 重写run()

2、启动多线程,使用静态代理

1)创建真实角色

2)创建代理角色 Thread +引用

3)代理角色.start()

结合以下代码进行分析:

1)创建一个类对象,并且该类对象需要实现Runnable接口,并且重写run()方法

public class Programer implements Runnable{  @Override  public void run() {  for (int i=0;i<100;i++) {  System.out.println("一边敲helloworld......"+i);  }  }  }

2)启动多线程,使用静态代理

public class ProgramerApp {  public static void main(String[] args) {  //1)创建真实角色  Programer programer = new Programer();  //2)创建代理角色 + 真实角色引用  Thread thread =new Thread(programer);  //3)调用.start() 启动线程  thread.start();  for (int i=0;i<100;i++) {  System.out.println("一边聊qq....."+i);  }  }  }

分析:

(1)将第二种创建多线程的方法和第一种创建多线程的方法进行对比之后,可以发现第二种方法具有明显的优势。在java中,具有单继承多实现的特点,在创建类的时候,我们只能继承一个父类对象,但是在实现接口的时候,我们可以实现多个接口。所以我们使用实现接口的方法来进行多线程的创建,可以给我们未来继承的其他父类预留位置。

(2)与此同时,方法二具有资源共享的特点。这个特点我们再结合下面一段模拟“12306”抢票软件的运行方式进行讲解。

public class Web12306 implements Runnable {  private int num = 10;  @Override  public void run() {  while(true) {  if(num<=0) {  break;  }  System.out.println(Thread.currentThread().getName()+"抢到了第--"+num--+"--张票");  }  }  public static void main(String[] args) {  //创建真实对象  Web12306 web = new Web12306();  //创建代理对象  Thread t1 = new Thread(web,"路人甲");  Thread t2 = new Thread(web,"路人乙");  Thread t3 = new Thread(web,"路人丙");  //启动线程  t1.start();  t2.start();  t3.start();  }  }

查看结果:

路人乙抢到了第--9--张票  路人丙抢到了第--8--张票  路人丙抢到了第--6--张票  路人丙抢到了第--5--张票  路人丙抢到了第--4--张票  路人甲抢到了第--10--张票  路人丙抢到了第--3--张票  路人乙抢到了第--7--张票  路人丙抢到了第--1--张票  路人甲抢到了第--2--张票

分析:如以上场景所示,我们将需要发售的10张车票都放入run()方法中,在外部使用3个线程进行模拟抢票场景。“路人甲”,“路人乙”,“路人丙“,分别代表3个线程,同时启动,然后运行run里面的内容,将10张票进行了资源的共享。

所以综上所述:

在创建的多线程的时候,强烈推荐使用方法二,实现接口的方法,具有“避免单继承的局限性”和“便于资源共享”的优点。

三、线程状态

新生状态、就绪状态、运行状态、阻塞状态、死亡状态

1、停止线程

第一种:自然终止,线程体正常执行完毕

第二种:外部干涉:

1)、线程类中 定义 线程体使用的标识

2)、线程体中使用该标识

3)、对外提供该标识的设置方法

4)、外部根据该条件调用该方法即可

第一种不必过多叙述,下面结合代码详解第二种。

public class StopDemo01 {  public static void main(String[] args) {  Study s = new Study();  new Thread(s).start();//使用匿名类  for(int i = 0;i <= 10 ;i++) {  if(5 == i) {//4)、根据外部条件进行调用stop方法  s.stop();  }  System.out.println("main----->"+i);  }  }  }  class Study implements Runnable {  //1)、线程类中 定义 线程体使用的标识  private boolean flag = true;  @Override  public void run() {  //2)、线程体中使用该标识  while(flag) {  System.out.println("study thread.....");  }  }  //3)、对外提供该标识的设置方法  public void stop() {  this.flag = false;  }  }

分析:在我们重写run()方法的时候,我们使用了一个自己定义的boolean标识符flag,并且提供相应的重设方法。然后在外部进行按需调用,短暂的停止本线程。

2、阻塞状态

(1)join:合并线程,主要是将多条线程合并为一条线程,然后按照顺序依次执行

public class JoinDemo01 extends Thread {  @Override  public void run() {  for (int i =0 ; i<100 ; i++) {  System.out.println("join...."+i);  }  }  public static void main(String[] args) throws InterruptedException {  JoinDemo01 join = new JoinDemo01();//新生  Thread t = new Thread(join);//新生状态  t.start();//就绪状态  //cpu调度 进入运行  for (int i=0 ;i<100;i++) {  if (50 == i) {  t.join();//此时 cpu阻塞了main方法  必须将t执行完之后,才能够重新开始执行main线程  }  System.out.println("main....."+i);  }  }  }

分析:在上述代码块中,我们首先是启动join线程,该线程和main方法同时在cpu进行运行,当我们计数计到50的时候,我们调用了父类Thread中的join方法,然后将join线程对象合并到main线程中,所以此时计算机中只剩下了一条线程,main方法中未执行完的任务被排在了线程对像join后面,等到join全部执行完之后,计算机再重新执行main方法中的剩余部分。

(2)yield:暂停自己的线程,将线程让给其他线程,是一个static方法

public class YieldDemo01 extends Thread{  @Override  public void run() {  for(int i=0;i<100;i++) {  System.out.println("Yield....."+i);  }  }  public static void main(String[] args) {  YieldDemo01 yield = new YieldDemo01();//新生状态  Thread t = new Thread(yield);//新生代理  t.start();//就绪状态  for(int i =0;i<100;i++) {  if(0 == i%20) {  Thread.yield();//暂停main函数  }  System.out.println("mian----->"+i);  }  }  }

分析:在上述代码中,我们将Thread.yield()放在了main方法中,所以当外部条件满足的时候,程序将进入if语句中,执行Thread.yield()语句,然后暂停main方法的线程。

(3)sleep:休眠,此时线程并不不释放锁,本线程也不会被其他线程访问

我们模拟一个与时间相关的倒计时来进行说明:

public class SleepDemo01 {  public static void main(String[] args) throws InterruptedException  {  Date endTime = new Date(System.currentTimeMillis()+10*1000);  long end = endTime.getTime();  while(true) {  //输出  System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(endTime));  //构建下一秒的时间  endTime = new Date(endTime.getTime() - 1000);  //等待一秒  Thread.sleep(1000);//暂停1秒  //10秒以内继续  否则就退出  if(end-10*1000 > endTime.getTime()) {  break;  }  }  }  }

我们查看一下结果:

2019-03-10 21:49:42  2019-03-10 21:49:41  2019-03-10 21:49:40  2019-03-10 21:49:39  2019-03-10 21:49:38  2019-03-10 21:49:37  2019-03-10 21:49:36  2019-03-10 21:49:35  2019-03-10 21:49:34  2019-03-10 21:49:33  2019-03-10 21:49:32

分析:最后的结果是,每间隔一秒钟,打印当前时间的10以内的时间。如果没有使用Thread.sleep()方法,那么在控制台上打印时间的时候,将会在一瞬间完成,打印出上面的结果。加入sleep方法之后,每次打印时间间隔1秒。