异常

第一章异常

1.1异常概念
  • 指的是程序在执行过程中,出现的非正常情况,最终会导致JVM的非正常停止
1.2异常的体系
  • 异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Error,java.lang.Exception,平常讲的异常就是指java.lang.Exception
1.3异常分类
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/*
    java.lang.Throwable类是java 语言中所有错误或异常的超类
        Exception:编译期异常,进行编译(写代码)java程序中出现的问题
            RuntimeException:运行期异常,java程序 运行过程中出现的问题,把异常处理掉,程序可以继续执行
        Error:错误
            错误程序不能执行
*/
public class Demo01Exception {
    public static void main(String[] args) {
        //Exception:编译期异常,进行编译(写代码)java程序中出现的问题
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");//用来格式化日期
        Date date = null;//把字符串格式的日期,解析为Date格式的日期
        try {
            date = sdf.parse("1999-2-2");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date);
        System.out.println("===========");
        // RuntimeException:运行期异常,java程序 运行过程中出现的问题,把异常处理掉,程序可以继续执行
        int[] arr=new int[]{1,2,3};
        try{
            System.out.println(arr[3]);
        }catch (Exception e){
            System.out.println(e);//java.lang.ArrayIndexOutOfBoundsException 数组越界异常
        }
        //Error:
        System.out.println("===========================");
        //动态创建一个整型数组
        int[] arr1=new int[1024*1024*1024*1024*1000000];//数组过大
        System.out.println(Arrays.toString(arr1));
        System.out.println("后续代码");

    }
}

1.4异常产生过程解析
public class Demo02Exception {
    public static void main(String[] args) {
        //定义一个数组
        int[] arr={1,2,3};
        int element = getElement(arr, 3);//java.lang.ArrayIndexOutOfBoundsException 异常
        System.out.println(element);
    }
    /*
        定义一个方法,获取数组指定索引处的元素
     */
    public static int getElement(int[] arr,int index){
        int i = arr[index];
        return i;

    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述(img-KZRqLr1c-1591524506888)(C:\myNote\java_learning\异常的产生过程解析.PNG)]

第二章异常的处理

  • java异常处理的五个关键字:try,catch,finally,throw,throws
2.1抛出异常throw
package com.itheima.day16.demo01;
/*
    throw关键字
    作用:
        可以使用throw关键字在指定的方法中抛出异常
    使用格式:
        throw new XXException("异常产生的原因")
    注意:
        1.throw关键字必须写在方法的内部
        2.throw关键字后边new的对象必须是Exception或者Exception的子类
        3.throw关键字抛出指定的异常对象,我们就必须处理这个异常对象
            throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理异常对象,默认交给JVM处理(打印异常对象,中断程序)
                NullPointerException,
                ArrayIndexOutOfBoundsException,都属于RuntimeException子类的对象
            throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try..catch
        工作中,我们首先必须对方法传递过来的参数进行合法性的校验,,如果参数不合法,那么我们就必须使用抛出异常的处理方式,告知方法的调用者,传递的参数有问题
 */
public class Demo03Throw {
    public static void main(String[] args) {
        int[] arr=null;
        int element = getElement(arr, 0);
        System.out.println(element);
    }

    private static int getElement(int[] arr,int index) {
        if(arr==null){
            throw new NullPointerException("传递的数组是null");
        }
        if(index<0||index>arr.length-1){
            throw new ArrayIndexOutOfBoundsException("数据索引越界");
        }
        int i = arr[index];
        return i;
    }
}
2.2Objects非空判断
  • Objects类是由一些静态的使用的方法组成的,这些方法是null-save 或null-tolerant(容忍空指针的),在他的源码中,对对象为null的值进行了抛出异常操作

    public static <T> T requireNonNull(T obj) {
            if (obj == null)
                throw new NullPointerException();
            return obj;
        }
    
    import java.util.Objects;
    
    public class Demo04Objects {
        public static void main(String[] args) {
            method(null);//NullPointerException
        }
    
        private static void method(Objects obj) {
            //Objects.requireNonNull(obj);
            Objects.requireNonNull(obj,"传递的对象为空");
        }
    }
    
    
2.3异常处理的一种方式throws
  • 声明异常:将问题标识出来,报告给调用者,如果方法内通过throw抛出了编译时的异常(写代码的时报错),而没有捕获处理,那么必须通过throws进行声明,让调用着去处理

  • throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)

  • 声明异常的格式

    修饰符 返回值类型 方法名(参数) throws 异常类名,异常类名2
    
  • 代码例子

    import java.io.FileNotFoundException;
    import java.io.IOException;
    
    /*
        throws关键字:异常处理的第一种方式
        作用:
            当方法内部抛出异常对象的时候,那么我们就必须处理这个异常对象
            可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,交给别人处理),最终交给jvm处理,
        使用格式:
            修饰符 返回值类型 方法名(参数列表) throws AAAException..{
                throw new AAAexception("产生原因")
            }
        注意:
            1.throw 抛出的如果是RuntimeException,或者RuntimeException的子类可以不用throws声明异常,直接交给JVM处理(主要包括空指针异常,和数据越界)
            2.throws关键字必须写在方法的声明处
            3.方法内部如果抛出了多个异常,那么throws也必须声明多个异常,如果抛出的多个异常对象中有父子关系,那么直接声明父类对象即可
            4.throws关键字后边声明的异常必须是Exception或者是Exception的子类
            5.调用了一个声明抛出异常的方法,我们就必须处理声明的异常,要么继续throws声明抛出,交给方法的调用者处理,最终JVM处理,要么try...catch自己处理异常
     */
    public class Demo05Throws {
        public static void main(String[] args)throws IOException {
            redFile("d:\\a.txt");//文件名错误!!
        }
        /*
            定义一个方法,对传递的文件路径进行判断
            如果文件名不是c:\\a.txt 抛出FileNotFoundException
            如果文件名后缀不是.txt结尾,抛出IOException
         */
        private static void redFile(String Filename) throws FileNotFoundException,IOException{//注意FileNotFoundException 是IOException的子类
            if(!Filename.equals("c:\\a.txt")){
                throw new FileNotFoundException("文件名错误!!");
            }
            if(!Filename.endsWith(".txt")){
                throw new IOException("文件名后缀不是.txt");
            }
        }
    
    }
    
