java的多线程

多线程

进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

 

什么是多线程呢?即就是一个程序中有多个线程在同时执行。

通过下图来区别单线程程序与多线程程序的不同:

单线程:

 

 

多线程:          

 

 

主线程 

回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如下代码演示:

/*

 *进程:进程就是系统中运行的一个应用程序,我们可以打开任务管理器查看进程那一栏

 *

 *线程:进程中包含一个或多个线程,线程控制进程的执行

 *

 *也可以说应用程序中有一个或多个线程

 *

 *main方法其实就是一个线程:所以main方法顺序执行

 *main方法所在的线程叫 主线程

 */

public class Demo01 {

    public static void main(String[] args) {

       System.out.println(1/0);

    }

}

 

我们可以看到 System.out.println(1/0)抛出异常, Exception in thread main,说明该异常是从main线程中抛出的,说明main方法在一个线程中,这个线程就是主线程

 

Thread类概述

该如何创建线程呢?通过API中搜索,查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。

  1. 构造方法

  1. 常用方法

继续阅读,发现创建新执行线程有两种方法。

  1. 一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。
  2. 另一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

     

    创建线程方式一继承Thread类 

创建线程的步骤:

1 定义一个类继承Thread。

2 重写run方法。

3 创建子类对象,就是创建线程对象。

4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

  

   /*回想自定义异常:

 *   Throwable

 *      Exception

 *      Error

 * 自己定义一个类继承Exception/RuntimeException

 * class AgeException extends Exception{

 *

 * }

 * class Frog extends Animal{

 *

 * }
 * 自定义线程思想跟上面一致

 * class 类名  extends Thread{

 *    //重写run方法

 *    public void run(){

 *       //线程要干的事(线程要执行的代码)

 *    }

 * }

 * 执行结果分析:

 *    发现每次打印顺序不同,但是main线程中的代码和自定义线程中的代码一定执行完

 */

public class Demo02 {

    public static void main(String[] args) {

       ThreadDemo01 td=new ThreadDemo01();

       //td.run();//不会开启任何自定义线程,这句话仅仅相当于创建对象调用方法

       td.start();

      

       for(int i=0;i<10;i++){

       System.out.println("main..."+i);

       }

    }

}



public class ThreadDemo01 extends Thread{

    @Override

    public void run() {

       for(int i=0;i<10;i++){

       System.out.println("run..."+i);

       }

    }

}

思考:线程对象调用 run方法和调用start方法区别?

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

 

继承Thread类原理

我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?

继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢?如下代码:

Thread t1 = new Thread();

t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。

Thread t1 = new Thread();
t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。

创建线程的目的是什么?

是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。

对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。

Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。

 多线程的内存图解

每个线程执行的代码都会在一个栈中,CPU在多个线程的run方法的代码中做着随机切换动作

 

 

Thread类的方法

开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。既然找到了,我们就可以试试。

  1. Thread.currentThread()获取当前线程对象
  2. Thread.currentThread().getName();获取当前线程对象的名称
  3. Thread.sleep(long millis);使当前线程睡眠millis毫秒

public class ThreadDemo02 extends Thread{



    @Override

    public void run() {//由于Thread类中run方法没有抛出任何异常

                    //子类继承Thread类重写run方法也不能抛出任何异常

  /*    try{

    Thread.sleep(1000);//1s=1000ms

      }catch(InterruptedException e){

      

      }*/

      

       for(int i=0;i<10;i++){

       System.out.println(getName()+"..."+i);

       }

    }

}

    1.  

创建线程方式—实现Runnable接口

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

为何要实现Runnable接口,Runable是啥玩意呢?继续API搜索。

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。

  1. 接口中的方法

  1. Thread类构造方法

创建线程的步骤。

1、定义类实现Runnable接口。

2、覆盖接口中的run方法。。

3、创建Thread类的对象

4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。

5、调用Thread类的start方法开启线程。

  1. 代码演示:
public class RunnableDemo01 implements Runnable{

    @Override

    public void run() {

     for(int i=0;i<10;i++){

          System.out.println(Thread.currentThread().getName()+"..."+i);

     }

    }

}

 

  1. 自定义线程执行任务类

/*创建线程的第二种方式:

 *   实现Runnable接口

 *   class 类名 implements Runnable{

 *      public void run(){

 *        //线程执行的代码

 *      }

 *   }

 * Thread类中的构造方法:

 *    Thread(Runnable target)



 */

public class Demo01 {

    public static void main(String[] args) {

        RunnableDemo01 rd=new RunnableDemo01();

        /*

         * new Thread()是开启一个线程

         * 传入的rd:告诉线程执行的代码

         * start():JVM调用run方法

         *

         */

        new Thread(rd).start();

       

        for(int i=0;i<5;i++){

            System.out.println(Thread.currentThread().getName()+"..."+i);

        }

    }

}

 

实现Runnable的原理

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。

创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

线程的匿名内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

  1. 方式1:创建线程对象时,直接重写Thread类中的run方法

   

     /*

 *匿名内部类

 *  new 父类名/父接口名(){

 *     //重写父类或父接口的方法

 *  };

 *  上面的创建匿名内部类完成两个操作:

 *   1.底层会创建一个父类或父接口的匿名子类或匿名实现类

 *   2.还会创建这个子类或实现类的对象

 *   class 匿名 extends 父类{

 *  

 *   }

 *   new 匿名();

 */