2.4异常处理的第二种方式,捕获异常
  • 因为我们使用throws声明的异常,如果不进行处理的话最终是JVM进行处理,处理的结果就是中断程序,打印异常结果 ,所以我们需要对异常进行捕获,这样我们后续的代码就能继续执行

  • 例子

    import java.io.FileNotFoundException;
    
    /*
        try...catch:异常处理的第二种方式
        格式:
            try{
            }catch(定义一个异常的变量,用来接受try中抛出的异常对象){
                异常处理的逻辑,异常对象之后,怎么处理异常对象,
                一般在工作中,会把异常的信息记录到一个日志表中
            }
            catch(){
            }
        注意:
            1.try中可以出现多个异常对象,使用多个catch进行捕获异常对象
            2.如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完catch中的逻辑,继续执行try..catch后的代码
                如果try中没有产生异常,那么就执行try中的代码,执行完之后,在try..catch之后的代码
     */
    public class Demo01TryCatch {
        public static void main(String[] args) {
            try{
                readFile("D:\\a.txt");
            }catch (FileNotFoundException e){
                System.out.println("catch代码执行了!!");
            }
            System.out.println("后续代码!!");
    
        }
    
        private static void readFile(String fileName) throws FileNotFoundException{
            if(!fileName.equals("C:\\a.txt")){
                throw new FileNotFoundException("文件没有找到");
            }
        }
    }
    
2.5Throwable中3个异常处理的方法
  • 例子

    import java.io.FileNotFoundException;
    public class Demo01TryCatch {
        public static void main(String[] args) {
            try{
                readFile("D:\\a.txt");
            }catch (FileNotFoundException e){
                //System.out.println("catch代码执行了!!");
                /*
                    Throwable中定义了3个异常处理的方法:
                    String getMessage() 返回此 throwable的简短描述信息
                    String toString() 返回throwable的详细消息字符串
                    void printStackTrace() JVM打印异常对象,默认使用此方法,打印的异常对象是最全面的,包括异常产生的原因,内容,位置等三方面的信息。
                 */
                System.out.println(e.getMessage());//文件没有找到
                System.out.println(e.toString());//java.io.FileNotFoundException: 文件没有找到,重写了Object 的toString()方法,与直接打印e效果一样
                System.out.println(e);//java.io.FileNotFoundException: 文件没有找到
                e.printStackTrace();
    
            }
            System.out.println("后续代码!!");
    
        }
    
        private static void readFile(String fileName) throws FileNotFoundException{
            if(!fileName.equals("C:\\a.txt")){
                throw new FileNotFoundException("文件没有找到");
            }
        }
    }
    
    
2.6finally代码块
  • 有一些特定的代码无论异常是否发生,都需要执行,另外,因为异常会引发程序的跳转,导致有些语句执行不到,而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行

  • finally一般的使用情况:当我们在try语句中打开了一些物理资源(磁盘文件/网络连接/数据库连接),我们都得在使用完之后,最终关闭打开的资源

  • 例子

    import java.io.FileNotFoundException;
    
    /*
        finally代码块
        格式:
            try{
            }catch(异常变量){
            }
            ...
            catch(异常类名 变量){
            }finally{
                无论异常都得执行
            }
     */
    public class Demo02TryCatchFinally {
        public static void main(String[] args) {
            try {
                readFile("D:\\a.txt");
                System.out.println("我想执行以下!!");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }finally {//都会被执行,无论try 中语句是否异常
                System.out.println("资源被释放!!!!");
            }
        }
    
        private static void readFile(String fileName) throws FileNotFoundException{
            if(!fileName.equals("C:\\a.txt")){
                throw new FileNotFoundException("文件没有找到!!");
            }
        }
    }
    
    
2.7异常注意的事项
  • 例子

    import java.util.ArrayList;
    import java.util.List;
    
    /*
        多个异常使用捕获处理方式:
            1.多个异常分别处理
            2.多个异常一次捕获,多次处理,一个try,多个catch注意事项,catch 里面定义的异常变量,如果有子父类有继承的关系,那么子类异常要写在catch上方
            3.多个异常一次捕获一次,一次处理
     */
    public class Demo01Exception {
        public static void main(String[] args) {
            //多个异常分别处理
            /*try{
                int[] arr={1,2,3};
                System.out.println(arr[3]);//ArrayIndexOutOfBoundsException: 3
            }catch(ArrayIndexOutOfBoundsException e){
                System.out.println(e);
            }
           try{
               List<Integer> list = new ArrayList<>();
               list.add(1);
               list.add(2);
               list.add(3);
               System.out.println(list.get(3));//IndexOutOfBoundsException: Index: 3, Size: 3
           }catch(IndexOutOfBoundsException e){
               System.out.println(e);
           }*/
            //多个异常一次捕获,多次处理
           /* try{
                int[] arr={1,2,3};
                System.out.println(arr[3]);//ArrayIndexOutOfBoundsException: 3
                List<Integer> list = new ArrayList<>();
                list.add(1);
                list.add(2);
                list.add(3);
                System.out.println(list.get(3));//IndexOutOfBoundsException: Index: 3, Size: 3
                *//*
                    try中如果出现的异常,会把异常对象抛出给catch处理,抛出的异常对象,会从上到下一次赋值给catch中定义的异常变量
                 *//*
            }catch (ArrayIndexOutOfBoundsException e){//注意ArrayIndexOutOfBoundsException只能写在处理语句块的上面,如果写在下面就没使用过,报错,多态
                System.out.println(e);
            }catch (IndexOutOfBoundsException e){//I
                System.out.println(e);
            }*/
            //多个异常一次捕获,一次处理
            try{
                int[] arr={1,2,3};
                System.out.println(arr[3]);//ArrayIndexOutOfBoundsException: 3
                List<Integer> list = new ArrayList<>();
                list.add(1);
                list.add(2);
                list.add(3);
                System.out.println(list.get(3));//IndexOutOfBoundsException: Index: 3, Size: 3
            }catch(IndexOutOfBoundsException e){
                System.out.println(e);//重写了toString 方法
            }
            System.out.println("后续代码!!");
            //运行时异常可以不处理,既不捕获也不声明抛出
            int[] arr={1,2,3};
            System.out.println(arr[3]);//ArrayIndexOutOfBoundsException: 3
            List<Integer> list = new ArrayList<>();
            list.add(1);
            list.add(2);
            list.add(3);
            System.out.println(list.get(3));//IndexOutOfBoundsException: Index: 3, Size: 3
        }
    }
    
    
  • 如果finally里面有return 语句,那么将会永远返回finally中的结果,应该避免这种情况

    public class Demo02Exception {
        public static void main(String[] args) {
            System.out.println(getA());//100
    }
    
        public static int getA() {
            int a=101;
            try{
                return a;
            }catch (Exception e){
                System.out.println(e);
            }finally {
                a=100;
                return a;
            }
        }
        }
    
    
  • 子父类的异常

    /*
        子父类异常:
            如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者时父类异常的子类,或者不抛出异常
            如果父类没有抛出异常,子类重写父类该方法时也可以不抛出异常,此时子类产生的异常,只能捕获处理,不能声明抛出
        注意:
            总结一句话:父类异常什么样,子类异常就时什么样
     */
    public class Fu {
        //子类重写父类方法时,抛出和父类相同的异常
        public void show01() throws IndexOutOfBoundsException,ClassCastException{}
        //子类重写父类方法时,抛出父类异常的子类
        public void show02() throws IndexOutOfBoundsException{}
        //子类重写父类的方法,不抛出异常
        public void show03() throws IndexOutOfBoundsException{}
        //父类没有抛出异常,子类产生的异常只能进行捕获,不能够声明抛出
        public void show04(){}
    }
    class zi extends Fu{
        @Override
        子类重写父类方法时,抛出和父类相同的异常
        public void show01() throws IndexOutOfBoundsException, ClassCastException {}
        //子类重写父类方法时,抛出父类异常的子类
        public void show02() throws ArrayIndexOutOfBoundsException{}
        //子类重写父类的方法,不抛出异常
        public void show03(){}
        //父类没有抛出异常,子类产生的异常只能进行捕获,不能够声明抛出,这里不要使用运行期异常子类做实验,达不到效果
        public void show04() {
            try {
                throw new Exception();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    
2.8自定义异常
  • 例子

    /*
        自定义异常类:
            java提供的异常类,不够我们使用,需要自己定义一些异常类
        格式:
            public class XXXException extends Exception |RuntimeException{
                添加一个空参数的构造方法
                添加一个带参数异常信息的构造方法
            }
        注意:
            1.自定义异常类一般是以Exception结尾的,说明这是一个异常的类
            2.自定义异常类,必须继承Exception或者RuntimeException
                继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了异常,可以throws声明这个异常,继续往上抛,要么try..catch反正时一定要处理的
                继承RuntimeException:那么自定义的异常类就是一个运行期异常,无须处理,交给虚拟机处理,
     */
    public class RegisterException extends Exception{
        //添加一个空参数的构造方法
        public RegisterException(){
            super();
            
        }
        /*
            添加一个带异常信息的构造方法,查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部会调用父类异常信息的构造方法,让父类来处理这个异常
         */
        public RegisterException(String message){
            super(message);
        }
    }
    
    
  • 自定义异常的综合练习

    public class RegisterException extends Exception{
        //添加一个空参数的构造方法
        public RegisterException(){
            super();
    
        }
        /*
            添加一个带异常信息的构造方法,查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部会调用父类异常信息的构造方法,让父类来处理这个异常
         */
        public RegisterException(String message){
            super(message);
        }
    }
    //
    package com.itheima.day16.demo04;
    
    import java.util.Scanner;
    
    /*
        需求:我们模拟注册操作,如果用户名已存在,则抛出异常并,提示:亲,该用户名已经被注册。
        分析:
            1.使用数组保存已经注册的用户名(数据库)
            2.使用Scanner获取用户输入的注册的用户名(前端)
            3.定义一个方法,对用户输入的注册名进行判断
                遍历存储已经注册过的用户名数组,获取每一个用户名
                使用获取到的用户名和用户输入的用户名进行比较
                    true:
                    用户名已经存在,抛出RegisterException异常,告知用户:"亲,该用户名已经被注册";
                    false:
                        继续遍历比较
                    如果循环结束了,还没有找到重复的用户名,提示用户“恭喜你,注册成功!!”
     */
    public class Demo01RegisterException {
        //1.使用数组保存已经注册的用户名(数据库)
        static String[] userName={"张三","李四","王五"};
    
      /*  public static void main(String[] args) throws RegisterException {
             //2.使用Scanner获取用户输入的注册的用户名(前端)
             Scanner sc = new Scanner(System.in);
            System.out.println("请输入要注册的用户名:");
            String checkName = sc.next();
            checkName(checkName);
        }
            //定义方法进行比较
        private static void checkName(String username) throws RegisterException{
            for (String name : userName) {
                if(name.equals(username)){
                    throw new RegisterException("亲,该用户名已经被注册了!!");
    
                }
            }
            System.out.println("恭喜你成功注册:"+username);
        }*/
      //方式二,使用try catch 捕获异常
      public static void main(String[] args) {
          //2.使用Scanner获取用户输入的注册的用户名(前端)
          Scanner sc = new Scanner(System.in);
          System.out.println("请输入要注册的用户名:");
          String checkName = sc.next();
          checkName(checkName);
      }
    
        private static void checkName(String username) {
            for (String s : userName) {
                if (s.equals(username)){
                    try {
                        throw new RegisterException("亲,该用户名已经被注册了!!");
                    } catch (RegisterException e) {
                        e.printStackTrace();
                        return;//打印完异常信息后退出方法,否则后续代码还会执行
                    }
                }
    
            }
            System.out.println("恭喜你成功注册!!"+username);
        }
    
    }
    //还有一种情况,异常继承自RuntimeException,那么异常可以不用处理,直接交给JVM处理,不需要声明异常,也不需要捕获异常
    
    

第三章多线程

我们在之前,学习的程序在没有跳转语句的前提下都是从上到下依次执行的,那么现在想要设计一个程序,边打游戏边听歌,这就得使用多进程或者多线程来解决。

3.1并发与并行
  • 并发:指两个或者多个事件在同一时间段内发生
  • 并行:指两个或者多个事件在同一时刻发生(同时发生)
  • 注意:单核处理器的计算机肯定不能并行的处理多个任务的,只能时多个任务在单个CPU上并发的运行,同理线程也一样,从宏观上来讲,线程是并行运行的,但是从微观上分析,确是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU的时候,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
3.2线程与进程
  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的一次执行过程,是系统运行程序的基本单位,系统运行一个程序即是一个进程从创建,运行到消亡的过程

  • 进程的例子

    硬盘:永久存储ROM,里面装了各种应用程序,QQ,快播
    内存:临时存储RAM,所有的应用程序都需要进入到内存中进行执行,进入到内存中的程序我们称之为一个一个的进程,把进程从内存中消除,称之为结束进程
    
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程,一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

  • 总结:一个程序运行后至少有一个进程,一个进程可以包含多个线程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来在这里插入图片描述直接上传(img-eQwzeV8K-1591524506892)(C:\myNote\java_learning\线程的概念.PNG)]

  • 线程的调度

    • 分时调度

      所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间

    • 抢占式调度

      优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用的为抢占式调度

  • 抢占式调度方式详解

    大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序,比如我们可以边听歌边看新闻,实际上,CPU(中央处理器)使用抢占式调度模式在多个线程之间进行着高速的切换,(也可以说是进程,毕竟线程是进程中的最小执行单位,是程序中某一模块的),对于CPU的一个核而言,某一时刻,只能执行一个线程,而CPU的在多个线程之间切换速度相比较我们的感觉来说要更快,看上去就是在同一时刻(并发)运行,其实多线程程序并不能提高程序的运行速度,但是能够提高程序的运行效率,让CPU效率更高,CPU使用率提升。

3.3创建线程类
  • java使用java.lang.Thread类代表线程,所有的线程对象,都必须是Thread类或其子类的实例,每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下

    • 定义Thread类的子类,并重写该类的run()方法,该run()方法就代表了线程需要完成的任务,因此把run()方法称为线程的执行体
    • 创建Thread类的子类的实例,即创建了线程对象
    • 调用线程对象的start()方法来启动该线程
  • 主线程

    /*
        主线程:执行主(main)方法的线程
        单线程的程序:Java程序中只有一个线程
        执行从main方法开始,从上到下开始执行
        jvm执行main方法,main方法会进入到栈内存,JVM会找操作系统开辟一条main方法通向CPU的路径
        cpu可以通过这个路径来执行main()方法,而这个路径有一个名字名叫作main()线程,也称之为主线程
     */
    public class Demo01MainThread {
        public static void main(String[] args) {
            Person p1 = new Person("小强");
            p1.run();
            System.out.println(0/0);//单线程的弊端,如果发生异常,后续代码没有执行,后期可以添加多线程解决
            Person p2 = new Person("旺财");
            p2.run();
        }
    }
    //
    public class Person {
        private String name;
    
        public Person() {
        }
    
        public Person(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public void run(){
            //定义一个循环执行20次
            for (int i = 0; i < 20; i++) {
                System.out.println(name+"--->"+i);
            }
        }
    }
    
3.4创建多线程的第一种方式
  • 创建多线程的第一种方式:创建Thread类的子类

        //1.创建Thread类的子类对象
    public class MyThread extends Thread {
        //2.重写Thread类的run()方法
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println("run:"+i);
            }
        }
    }
    //
    package com.itheima.day16.demo06;
    /*
        创建多线程的第一种方式:创建Thread类的子类
        java.lang.Thread类:是描述线程的类,我们要想实现多线程的程序,就必须继承Thread类
        实现步骤:
            1.创建一个Thread类的子类
            2.在Thread类的子类中重写Thread类中的run()方法,设置线程任务(开启线程要做什么?)
            3.创建Thread类的子类对象
            4.调用Thread类中的start()方法,开启新的线程执行run()方法,
                void start()使该线程开始执行,java虚拟机调用该线程的run()方法。
                结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新线程,执行其run()方法)
                多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动
            java程序属于抢占式调度,哪个线程的优先级高,哪个线程就优先执行,同一优先级,随机选择一个执行
     */
    public class Demo01Thread {
        public static void main(String[] args) {
            //3.创建Thread类的子类对象
            MyThread myThread = new MyThread();
           // 4.调用Thread类中的start()方法,开启新的线程执行run()方法,
            myThread.start();
            for (int i = 0; i < 20; i++) {
                System.out.println("main:"+i);
            }
        }
    }
    
3.5多线程的原理
  • 多线程原理_随机打印结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RVpyca6y-1591524506894)(C:\myNote\java_learning\多线程原理_随机打印结果.PNG)]在这里插入图片描述

  • 多线程原理_内存图解

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gv0dDNsh-1591524506896)(C:\myNote\java_learning\多线程原理_多线程内存图解.PNG)]在这里插入图片描述