public class Demo02 {

    public static void main(String[] args) {

       //对继承Thread类使用匿名内部类方式

        Thread t=new Thread(){ //多态

            @Override

            public void run() {

                 for(int i=0;i<10;i++){

                     System.out.println(getName()+"..."+i);

                 }

            }

        };

        t.start();

   

    }

}

 

  1. 方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法

 

 public class Demo02 {

    public static void main(String[] args) {

       

        //对实现Runnable接口采用匿名内部类方式

        Runnable r=new Runnable() {

            @Override

            public void run() {

                 for(int i=0;i<10;i++){

                     System.out.println(Thread.currentThread().getName()+"..."+i);

                 }             

            }

        };

       

        new Thread(r).start();

    }

}

 

  1. 线程安全问题

    1. 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  1. 我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。

需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

  1. 测试类
/*多线程卖票

 *   用三个售票窗口卖100张票

 *   从第100张一直卖到第1张

 *   例如:

 *     A窗口   B窗口  C窗口

 *     100  98   96

 *     99   97   95

 *     .......

 *   用线程模拟售票窗口

 *   线程执行的代码卖票通过打印卖第几张票来模拟

 *

 */

public class Demo01 {

    public static void main(String[] args) {

       //1.创建Runnable的子类对象

        Ticket t=new Ticket();

     

      //2.创建三个线程模拟三个窗口,让它们去卖这100张票

        new Thread(t).start();//Thread-0

        new Thread(t).start();//Thread-1

        new Thread(t).start();//Thread-2

    }

}

  1. 模拟票
public class Ticket implements Runnable{

    //存储卖的票数

    private int ticket=100;



    @Override

    public void run() {

      while(true){

    //打印卖第几张票 

    /*  try {

                Thread.sleep(100);

            } catch (InterruptedException e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }*/



         if(ticket>0){

                System.out.println(Thread.currentThread().getName()+"卖第"

                                 +ticket--+"张");//后置自减,先使用再自减

         }

      }

    }

}

 1 结果

     出现0票,-1票

    

  1.  

    1. 线程安全问题分析

      把睡眠放在if(ticket>0)后面也能有错票产生

    1. 线程安全问题解决

      1. 同步代码块

   

  /*我们需要通过同步代码块来保证共享数据的安全

 *同步代码块

 *  synchronized(锁对象){//锁对象可以是任意对象

 *      //需要保证安全的代码

 *      //我们一般把涉及共享数据代码放在里面

 * 

 *  }

 * 注意事项:保证所有的线程使用同一个锁对象

 *

 */

public class Ticket implements Runnable{

    //存储卖的票数

    private int ticket=100;

   

    Object obj=new Object();//做为锁对象

    @Override

    public void run() {

      while(true){

    //打印卖第几张票 

    /*  try {

                Thread.sleep(100);

            } catch (InterruptedException e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }*/

    synchronized(obj){

         if(ticket>0){

                System.out.println(Thread.currentThread().getName()+"卖第"

                                 +ticket--+"张");//后置自减,先使用再自减

         }

     }

   

      }

    }

}
      1. 同步方法

 /*第二种保证安全的机制:

 *  同步方法格式:

 *  修饰符 synchronized 返回值类型  方法名(){

 *          //代码

 *  }

 *  同步方法也相当于同步代码块,同步方法的锁对象是this

 *  等效代码

 *   修饰符 返回值类型  方法名(){

 *      synchronized(this){

 *           //代码

 *          

 *      }

 *  }

 */

public class Ticket02 implements Runnable {

    // 存储卖的票数

    private int ticket = 100;



    @Override

    public  void run() {

            //Thread-0

            while (true) {

                // 打印卖第几张票

                method01();

            }

     

       

    }

    private synchronized void method01() {

        if (ticket > 0) {

            System.out.println(Thread.currentThread().getName() + "卖第"

                    + ticket-- + "张");// 后置自减,先使用再自减

        }

    }

}

 

  1. 线程池

    1. 线程池概念

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

 

    1. 使用线程池方式--Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。        

  1. Executors:线程池创建工厂类
    1. public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
  2. ExecutorService:线程池类
    1. Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  3. 使用线程池中线程对象的步骤:
    1. 创建线程池对象
    2. 创建Runnable接口子类对象
    3. 提交Runnable接口子类对象
    4. 关闭线程池

代码演示:

package com.whhp.thread04;



import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;



/*创建线程的时机:当我们new Thread()调用start()方法时候会创建线程

 *销毁线程的时机:当线程中的run方法执行完,线程会被销毁

 *创建和销毁动作都比较消耗资源

 *

 *线程池

 *

 */

public class Demo01 {

    public static void main(String[] args) {

        //创建一个线程池,线程池中有三个线程

        ExecutorService es=Executors.newFixedThreadPool(3);

       

        //向线程池中提交任务

        es.submit(new Task());

    }

}

class Task implements Runnable{

    @Override

    public void run() {

       for(int i=0;i<10;i++){

           System.out.println(Thread.currentThread().getName()+"..."+i);

       }

    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值