3.6Thread类的常用方法
  • 获取线程的名称

    /*
        获取线程的名称:
            1.使用Thread类中的方法getName()
                String getName()返回该线程的名称
            2.可以先获取到当前正在执行的线程,使用线程中的getName()方法获取线程名称
                static Thread currentThread()返回当前正在执行的线程对象的引用
     */
    //定义一个新的线程
    public class MyThread extends Thread {
    //2.重写Thread类中的run()方法设置线程任务
        @Override
        public void run(){
            //获取线程的名称方法一
            /*String name = getName();
            System.out.println("当前线程的名字是:"+name);*/
            //获取线程的名字方法二
            System.out.println(Thread.currentThread().getName());
        }
    }
    //
    /*
        线程的名称:
            主线程:main
            新线程:Thread-0,Thread-1,Thread-2
     */
    public class Demo01GetThreadName {
        public static void main(String[] args) {
            //3.创建Thread类的子类对象
            MyThread myThread = new MyThread();
            //调用start()方法,开启线程,执行run()方法
            myThread.start();//Thread-0
            //获取主线程的名字
            System.out.println("主线程的名字:"+Thread.currentThread().getName());
            new MyThread().start();//Thread-1
            new MyThread().start();//Thread-2
    
        }
    }
    
  • 设置线程的名称

    /*
        设置线程名称:
            1.使用Thread类中的方法setName(名字)
                void setName(String name)改变线程名称,使之与参数name相同
            2.创建一个带参数的构造方法,参数传递线程的名称,调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字
                Thread(String name)分配新的Thread对象
     */
    public class MyThread  extends Thread{
        public MyThread(){}
        //带参构造
        public MyThread(String name){
            super(name);
        }
       //重写run()方法设置线程任务
       public void run(){
           //System.out.println("改名前的线程名字:"+getName());
           System.out.println(Thread.currentThread().getName());
       }
    }
    //
    public class Demo01SetThreadName {
        public static void main(String[] args) {
            /*//创建Thread 子类对象
            MyThread myThread = new MyThread();
            //开启线程
            myThread.start();//Thread-0
            myThread.setName("小强");
           // myThread.start();//注意已经开启的线程不能够再次开启,IllegalThreadStateException
            new MyThread().start();//小强*/
            System.out.println("第二种给线程改名的方法................................");
            new MyThread("旺财").start();//旺财
        }
    
    }
    
    
  • Thread类中的sleep()方法

    /*
        public static void sleep(long millis):使当前正在执行的线程已指定的毫秒数进行暂停,毫秒数结束之后,线程继续执行,这是一个静态方法,无需创建对象调用
     */
    public class Demo01Sleep {
        public static void main(String[] args) {
            //模拟秒表
            for (int i = 1; i <= 30; i++) {
                System.out.println("秒数:"+i);
                try {
                    Thread.sleep(1000);//代表1秒,1秒=1000ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    
  • 创建多线程的第二种方式

    //1.创建一个Runnable接口的实现类
    public class RunnableImpl implements Runnable {
    //2.在实现类中重写Runnable接口的run方法设置线程任务
        public void run(){
            for (int i = 0; i < 20; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    //
    /*
        创建多线程程序的第二种方式:实现Runnable接口
        java.lang.Runnable
            Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
        java.lang.Thread类的构造方法
            Thread(Runnable target) 分配新的 Thread 对象。
            Thread(Runnable target, String name) 分配新的 Thread 对象。
        实现步骤:
            1.创建一个Runnable接口的实现类
            2.在实现类中重写Runnable接口的run()方法,设置线程任务
            3.创建一个Runnable接口的实现类对象
            4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
            5.调用Thread类中的start方法,开启新的线程执行run方法
     */
    public class Demo01Runnable {
        public static void main(String[] args) {
            //3.创建一个Runnable接口的实现类对象
            RunnableImpl runnable = new RunnableImpl();
            //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
            Thread thread = new Thread(runnable);
            //5.调用Thread类中的start方法,开启新的线程执行run方法
            thread.start();
            for (int i = 0; i < 20; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    
  • Thread和Runnable的区别

    如果一个类继承Thread,则不适合资源共享,但是如果实现了Runnable接口的话,则很容易实现资源共享。

    总结:

    实现Runnable接口比继承Thread类所具有的优势:

    1. 适合多个相同的程序代码的线程去共享同一个资源
    2. 可以避免java中的单继承的局限性,一个类只能继承一个类,类继承了Thread类就不能继承其他的类,实现了Runnable接口,还可以继承其他的类,实现其他的接口
    3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。实现Runnable接口的方式,把设置线程任务和开启新的线程进行了分离(解耦),实现类中重写了run方法;用来设置线程任务,创建Thread对象,调用start方法,用来开启线程。
    4. 线程池只能放入实现Runnable或Callable类线程,不能直接放入继承Thread的类
    5. 备注:在Java中,每次程序运行至少启动2个线程,一个是main线程,一个是垃圾收集线程,因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程。
3.7匿名内部类来实现线程的创建
  • 例子

    /*
        匿名内部类方式实现线程的创建
        匿名:没有名字
        内部类:写在其他类内部的类
        匿名内部类作用:简化代码
            把子类继承父类,重写父类的方法,创建子类对象合成一步完成
            把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
        匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
        格式:
            new 父类/接口(){
                重写父类/接口中的方法
            };
     */
    public class Demo01InnerClassThread {
        public static void main(String[] args) {
            //线程的父类是Thread
            new Thread(){
                public void run(){
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+i);
                    }
                }
            }.start();
            //线程的接口Runnable
            Runnable r=new Runnable(){
                public void run(){
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"程序员");
                    }
                }
            };
            new Thread(r).start();
            //简化版本
            System.out.println("=========");
            new Thread(new Runnable(){
                public void run(){
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"黑马");
                    }
                }
            }).start();
        }
    }
    
3.8线程安全
  • 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  • 线程安全代码实现

    public class RunnableImpl implements Runnable {
        //设置线程任务
        private int ticket=100;
        public void run(){
            while (true) {
                if (ticket != 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
    ///
    /*
        模拟卖票案例,创建三个线程
     */
    public class Demo01Ticket {
        public static void main(String[] args) {
            //创建Runnable接口实现类对象
            RunnableImpl runnable = new RunnableImpl();
            //创建Thread对象,构造方法中传入Runnable接口的实现类对象
            Thread t0 = new Thread(runnable);
            Thread t1 = new Thread(runnable);
            Thread t2 = new Thread(runnable);
            //调用start方法开启线程
            t0.start();
            t1.start();
            t2.start();
        }
    }
    
  • 线程安全产生的原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xbu8H0NL-1591524506898)(C:\myNote\java_learning\线程安全产生的原理.PNG)]在这里插入图片描述

  • 解决线程安全问题

    当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题,也就是解决重复票与不存在票问题,java中提供了同步机制(synchronized)来解决。

    三种同步操作:

    1. 同步代码块
    2. 同步方法
    3. 锁机制
    • 同步代码块

      /*
          买票问题出现了线程安全问题
          卖出了不存在的票和重复的票
          解决线程安全问题的一种方案:使用同步代码块
          格式:
              syschronized(锁对象){
                  可能会出现线程安全问题的代码(访问了共享数据的代码)
              }
          注意:
              1.同步代码块中的锁对象,可以使用任意的对象
              2.但是必须保证多个线程使用的锁对象是同一个
              3.锁对象作用:
                  把同步代码块锁住,只让一个线程在同步代码块中执行
       */
      public class RunnableImpl implements Runnable {
          //设置线程任务
          private int ticket=100;
          Object obj =new Object();//注意这个锁对象只能出现在run()方法外面,要保证锁对象唯一
          //设置线程任务
          public void run(){
              //使用死循环,让卖票一直执行
              while (true) {
              synchronized (obj){
                  //先判断票存不存在
                  if (ticket != 0) {
                      //提高线程安全出现的概率,让线程睡眠
                      try {
                          Thread.sleep(10);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
                      ticket--;
                  }
              }
              }
          }
      }
      //
      /*
          模拟卖票案例,创建三个线程
       */
      public class Demo01Ticket {
          public static void main(String[] args) {
              //创建Runnable接口实现类对象
              RunnableImpl runnable = new RunnableImpl();
              //创建Thread对象,构造方法中传入Runnable接口的实现类对象
              Thread t0 = new Thread(runnable);
              Thread t1 = new Thread(runnable);
              Thread t2 = new Thread(runnable);
              //调用start方法开启线程
              t0.start();
              t1.start();
              t2.start();
          }
      }
      
      
    
  
- 同步技术的原理

  使用了一个锁对象,这个锁对象叫同步锁,也叫对象所,也叫对象监视器,3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票,

t0抢到了cpu的执行权,执行run方法(线程任务),遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步代码块中执行
t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象,发现没有,t1就会进入到阻塞状态,会一直等待t0对象释放锁对象,等到t0线程执行完同步代码块的代码后,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行
总结:
同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步
同步保证了只能有一个线程在同步中执行共享数据,保证了安全
但是要注意的是:程序频繁的判断锁,获取锁,释放锁,程序的效率会降低


- 解决线程同步的第二个方法

- 同步方法,锁对象是this,也就是Runnable 实现类对象
- 静态同步方法,锁对象是本类的class属性

```java
/*
    卖票案例出现了线程安全问题
    卖出了不存在的票和重复的票
    解决线程安全的第二种方案:使用同步方法
    使用步骤:
        1.把访问了共享数据的方法抽取出来,放到一个方法中
        2.在方法上添加synchronized修饰符
    格式:定义方法的格式
    修饰符 synchronized 返回值类型 方法名(参数列表){
        可能会出现线程安全的代码(访问了共享数据的代码)
    }
 */
public class RunnableImpl implements Runnable {
    //设置线程任务
    private static int ticket=100;
    public void run(){
        //使用死循环,让卖票动作重复执行
        while (true) {
            payTicket();
            }
        }


     /*
        静态的同步方法
        锁对象是本类的class属性-->class文件对象(反射)
      */
     public static synchronized void payTicketStatic(){
        //第一种方法体
         /*
         //先判断票存不存在
         if (ticket != 0) {
             //提高安全问题出现的概率,让程序睡眠
             try {
                 Thread.sleep(10);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
             ticket--;

         }
         */
         //第二种方法体,为了验证锁对象是本类的class属性
         synchronized (RunnableImpl.class){
             //先判断票存不存在
             if (ticket != 0) {
                 //提高安全问题出现的概率,让程序睡眠
                 try {
                     Thread.sleep(10);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
                 ticket--;

             }
         }
     }
    /*定义一个同步方法:
        同步方法也会把方法内部的代码给锁住
        只让一个线程执行
        同步方法的锁对象就是实现类的对象new RunnableImpl()
        也就是调用线程任务的方法的对象 this
     */
    public synchronized void payTicket(){
        //先判断票存不存在
        if (ticket != 0) {
            //提高安全问题出现的概率,让程序睡眠
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
            ticket--;
        }
    }
    }

//
/*
    模拟卖票案例,创建三个线程
 */
public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnable接口实现类对象
        RunnableImpl runnable = new RunnableImpl();
        //创建Thread对象,构造方法中传入Runnable接口的实现类对象
        Thread t0 = new Thread(runnable);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        //调用start方法开启线程
        t0.start();
        t1.start();
        t2.start();
    }
}

  • 解决线程安全的第三种方式:使用Lock锁

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /*
        卖票案例出现了线程安全问题
        卖出了不存在的票和重复的票
        解决线程安全的第三种方案:使用Lock锁
        java.util.concurrent.locks.Lock接口
        Lock实现提供了比使用synchronized方法和语句可获取的更广泛的锁定操作
        Lock接口中的方法:
            void lock()获取锁
            void unlock()释放锁
        java.util.concurrent.locks.ReentrantLock implements Lock接口
        使用步骤:
            1.在成员位置创建一个ReentrantLock对象
            2.在可能会出现线程安全问题的代码前调用Lock接口中的Lock()获取锁
            3.在可能会出现安全问题的代码后调用Lock接口中的unlock()释放锁
     */
    public class RunnableImpl implements Runnable {
        //1.在成员位置创建一个ReentrantLock对象(使用多态)
        Lock l = new ReentrantLock();
        //设置线程任务
        private static int ticket=100;
        //Lock锁的第一种使用方式
       /* public void run(){
            //使用死循环,让卖票动作重复执行
            while (true) {
                //2.在可能会出现线程安全问题的代码前调用Lock接口中的Lock()获取锁
                l.lock();
                //先判断票存不存在
                if (ticket != 0) {
                    //提高安全问题出现的概率,让程序睡眠
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
                    ticket--;
                }
                //3.在可能会出现安全问题的代码后调用Lock接口中的unlock()释放锁
                l.unlock();
                }
    
            }*/
        //Lock锁的第二种使用方式,把释放锁放在finally语句块中,无论程序是否异常,都会把锁释放,推荐使用这种方式
        public void run(){
            //使用死循环,让卖票动作重复执行
            while(true){
                //2.在可能会出现线程安全问题的代码前调用Lock接口中的Lock()获取锁
                l.lock();
                //先判断票存不存在
                if(ticket!=0){
                    try {
                        Thread.sleep(10);
                        System.out.println(Thread.currentThread().getName()+"正在卖" + ticket + "张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        //3.在可能会出现安全问题的代码后调用Lock接口中的unlock()释放锁
                        l.unlock();//无论程序是否异常,都会把锁释放
                    }
                }
            }
        }
        }
    
    //
    /*
        模拟卖票案例,创建三个线程
     */
    public class Demo01Ticket {
        public static void main(String[] args) {
            //创建Runnable接口实现类对象
            RunnableImpl runnable = new RunnableImpl();
            //创建Thread对象,构造方法中传入Runnable接口的实现类对象
            Thread t0 = new Thread(runnable);
            Thread t1 = new Thread(runnable);
            Thread t2 = new Thread(runnable);
            //调用start方法开启线程
            t0.start();
            t1.start();
            t2.start();
        }
    }
    
    

第四章线程的状态

4.1线程的6大状态
  • NEW(新建状态)
    至今尚未启动的线程处于这种状态。

  • RUNNABLE(运行状态)
    正在 Java 虚拟机中执行的线程处于这种状态。

  • BLOCKED(阻塞状态)
    受阻塞并等待某个监视器锁的线程处于这种状态。 阻塞状态具有CPU的执行权,等待CPU空闲时执行

  • WAITING(无限永久等待状态)
    无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。

  • TIMED_WAITING(休眠状态)
    等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 放弃CPU的执行资格,cpu空闲也不执行。

  • TERMINATED(死亡状态)
    已退出的线程处于这种状态。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zmNTA6oE-1591524506900)(C:\myNote\java_learning\线程状态图.PNG)]在这里插入图片描述

4.2计时等待(timed waiting)

为了减少线程执行的速度,使线程暂停执行,使用sleep()方法,使线程休眠,达到,减慢线程的作用

例子

//秒表代码
public class Demo01Sleep {
    public static void main(String[] args) {
        //模拟秒表
        for (int i = 1; i <= 30; i++) {
            System.out.println("秒数:"+i);
            try {
                Thread.sleep(1000);//代表1秒,1秒=1000ms
                System.out.println(Thread.currentThread().getName());//输出当前主线程的名字
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
4.3锁阻塞状态(BLOCKED)

一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如线程A和线程B代码中使用的是同一锁对象,如果线程A获取到锁对象,线程A进入到运行状态(RUNNABLE),线程B就进入到阻塞状态(Blocked),代线程A释放完锁,线程B进入到运行状态。

4.4无限等待状态(WAITING)

案例介绍

消费者和生产者之间的通信,消费者去包子铺买包子,告诉老板要吃啥包子和数量,然后顾客就等着老板开始做包子(调用wait()方法),进入无限等待状态(Waiting),老板开始做包子,做完包子告诉消费者,(调用notify()方法),告诉顾客包子做好了,趁热吃。一个等待状态,一个唤醒,这就是线程的通信

代码实现

/*
    等待唤醒案例:线程之间的通信
        创建一个顾客线程(消费者):告知老板要的包子数量和种类,调用wait()方法,放弃cpu的执行,进入到waiting状态,
        创建一个老板线程(生产者):花了5秒中做包子,做好包子后调用notify()方法,唤醒顾客吃包子
    注意:
        顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行,因为调用wait()方法和ntify()方法必须使用同一个锁对象
        同步使用的锁对象必须保证唯一
        只有锁对象才能调用wait()和notify()方法
        注意遇到锁后并不是进入阻塞状态了,因为调用了wait方法所以进入到waiting状态
    Object类中的方法:
    void wait(),在其他线程调用此对象的notify()方法或者notifyALL()方法前,导致当前线程等待
    void notify(),唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码
 */
public class Demo01WaitandNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        //创建一个顾客线程,使用Thread 类及其子类创建
        new Thread(){
            public void run(){
                //保证等待和唤醒的线程只有一个,需要使用同步代码块,调用wait()方法是这个线程进入到waiting状态
                synchronized (obj){
                    System.out.println("告诉老板要吃的包子个数和种类:");
                    System.out.println(Thread.currentThread().getName());//打印下当前线程的名字
                    try {
                        //锁对象调用wait()方法,让顾客进入到无限等待状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }//唤醒后执行的代码
                    System.out.println("包子已经做好了,开吃"+Thread.currentThread().getName());
                }
            }
        }.start();
        //使用Runnable接口创建线程老板
        new Thread(new Runnable() {
            @Override
            public void run() {
                    //花费五秒中做包子
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                synchronized (obj){
                    System.out.println("老板5秒中之内做好了包子"+Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName());//打印下当前线程的名字
                    //唤醒顾客
                    obj.notify();
                }
            }
        }).start();
    }
}

wait(long m)和notifyAll()方法

/*
    进入到TimeWaiting(计时等待)有两种方式
        1.使用sleep(long m)方法,在毫秒值介乎是之后,线程睡醒进入到Runnable/Blocked状态
        2.使用wait(long m),wait(long m)后还没有被唤醒,还没有被notify()唤醒,就会自动醒来,线程进入到Runnable/Blocked状态
    唤醒方法
        void notify()唤醒此对象监视器上的单个线程
        void notifyAll(),唤醒在此监视器上等待的所有线程
 */
public class Demo02WaitandNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        //创建一个顾客线程,使用Thread 类及其子类创建
        new Thread(){
            public void run(){
                while(true){
                    //保证等待和唤醒的线程只有一个,需要使用同步代码块,调用wait()方法是这个线程进入到waiting状态
                    synchronized (obj){
                        System.out.println("告诉老板要吃的包子个数和种类:");
                        System.out.println(Thread.currentThread().getName());//打印下当前线程的名字
                        try {
                            //锁对象调用wait(long m)方法,让顾客等待5秒,5秒后自动醒来
                            obj.wait(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }//唤醒后执行的代码
                        System.out.println("包子已经做好了,开吃"+Thread.currentThread().getName());
                    }
                }
            }
        }.start();
    }
}

第五章等待唤醒机制

5.1线程之间通信

多个线程在处理同一个资源,但是处理的动作(线程任务)却不相同

比如:线程A是用来生产包子的,线程B是用来吃包子的,包子可以理解为同一个资源,线程A和线程B处理的东顾总,一个是生产一个是消费,那么线程A和线程B就存在线程通信的问题。

为什么要处理线程通信:

多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务的时候,并且希望它们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮我们达到多线程共同操作一份数据的目的。

如何保证线程通信有效的利用资源

多个线程在处理同一资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程能够有效的利用资源,而这个手段就是–等待唤醒机制。

5.2等待唤醒机制

等待唤醒机制

这是多个线程间的一种协作机制,谈到线程我们经常想到的时线程之间的竞争(race),比如去争夺锁,但是这并不是故事的全部,线程之间也会有协作机制,就好比在公司你和你的同事,你们可能存在晋身时的竞争,但是更多的时候你们时一起合作完成某些任务。

就是在一个线程进行规定操作之后,就进入等待状态(wait()),等待其他线程执行完它们的代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待的线程。

wait/notify就是线程间的一种协作机制。

等待唤醒中的方法

等待唤醒机制就是用于解决线程间的通信的问题的,使用到的三个方法的含义如下:

  1. wait():线程不在活动,不在参与调度,进入到wait set中,因此不会浪费cpu资源,也不会去竞争锁了,这时的线程状态即是WAITING,它还要等者别的线程执行一个特别的动作,也就是通知(notify),在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(read queue)中

  2. notify:则选取所通知对象的wait set中的一个线程释放:例如,餐馆有空位置后,等待就餐时间最久的顾客最先入座。

  3. notifyAll:则释放所通知对象的wait set上的全部线程

    注意:
    哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试的去获取锁对象,(很可能面临其他线程的竞争,竞争cpu的使用权),成功后才能在当初调用wait方法之后的地方恢复执行
    总结如下:
    1.如果能够获取锁,线程就从waiting 状态变成RUNNABLE状态
    2.否则,从wait set出来,又进入entry set,线程就从WAITING 状态又变成BLOCKED状态,这个时候如果要再次进入RUNNABLE状态,就要和其他线程一起竞争cpu的执行权了
    

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法的进程。void notify:唤醒在此对象监视器(线程锁)等待的单个线程,所以wait,notify必须要由同一个锁对象调用。
  2. wait方法与notify方法是属于Object类的方法的。因此:锁对象是可以是任意对象,而任意对象的所属类都是继承了Object类。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数(方法)中使用,因为:必须要通过锁对象调用这2个方法。

等待唤醒机制需求分析

资源:包子
线程A:生产者,包子铺,线程执行的动作是生产包子
线程B:消费者,线程执行的动作是吃包子
需求分析:
资源类:包子类 需要设置包子的属性 (皮,馅,包子的状态,有true,没有false)
线程类:
	生产者(包子铺)类,是一个线程类,可以继承Thread,需要设置线程任务(run):生产包子,对包子的状态进行判断,true:有包子,包子铺调用wait方法进入等待状态,false:没有包子,包子铺生产包子,增加一些趣味性,交替生产两种包子,有两种状态(i%2==0)包子铺生产好了包子,修改包子的状态为true,唤醒吃货线程(消费者),让吃货线程吃包子
	消费者(吃货)类:是一个线程类,可以继承Thread,设置线程任务(run):吃包子,首先对包子的状态进行判断,false:没有包子,吃货线程调用wait方法进入等待状态,true:有包子,吃货吃包子,吃货吃完包子,修改包子的状态为false,没有包子,吃货唤醒包子铺线程,生产包子。

等待唤醒机制代码实现

/*
    资源类:包子类
    设置包子的属性:
        皮
        馅
        包子的状态:有true 没有flag
 */
public class BaoZi {
    //皮
    String pi;
    //馅
    String xian;
    //包子的状态,设置初始值,为没有包子
    boolean flag=false;
}
/
/*
    生产者(包子铺)类:是一个线程类,可以继承Thread
    设置线程任务(run):生产包子
    对包子的状态进行判断
    true:有包子
        包子铺调用wait方法进入等待状态
    false:没有包子
        包子铺生产包子
        增加一些趣味性:交替生产两种包子
            有两种状态(i%2==0)
        包子铺生产好了包子
        修改包子的状态为true 有
        唤醒吃货线程,让吃货线程吃包子
    注意:
        包子铺线程和包子线程的关系-->通信(互斥)
        必须使用同步技术保证两个线程只能由一个在执行
        锁对象必须保证唯一,可以使用包子对象作为锁对象
        包子铺类和吃货的类就需要把包子对象作为参数传递进来??
            1.需要在成员位置上创建一个包子变量
            2.使用带参数的构造方法,为这个包子变量赋值
 */
public class BaoZiPu extends Thread{
   // 1.需要在成员位置上创建一个包子变量
    private BaoZi bz;
    //2.使用带参数的构造方法,为这个包子变量赋值

    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }
    //3.设置线程任务(run):生产包子
    @Override
    public void run(){
        //定义包子的起使数量
        int count=0;
        //让包子铺一直生产包子
        while(true){
            // 必须使用同步技术保证两个线程只能由一个在执行
            synchronized (bz){
                //对包子的状态进行判断
                if(bz.flag==true){
                    try {
                        //如果有包子,进入等待状态
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒后,包子铺生产包子
                //增加一些趣味性
                if(count%2==0){
                    //生产 韭菜鸡蛋馅包子
                    bz.pi="薄皮";
                    bz.xian="韭菜鸡蛋馅";
                } else{
                    bz.pi="冰皮";
                    bz.xian="牛肉大葱馅";
                }
                count ++;
                System.out.println("包子铺正在生产第"+count+"个包子,它是"+bz.pi+bz.xian+"的");
                //制作包子耗时3秒一个
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("生产完包子了");
                //修改包子的状态
                bz.flag=true;
                //唤醒吃货线程,让吃货线程吃包子
                bz.notify();
                System.out.println("包子铺已经生产好第"+count+"个包子,它是"+bz.pi+bz.xian+"的,吃货趁热吃吧!!");

            }
        }

    }
}
/
/*
    消费者(吃货类):是一个线程类,可以继承Thread
    设置线程任务()润:吃包子
    对包子的状态进行判断
    false:没有包子
        吃货调用wait()方法进入等待状态
    true:有包子
        吃货吃包子
        吃货吃完包子修改包子的状态为false,没有,吃货唤醒包子铺线程,生产包子
 */
public class ChiHuo extends Thread{
    //1.需要在成员变量上创建一个包子变量
    private BaoZi bz;
    //2.使用带参的构造方法为这个包子变量赋值

    public ChiHuo(BaoZi bz) {
        this.bz = bz;
    }
    //设置线程任务
    @Override
    public void run(){
        while(true){
            synchronized (bz){
                //对包子的状态进行判断
                if(bz.flag==false){
                    //如果包子的状态为false则进入等待状态
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒后吃包子
                System.out.println("吃货正在吃"+bz.pi+bz.xian+"的包子");
                //吃货吃完包子
                //修改包子的状态
                bz.flag=false;
                //这个时候没有包子了要唤醒包子铺做包子
                bz.notify();
                System.out.println("吃货已经把"+bz.pi+bz.xian+"的包子吃完了,包子铺开始生产包子");
                System.out.println("================================================================");
            }

        }
    }
}
/
/*
    我们把要操作的资源作为锁对象,保证包子铺和包子的锁对象一致,然后把锁对象,作为成员变量,利用带参的构造方法将成员变量存进去
 */
public class Demo {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi();
        //创建包子铺线程
        new BaoZiPu(bz).start();
        new ChiHuo(bz).start();
    }
}

第六章线程池

6.1线程池的引入

我们在使用线程的时候去创建一个线程,这样实现起来非常简便,但是就会存在一个问题:如果并发的线程数量很多,并且每一个线程都是执行一个时间很短的任务就被结束,这样频繁的创建线程就会大大降低系统的效率,因为频繁的创建和销毁线程都是需要时间的。那么有没有一种办法可以使得线程可以服用,就是执行完一个任务,并不销毁,而是可以继续执行其他的任务,在java中我们可以通过线程池来达到同样的效果。(我们可以类比工厂上的一条流水线,这个流水线我们可以看作是一个进程,这个流水线上的工人可以看作一个一个的线程,这些线程之间互相协作,完成某个任务,但是为了提高流水线的效率,我们可以把这些工人培养成多工的工人,可以让它适应不同岗位,减少人才的流失,那里少人,哪里补上)

6.2线程池概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复重复的里利用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源。线程池的很多操作都是与优化资源相关的。

6.3线程池的底层原理
在jdk1.5之后,JDK内置了线程池,我们可以直接使用
线程池--->集合(ArrayList,HashSet,**LinkedList<Thread>**,HashMap)
当程序第一次启动的时候,创建多个线程,保存到一个集合当中,
当我们想要使用线程的时候,就可以从集合中取出线程来使用
Thread t=Linked.removeFirst();
当我们使用完毕,需要把线程归还给线程池
list.add(t)
或者
linked.addLastast();
6.4线程池的好处
  1. 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务。
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的客观理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程大约需要1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
6.5线程池代码实现
public class RunnableImpl implements Runnable {
    //2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
    }
}
/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    线程池是jdk1.5之后提供的
    java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
    Excutors类中的静态方法:
        static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程
        参数:
            int nThreads:创建线程池中包含的线程数量
        返回值:
            ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接受,(面向接口编程)
        java.util.concurrent.ExecutorService 接口,线程池接口
            用来从线程池中获取线程,调用start方法,执行线程任务
                submit(Runnable task)提交一个Runnable任务用于执行
            关闭/销毁线程池的方法:
                void shutdown()
        线程池的使用步骤:
            1.使用线程的工厂类Executors里面提供的静态方法,newFixedThreadPool(int nThreads)生产一个指定线程数量的线程池
            2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
            3.调用ExcecutorService中的方法shutdown销毁线程池(不建议执行)

 */
public class Demo01ThreadPool {
    public static void main(String[] args) {
        //1.使用线程的工厂类Executors里面提供的静态方法,newFixedThreadPool(int nThreads)生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExcecutorService中的方法shutdown销毁线程池(不建议执行)
        //线程池会一直开启,使用完了线程会自动把线程归还给线程池
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程
        new Thread(new RunnableImpl()).start();
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值