Java进阶第五天

Java进阶 专栏收录该内容
11 篇文章 0 订阅

1、异常

1.1 异常类和处理方式
(1)异常是一个类,发生异常时会创建一个异常对象并抛出。
(2)异常不是语法错误,语法错误编译不通过,不会产生字节码文件,不会运行
   Throwable父类
   Error子类 不能处理,只能尽量避免
   Exception子类  编译期(写代码时)异常
           RuntimeException 运行期异常
(3)两种处理方式:1、抛出异常,给虚拟机处理,虚拟机会中断程序,将异常打印出来
          2、try-catch 处理之后抛出异常但后面的代码可以正常执行
public class Demo01Exception {
  public static void main(String[] args) {
    //runException();//ArrayIndexOutOfBoundsException 运行时抛出数组越界异常
    error();//OutOfMemoryError 内存溢出错误,创建的数组太大,超出了给JVM分配的内存
  }
  //编译期异常
  //抛出异常
  public static void cancel1() throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
    sdf.parse(“1990-09-08”);
  }
  //try-catch
  public static void cancel2() {
    SimpleDateFormat sdf= new SimpleDateFormat(“yyyy-MM-dd”);
    try {
      sdf.parse(“1880-07-31”);
    } catch (ParseException e) {
      e.printStackTrace();
    }
  }
  //运行期异常,可以用try-catch
  public static void runException() {
    int[] arr = {1, 2, 3};
    try{
      System.out.println(arr[3]);
    } catch (Exception e) {
      //处理异常
      System.out.println(e);//java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    }
  }
  //错误,必须修改代码
  public static void error() {
    int[] arr = new int[1024 * 1024 * 1024];
  }
}
1.2 异常的产生过程解析
异常的产生过程:以数组越界举例
  访问数组的3索引,而数组没有3索引,JVM(Java虚拟机)会检测出程序会出现异常
  JVM会做两件事:
(1)根据异常产生的原因创建一个异常对象,该对象包括异常产生的内容、原因、位置
new ArrayIndexOutOfBoundsException(“3”)
(2)get方法中没有处理异常的代码try-catch,JVM就会把异常对象抛出给方法的调用者,也就是main方法,由它处理异常。
  main方法接收了异常对象,但main方法也没有处理异常的代码,继续将异常抛出给main方法的调用者JVM,
  JVM接收了这个异常对象,又做了两件事:
  (1)把异常对象(内容、原因、位置)以红色字体打印在控制台
        原因                        内容
Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at demo01Exception.DemoExceptionExpression.get(DemoExceptionExpression.java:21)//位置
at demo01Exception.DemoExceptionExpression.main(DemoExceptionExpression.java:17)//位置
  (2)终止当前在执行的Java程序->中断处理
public class Demo02ExceptionExpression {
  public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    int e = get(arr, 3);
    System.out.println(e);
  }
  public static int get(int[] arr, int index) {
    return arr[index];
  }
}
1.3 throw关键字
使用throw关键字在指定的方法这抛出指定的异常
  throw new ***Exception(“异常产生的原因”)
注意:
  (1)throw必须写在方法内部
  (2)throw后面new的必须是Exception对象或Exception的子类对象
  (3)throw抛出异常,我们就必须处理这个异常
如果是运行期异常RuntimeException 可以不处理,默认交给JVM处理(打印异常,中断程序)
如果是编译期异常,必须处理,要么抛出,要么try-catch
public class Demo03throw {
  public static void main(String[] args) {
    int[] arr = {1, 2, 3};
    get(arr, 3);
  }
  public static int get(int[] arr, int index) {
    //对方法传递的参数进行合法性校验,如果不合法,就必须使用抛出异常的方式,告知方法的调用者参数不合法
    if (arr == null) {
      throw new NullPointerException(“传递的数组的值是空”);
      //NullPointerException是运行期异常,可以不处理,默认交给JVM处理
    }
    if (index < 0 || index > arr.length - 1) {
      throw new ArrayIndexOutOfBoundsException(“传递的索引越界”);
      //ArrayIndexOutOfBoundsException是运行期异常,可以不处理,默认交给JVM处理
    }
    return arr[index];
  }
}
1.4 Objects非空判断
public class Demo04ObjectsExcepton {
  public static void main(String[] args) {
    method(null);
  }
  public static void method(Object obj) {
    //使用Objects类里的静态方法nonNull对传递的对象进行是否为空判断
    // Objects.requireNonNull(obj);
// Exception in thread “main” java.lang.NullPointerException
// at java.base/java.util.Objects.requireNonNull(Objects.java:221)
// at demo01Exception.Demo04ObjectsExcepton.method(Demo04ObjectsExcepton.java:11)
// at demo01Exception.Demo04ObjectsExcepton.main(Demo04ObjectsExcepton.java:7)
    Objects.requireNonNull(obj, “传递的对象为空”);
// Exception in thread “main” java.lang.NullPointerException: 传递的对象为空
//  at java.base/java.util.Objects.requireNonNull(Objects.java:246)
//  at demo01Exception.Demo04ObjectsExcepton.method(Demo04ObjectsExcepton.java:16)
//  at demo01Exception.Demo04ObjectsExcepton.main(Demo04ObjectsExcepton.java:7)
}
}
1.5 throws关键字(异常处理的第一种方法)
throws关键字 处理异常的第一种方式,交给别人处理
  当方法内部抛出异常对象时,就必须处理这个异常对象
  可以用throws关键字声明抛出异常,抛出给方法调用者,最终抛出给JVM进行打印异常和中断处理
  在方法声明时使用
  修饰符 返回值类型 方法名(参数列表) throws AAAException, BBBException{
    throw new AAAException(“原因”);
    throw new BBBException(“原因”);
  }
注意:
   (1)throws关键字必须写在方法声明处
   (2)throws关键字后的异常必须是Exception的对象或其子类对象
   (3)方法内部抛出多个异常,throws后也必须声明多个异常,若有继承关系,声明父类即可
   (4)调用声明异常的方法,就必须处理这个异常,要么继续使用throws声明抛出,要么try-catch
public class Demo05throws {
  public static void main(String[] args) throws IOException {
  //method(“C:\a.txt”);
  method(“C:\b.txt”);
  // Exception in thread “main” java.lang.Exception: 文件找不到
  //  at demo01Exception.Demo05throws.method(Demo05throws.java:24)
  //  at demo01Exception.Demo05throws.main(Demo05throws.java:19)
  }
  //对传递的文件路径进行合法性判断,如果不是C:\a.txt,就抛出文件找不到异常
  public static void method(String path) throws IOException {
    if (!path.equals(“C:\a.txt”)) {
      throw new FileNotFoundException(“文件找不到”);
      //FileNotFoundException是编译异常,必须处理
    }
    if (!path.endsWith(".txt")) {
      throw new IOException(“文件后缀名不对”);
      //IOException是编译异常,必须处理,IOException是FileNotFoundException的父类,只要声明IOException
    }
    System.out.println(“读取文件”);
  }
}
1.6 try-catch(异常处理的第二种方法)
public class Demo06trycatch {
  public static void main(String[] args) {
    try {
      method(“C:\a.tx”);
    } catch (IOException e) {
      System.out.println(e);//java.io.IOException: 文件后缀名错误
    }
    System.out.println(“后续代码”);//后续代码 处理异常后,try-catch后面代码可以继续执行,但方法中的产生异常的代码之后的代码无法执行
  }
  public static void method(String path) throws IOException {
    if (!path.endsWith(".txt")) {
      throw new IOException(“文件后缀名错误”);
    }
  }
}
1.7 Throwable类中3个处理异常的方法
  String getMessage();//返回Throwable的简短描述
  String toString();//返回Throwable的详细消息字符串
  void printStackTrace();//JVM打印异常对象,默认调用此方法,打印的信息最全面
public class Demo07Throwable {
  public static void main(String[] args) {
    try {
      method(“C:\a.tx”);
    } catch (IOException e) {
    //getMessage方法
    System.out.println(e.getMessage());//文件后缀名不对
    //toString方法
    System.out.println(e);//java.io.IOException: 文件后缀名不对
    //printStackTrace方法
    e.printStackTrace();
    //java.io.IOException: 文件后缀名不对
    //  at demo01Exception.Demo07Throwable.method(Demo07Throwable.java:26)
    //  at demo01Exception.Demo07Throwable.main(Demo07Throwable.java:14) 红色字体
    }
  }
  public static void method(String path) throws IOException {
    if(!path.endsWith(".txt")) {
      throw new IOException(“文件后缀名不对”);
    }
  }
}
1.8 finally代码块
finally必须与try-catch一起使用
一般用于资源释放
public class Demo08finally {
  public static void main(String[] args) {
    try {
      //可能会出现异常的代码
      method(".tx");
    } catch (IOException e) {
      //处理异常的代码
      e.printStackTrace();
    } finally {
      //无论是否出现异常都会执行的代码
      System.out.println(“释放资源”);
    }
  }
  public static void method(String path) throws IOException {
    if (!path.endsWith(".txt")) {
      throw new IOException(“文件后缀名不对”);
    }
  }
}
1.9 异常注意事项
多个异常怎么处理:
  多个异常多次捕获,多次处理
  多个异常一次捕获,多次处理
    如果要处理的异常有继承关系,子类的catch代码要写在父类上面,因为产生的父类异常和子类异常都可以赋值给父类异常对象,会导致子类异常对象没有被使用
  多个异常一次捕获,一次处理
public class Demo09Notice {
  public static void main(String[] args) {
    //多个异常多次捕获,多次处理
    //第一个异常,数组索引越界异常
    // try {
    //  int[] arr = {1, 2, 3};
    //  System.out.println(arr[3]);
    // } catch (ArrayIndexOutOfBoundsException e) {
    //  System.out.println(e);
    //java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    // }
    //第二个异常,空指针异常
    // try {
    //    Objects.requireNonNull(null, “传递的对象为空”);
    // } catch (ArrayIndexOutOfBoundsException e) {
    //    System.out.println(e);//java.lang.NullPointerException: 传递的对象为空
    // }
    //多个异常一次捕获,多次处理
    try {
    //  int[] arr = {1, 2, 3};
    //  System.out.println(arr[3]);
      Objects.requireNonNull(null, “传递的对象为空”);
    } catch (ArrayIndexOutOfBoundsException e) {
      System.out.println(e);
      //java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    } catch (NullPointerException e) {
      System.out.println(e);//java.lang.NullPointerException: 传递的对象为空
    }

//多个异常一次捕获,一次处理
    try {
      int[] arr = {1, 2, 3};
      System.out.println(arr[3]);
      Objects.requireNonNull(null, “传递的对象为空”);
    } catch (Exception e) {
      System.out.println(e);
      //java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    }
  }
}
1.10 finally有return语句的情况
如果finally中有return语句,返回的就是finally中的结果
public class Demo10finallyreturn {
  public static void main(String[] args) {
    System.out.println(method());
  }
  public static int method() {
    int a = 10;
    try {
      return a;
    } catch (Exception e) {
      System.out.println(e);
    } finally {
      a = 100;
      return a;
    }
  }
}
1.11 子父类异常
(1)如果父类方法抛出多个异常,子类重写父类方法时,要么抛出相同异常,要么抛出这些异常的子类,要么不抛出异常
(2)父类方法没有抛出异常,子类重写该方法也不能抛出异常,此时若子类产生该异常只能处理,不能抛出
也就是,父类和子类的异常处理方式一样
父类
public class Fu {
  public void method1() throws NullPointerException, ClassCastException {}
  public void method2() throws IndexOutOfBoundsException {}
  public void method3() throws IndexOutOfBoundsException {}
  public void method4() {}
  public void method5() {}
}
子类
class Zi extends Fu {
  //子类重写父类方法时,抛出相同异常
  public void method1() throws NullPointerException, ClassCastException {}
  //子类重写父类方法时,抛出这些异常的子类
  public void method2() throws ArrayIndexOutOfBoundsException {}
  //子类重写父类方法时,不抛出异常
  public void method3() {}
  //父类方法没有抛出异常,子类重写该方法也不能抛出异常
  public void method4() {}
  //若子类产生该异常只能处理,不能抛出,否则父类方法也会同时抛出异常
  public void method5() {
    try {
      throw new Exception(“产生异常”);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
1.12 自定义异常类
格式:
  public class XXXException extends Exception/RuntimeException {
    定义空参构造
    定义带异常信息的构造
  }
自定义的异常类
public class RegisterException extends Exception {
//定义无参构造
public RegisterException() {
super();
}
//定义带异常信息的构造
public RegisterException(String s) {
super(s);
}
}
测试类
public class Demo12Customize {
  static String[] usernames = {“张三”, “李四”, “王五”};
  //抛出异常
  /*
  public static void main(String[] args) throws RegisterException {
    System.out.print(“请输入用户名:”);
    Scanner sc = new Scanner(System.in);
    String input = sc.next();
    register(input);
  }
  public static void register(String username) throws RegisterException {
    for (String s : usernames) {
      if (s.equals(username)) {
        throw new RegisterException(“该用户名已被注册”);
      }
    }
    System.out.println(“注册成功”);
  }
  */
 
  //try-catch
  public static void main(String[] args){
    System.out.print(“请输入用户名:”);
    Scanner sc = new Scanner(System.in);
    String input = sc.next();
    register(input);
  }
  public static void register(String username) {
    for (String s : usernames) {
      if (s.equals(username)) {
        try {
          throw new RegisterException(“该用户名已被注册”);
        } catch (RegisterException e) {
          e.printStackTrace();
          return;//结束方法
        }
      }
    }
  System.out.println(“注册成功”);
  }
}

2、多线程

2.1 相关概念
cpu:中央处理器 对数据进行计算,指挥软件和硬件干活
   分类:AMD
      Intel
并发:一个cpu在一段时间内交替执行多个任务
并行:多个cpu同时执行多个任务
硬盘:永久存储 ROM
内存:临时存储 RAM 所有的应用程序要执行都必须进入内存
进程:进入内存的应用程序 结束程序就会将进程从内存中清除
线程:进程中的一个执行单元,负责程序的执行
   点击程序的某个功能,程序就会开辟一条到cpu的执行路径,该路径就叫线程
单核心单线程 1个线程在多个任务之间高速切换
4核心8线程 8个线程在多个任务之间高速切换
线程调度: 分时调度 线程轮流使用cpu,时间相同
     抢占式调度 优先级高的线程使用cpu的可能性高,时间长
主线程:执行main方法的线程
单线程程序:Java程序中只有一个线程 执行从main方法开始,从上到下依次执行
JVM执行main方法,main方法进入栈内存,JVM找操作系统开辟一条从main方法通向cpu的执行路径,cpu就可以通过这条路径执行main方法,该路径就是主线程
单线程程序出现异常后无法继续执行
2.2 主线程
(1)主线程:执行main方法的线程
单线程程序:Java程序中只有一个线程 执行从main方法开始,从上到下依次执行
(2)JVM执行main方法,main方法进入栈内存,JVM找操作系统开辟一条从main方法通向cpu的执行路径,cpu就可以通过这条路径执行main方法,该路径就是主线程
(3)单线程程序出现异常后无法继续执行
Person类
public class Person {
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() {
    for (int i = 0; i < 20; i++) {
        System.out.println(name + i);
    }
}

}
测试类
public class Demo02Mainthread {
  public static void main(String[] args) {
    //没有异常时是依次执行两个run 出现异常后只会执行异常代码之前的run
    Person p1 = new Person(“张三”);
    p1.run();
    System.out.println(0/0);
    Person p2 = new Person(“李四”);
    p2.run();
  }
}
2.3 创建线程的第一种方法 继承Thread类,重写run方法
Java支持多线程程序 属于抢占式调度
创建新执行线程有两种方法:
第一种:
  继承Thread类,重写run方法,创建对象,调用start方法(开启新线程,间接调用run方法)
  结果是两个线程,当前的main线程和创建的新线程,并发执行
  多次启动一个线程是非法的,尤其是线程结束执行后不能再次启动
  内存解释:
    (1)首先是main方法进入栈空间 ,压栈执行。
    (2)其次创建新线程,new MyThread()会在堆内存中开辟一块空间,并将地址值赋值给m
    (3)然后是m.start(),会开辟一块新的栈空间,不在原来的栈空间中,start方法调用的run方法会在新的栈空间中执行。
  每创建一个新线程并start,就会新开辟一个栈空间(不在原来的栈空间中)。不同的线程在不同的栈空间,互不影响。
  于是cpu就可以随机选择执行哪个栈空间中的方法
    (如果这里是直接m.run,那么依然是单线程程序,run方法会进入原来的栈内存压栈执行)
MyThread类 继承Thread类
public class MyThread extends Thread {
  //重写run方法,设置线程任务
  @Override
  public void run() {
    for (int i = 0; i < 20; i++) {
      System.out.println(“run” + i);
    }
  }
}
测试类
public class Demo03CreateThread {
  //JVM执行main方法,找OS开辟一条从main方法通向cpu的路径,cpu就可以通过这条路径执行main方法,这条路径就是主线程
  public static void main(String[] args) {
    //创建了新的执行线程,又会开辟一条到cpu的路径,这条路径是用来执行run方法的,方式是通过start方法调用run方法
    MyThread m = new MyThread();
    m.start();
    for (int i = 0; i < 20; i++) {
      System.out.println(“main” + i);
    /*
      第一次执行  第二次执行
       main0    main0
       run0     main1
       main1    main2
       run1     main3
       main2    main4
       run2     main5
       main3    main6
       run3     main7
       main4    main8
       run4     main9
       main5    main10
       run5    main11
       main6   main12
       run6    main13
    每次执行的结果不一样
    对于cpu而言,有2条路径,cpu随机选择执行哪个方法,我们控制不了cpu,所以就有了随机打印的结果
    对于两个线程,一个主线程,一个新线程,抢夺cpu的执行权(执行时间)
    */
    }
  }
}
2.4 获取线程名字
获取线程的名字的两种方法
(1)使用Thread类中的方法getName
(2)使用Thread类中的方法currentThread,获取当前正在执行的线程,然后再getName
设置线程名称的两种方法
(1)直接使用Thread类中的setNmae方法
(2)添加一个带名字参数的构造方法,调用父类的带参构造方法,将名字传递给父类,让父类给子类起名字
SecondThread类
public class SecondThread extends Thread {
  @Override
  public void run() {
    //第一种方法:使用Thread类中的方法getName
    //String name = getName();
    //System.out.println(name);
    Thread t = Thread.currentThread();
    String name = t.getName();
    System.out.println(name);
  }
}
ThirdThread类
public class ThirdThread extends Thread {
  ThirdThread() {
    super();
  }
  ThirdThread(String name) {
    super(name);
  }
 
  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName());
  }
}
测试类
public class Demo04ThreadName {
  public static void main(String[] args) {
    //获取线程名字
    //1、使用Thread类中的方法getName,这里在类中使用该方法
    //SecondThread st1 = new SecondThread();
    //st1.start();//Thread-0
    //new SecondThread().start();//Thread-1
 
    //2、使用Thread类中的方法currentThread,获取当前正在执行的线程,然后再getName
    SecondThread st2 = new SecondThread();
    st2.start();//Thread-0
    new SecondThread().start();//Thread-1
 
    //获取主线程的名字,不能之间getName,因为本类没有继承Thread,不能直接用它的成员方法
    System.out.println(Thread.currentThread().getName());//main

//设置线程名字
    ThirdThread tt = new ThirdThread();
    tt.setName(“张三”);
    tt.start();//张三
    new ThirdThread(“李四”).start();//李四
  }
}
2.5 暂停线程的sleep方法
让线程暂停指定的毫秒数再执行
public class Demo05ThreadSleep {
  public static void main(String[] args) {
    //一秒输出一个数字
    for (int i = 1; i <= 20; i++) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(i);
    }
  }
}
2.6 创建线程的第二种方法 实现Runnable接口
创建新执行线程有两种方法:
第二种:
  实现Runnable接口,重写run方法,创建实现类对象,创建Thread对象在其构造方法中传递实现类对象,调用start方法
该方法优点:
(1)避免了单继承的局限性,一个类实现了Runnable接口,还可以继承其它类或实现其它接口,而继承了Thread类就不可以再继承其它类了。
(2)增强程序的扩展性,降低程序的耦合性(解耦)
   将设置线程任务和开启新线程进行了分离。线程任务是写在实现类的run方法里的,传递不同的实现类对象,可以执行不同的任务。创建Thread对象,调用start方法,用来开启新线程
  (而第一种方法是将设置线程任务放在Thread子类里,要执行不同任务必须修改子类的内容)
Runnable的实现类RunnableImpl
public class RunnableImpl implements Runnable {
  @Override
  public void run() {
    for (int i = 0; i < 20; i++) {
      System.out.println(Thread.currentThread().getName() + i);
    }
  }
}
测试类
public class Demo06CreateThreadRunnable {
  public static void main(String[] args) {
    RunnableImpl run = new RunnableImpl();
    Thread t = new Thread(run);
    t.start();
    for (int i = 0; i < 20; i++) {
      System.out.println(Thread.currentThread().getName() + i);
    }
  }
}
2.7 匿名内部类
1、匿名内部类:简化代码
2、将子类继承父类,重写父类方法,创建子类对象合为一步
  将实现类实现接口,重写接口方法,创建实现类对象合为一步
3、最终的产物是子类对象或实现类对象
4、格式:
  new 父类/接口() {
    重写父类/接口方法
  };
public class Demo07AnonymousThread {
  public static void main(String[] args) {
    //继承Thread类创建线程
    new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 10; i++) {
          System.out.println(Thread.currentThread().getName() + i);
        }
      }
   }.start();
    //实现Runnable接口创建线程
    new Thread(new Runnable() {
      @Override
   public void run() {
        for (int i = 0; i < 10; i++) {
          System.out.println(“helloworld”);
        }
      }
    }).start();
  }
}

3、线程安全

(1)单线程程序不会有安全问题
(2)多线程程序不访问共享数据也不会有安全问题
(3)多线程访问共享数据会产生线程安全问题
下面是卖票案例,模拟三个线程共同卖同100张票,多线程访问了共享数据会出现线程安全问题
3.1 线程安全问题演示
本节使用创建线程的第二种方法来创建线程,即实现Runnable接口
Runnable的实现类RunnableImpl
public class RunnableImpl implements Runnable {
  int ticket = 100;
  @Override
  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 Demo01ThreadSecurity {
  public static void main(String[] args) {
    RunnableImpl run = new RunnableImpl();
    Thread t1 = new Thread(run);
    Thread t2 = new Thread(run);
    Thread t3 = new Thread(run);
    t1.start();
    t2.start();
    t3.start();
    //三个线程同时卖100张票,会产生安全问题,如同时在卖同一张票,卖不存在的票(0,-1),等等
  }
}
3.2 解决线程安全问题的第一种方法:使用同步代码块来避免线程安全问题
(1)synchronized(锁对象) {
  可能会发生线程安全问题的代码,也就是访问了共享数据的代码
}
(2)锁对象可以是任意对象,但要保证多个线程使用的锁对象是同一个,作用:把同步代码块锁住,只让一个线程在同步代码块中执行
(3)同步技术的原理:
  使用了一个锁对象,其叫同步锁/对象锁,对象监视器
  3个线程一起抢夺cpu的执行权,谁抢到谁执行run方法
  t1抢到了,执行run方法,遇到synchronized代码块,就检查代码块中是否存在锁对象,存在就获取锁对象,进入代码块执行
  t2抢到了,执行run方法,遇到synchronized代码块,就检查代码块中是否存在锁对象,不存在就会进入阻塞状态,一直等待t1归还锁对象,
  直到t1执行完代码块,归还锁对象给同步代码块,才能够获取锁对象,进入到同步代码块执行
  同步中的线程没有执行完不会归还锁对象,同步外的线程没有锁对象无法进入代码块执行,保证了只有一个线程在执行代码块
(4)问题:程序频繁地判断锁、获取锁、释放锁,效率会降低
Runnable的实现类RunnableImpl
public class RunnableImpl2 implements Runnable {
  int ticket = 100;
  Object obj = new Object();
  @Override
  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 Demo01ThreadSecurity2 {
  public static void main(String[] args) {
    RunnableImpl2 run = new RunnableImpl2();
  Thread t1 = new Thread(run);
  Thread t2 = new Thread(run);
  Thread t3 = new Thread(run);
  t1.start();
  t2.start();
   t3.start();
  //三个线程同时卖100张票,会产生安全问题,如同时在卖同一张票,卖不存在的票(0,-1),等等
  }
}
3.3 解决线程安全问题的第二种方法:定义同步方法
定义同步方法,将可能产生线程安全问题的代码放入方法,在方法上添加一个修饰符synchronized
  修饰符 synchronized 返回值类型 方法名(参数列表) {
    可能产生线程安全问题的代码
  }
使用该方法默认的锁对象就是该实现类的对象,也就是this
Runnable的实现类RunnableImpl
public class RunnableImpl3 implements Runnable {
  int ticket = 100;
  static int ticket2 = 100;
  @Override
  public void run() {
    while (true) {
      saleTicket();
    }
  }
  public synchronized void saleTicket() {
    if (ticket > 0) {
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    System.out.println(Thread.currentThread().getName() + “正在卖第” + ticket + “张票”);
    ticket–;
    }
  }
  //静态同步方法
  //其默认的锁对象不能是该实现类的对象this,因为静态方法优先于创建对象,在创建对象之前就已经进入到内存中
  //静态方法的默认锁对象是该实现类的class属性–>class文件对象(反射的知识),
   也就是RunnableImpl3.class
  public static synchronized void saleTicket2() {
    if (ticket2 > 0) {
      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + “正在卖第” + ticket2 + “张票”);
      ticket2–;
    }
  }
}
测试类
public class Demo01ThreadSecurity3 {
  public static void main(String[] args) {
    RunnableImpl3 run = new RunnableImpl3();
    Thread t1 = new Thread(run);
    Thread t2 = new Thread(run);
    Thread t3 = new Thread(run);
    t1.start();
    t2.start();
    t3.start();
    //三个线程同时卖100张票,会产生安全问题,如同时在卖同一张票,卖不存在的票(0,-1),等等
  }
}
3.4 解决线程安全问题的第三种方法:使用Lock锁
Lock接口常用方法:
  void lock();获取锁
  void unlock();释放锁
实现类ReentrantLock
使用步骤:
(1)在成员位置创建一个ReentrantLock类的对象
(2)在可能出现线程问题的代码前调用Lock接口的lock方法
(3)在可能出现线程问题的代码后调用Lock接口的unlock方法
Runnable的实现类RunnableImpl
public class RunnableImpl4 implements Runnable {
  int ticket = 100;
  //1、在成员位置创建一个ReentrantLock类的对象
  Lock l = new ReentrantLock();
  @Override
  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();
    }
 
    */
    //更好的写法 将释放锁写在finally里
    while (true) {
      l.lock();
      if (ticket > 0) {
        try {
          Thread.sleep(10);
          System.out.println(Thread.currentThread().getName() + “正在卖第” + ticket + “张票”);
          ticket–;
         } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          l.unlock();
        }
      }
    }
  }
}
测试类
public class Demo01ThreadSecurity4 {
  public static void main(String[] args) {
    RunnableImpl4 run = new RunnableImpl4();
    Thread t1 = new Thread(run);
    Thread t2 = new Thread(run);
    Thread t3 = new Thread(run);
    t1.start();
    t2.start();
    t3.start();
    //三个线程同时卖100张票,会产生安全问题,如同时在卖同一张票,卖不存在的票(0,-1),等等
  }
}

4、线程状态

4.1 概述
在Thread类中有一个内部类Thread.State描述了线程的各种状态
  NEW 新建状态 创建了新线程
  RUNNABLE 执行状态 抢夺到了cpu执行权就进入运行状态
  BLOCKED 阻塞状态 没抢夺到cpu执行权就进入阻塞状态 运行和阻塞状态可以相互切换
  TERMINATED 死亡状态 执行完run方法或调用stop方法就进入死亡状态
  TIMED_WAITING 休眠/睡眠状态 计时等待 调用了sleep(Thread类静态方法)方法或wait(Object类成员方法)方法,传递毫秒值参数,就进入休眠状态。睡醒之后查看cpu是否空闲,空闲进入运行状态,不空闲进入阻塞状态
  WAITING 无限(永久)等待状态 运行中的线程调用Object类的成员方法wait,不传递参数,就进入无限等待状态,要使用Object类的成员方法notify唤醒线程
  TIMED_WAITING和WAITING被称为冻结状态
4.2 等待唤醒案例
等待唤醒案例
  创建一个消费者线程,告知生产者包子的种类和数量,调用wait方法,放弃cpu的执行,进入无限等待状态
  创建一个生产者线程,花5s做包子,做好包子之后,调用notify方法,唤醒消费者
  注意:
  (1)生产者和消费者的代码必须包裹在同步代码块中,保证等待和唤醒只有一个在执行
  (2)锁对象必须唯一
  (3)只有锁对象才能调用wait和notify方法
public class Demo02WaitNotify {
  public static void main(String[] args) {
    //创建锁对象
    Object obj = new Object();
    //创建消费者线程
    //匿名对象创建
    new Thread() {
      @Override
       public void run() {
        //一直有人买包子
        while (true) {
          //告知生产者包子的种类和数量
          System.out.println(“3个肉包子”);
          synchronized (obj) {
            //这里只能使用try-catch,因为父类Thread的run方法没有抛出异常
            try {
              obj.wait();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            //唤醒之后执行的代码
            System.out.println(“开吃”);
          }
        }
      }
    }.start();
    //创建生产者线程
    new Thread() {
      @Override
      public void run() {
        //一直做包子
        while (true) {
          try {
            Thread.sleep(5000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          synchronized (obj) {
            System.out.println(“包子做好了”);
            obj.notify();
          }
        }
      }
    }.start();
    /*
      3个肉包子
      (5s)
      包子做好了
      开吃
      3个肉包子
      (5s)
      包子做好了
      开吃
      3个肉包子
      (5s)
      包子做好了
      开吃
      …
    */
  }
}
4.3 进入TIMED_WAITING状态的两种方法
(1)使用Thread类的静态方法sleep(long m),睡醒后会进入RUNNABLE/BLOCKED状态
(2)使用Object类的方法wait(long m),毫秒结束后还没被notify就会自动醒来,睡醒后会进入RUNNABLE/BLOCKED状态
public class Demo03WaitNotifyAll {
  public static void main(String[] args) {
    //创建锁对象
    Object obj = new Object();
    //创建消费者线程
    //匿名对象创建
    //  new Thread() {
    //    @Override
    //    public void run() {
    //      //一直有人买包子
    //      while (true) {
    //        //告知生产者包子的种类和数量
    //        System.out.println(“3个肉包子”);
    //        synchronized (obj) {
    //          //这里只能使用try-catch,因为父类Thread的run方法没有抛出异常
    //          try {
    //            obj.wait(5000);//传入毫秒值,时间到了没有人唤醒也会自动醒来
    //          } catch (InterruptedException e) {
    //            e.printStackTrace();
    //          }
    //          //唤醒之后执行的代码
    //          System.out.println(“开吃”);
    //        }
    //      }
    //    }
    //  }.start();

//notifyAll方法
    new Thread() {
      @Override
      public void run() {
        //一直有人买包子
        while (true) {
          //告知生产者包子的种类和数量
          System.out.println(“顾客1:3个肉包子”);
          synchronized (obj) {
            //这里只能使用try-catch,因为父类Thread的run方法没有抛出异常
            try {
              obj.wait();//传入毫秒值,时间到了没有人唤醒也会自动醒来
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            //唤醒之后执行的代码
            System.out.println(“顾客1开吃”);
          }
        }
      }
    }.start();
    new Thread() {
      @Override
      public void run() {
        //一直有人买包子
        while (true) {
          //告知生产者包子的种类和数量
          System.out.println(“顾客2:3个肉包子”);
          synchronized (obj) {
            //这里只能使用try-catch,因为父类Thread的run方法没有抛出异常
            try {
              obj.wait(5000);//传入毫秒值,时间到了没有人唤醒也会自动醒来
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            //唤醒之后执行的代码
            System.out.println(“顾客2开吃”);
          }
        }
      }
    }.start();
    //创建生产者线程
    new Thread() {
      @Override
      public void run() {
        //一直做包子
        while (true) {
          try {
            Thread.sleep(5000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          synchronized (obj) {
            System.out.println(“包子做好了”);
            obj.notify();//有多个等待线程,随机唤醒一个
            //obj.notifyAll();//唤醒当前对象锁上的所有线程
          }
        }
      }
    }.start();
  }
}
4.4 等待唤醒机制
(1)线程通信:多个线程在处理同一份资源,但线程任务不同,那么线程之间就存在线程通信问题
(2)为什么要处理线程通信?
多个线程并发执行时,在默认情况下cpu是随机切换线程的,当我们希望多个线程共同共同操作同一份资源并且有规律地执行时,就需要线程之间的通信。
(3)如何保证线程间通信有效利用资源?
多个线程处理同一份资源,并且任务不同时,需要线程通信来让多个线程共同操作同一份资源,也就是避免资源抢夺。
等待唤醒机制可以帮助我们使线程有效地利用资源。
(4)案例:买包子 有效地利用资源(生产一个包子,吃一个包子)
   通信:对包子的状态进行判断
      没有包子-消费者唤醒生产者-消费者等待-做包子-包子做好-修改包子状态为有
      有包子-生产者唤醒消费者-生产者等待-吃包子-吃完包子-修改包子状态为没有
4.5 包子铺案例
包子类
public class Baozi {
  private String outter;
  private String inner;
  private boolean state = false;
 
  public Baozi() {
  }
 
  public String getOutter() {
    return outter;
  }
 
  public void setOutter(String outter) {
    this.outter = outter;
  }
 
  public String getInner() {
    return inner;
  }
 
  public void setInner(String inner) {
    this.inner = inner;
  }
 
  public boolean isState() {
    return state;
  }
 
  public void setState(boolean state) {
    this.state = state;
  }
 
  public Baozi(String outter, String inner, boolean state) {
    this.outter = outter;
    this.inner = inner;
    this.state = state;
  }
}
包子铺线程
与顾客线程的关系->通信(互斥)
包子铺线程和顾客线程使用的锁对象必须唯一,可以使用包子对象作为锁对象
public class BaoziShop extends Thread {
  private Baozi bz;
 
  public BaoziShop(Baozi bz) {
    this.bz = bz;
  }
 
  //重写run方法,设置线程任务:做包子
  @Override
  public void run() {
    int count = 0;
    while (true) {
      synchronized (bz) {
        if(bz.isState()) {
          try {
            bz.wait();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        System.out.println(“开始做包子”);
        if(count % 2 == 0) {
          bz.setOutter(“薄皮”);
          bz.setInner(“猪肉”);
        } else {
          bz.setOutter(“蟹黄”);
          bz.setInner(“三鲜”);
        }
        count++;
        System.out.println(“正在做” + bz.getOutter() + bz.getInner() + “包子”);
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(“包子做好了”);
        bz.setState(true);
        bz.notify();
        System.out.println(bz.getOutter() + bz.getInner() + “包子做好了,请慢用”);
      }
    }
  }
}
顾客线程
public class Customer extends Thread {
  private Baozi bz;
 
  public Customer(Baozi bz) {
    this.bz = bz;
  }

@Override
  public void run() {
    while (true) {
      synchronized (bz) {
        if(!bz.isState()) {
          try {
            bz.wait();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        //唤醒之后执行的代码
        System.out.println(“开始吃” + bz.getOutter() + bz.getInner() + “包子”);
        try {
          Thread.sleep(3000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(bz.getOutter() + bz.getInner() + “包子吃好了”);
        bz.setState(false);
        bz.notify();
        System.out.println("==============================");
      }
    }
  }
}
测试类
public class Demo05BaoziTest {
  public static void main(String[] args) {
    Baozi bz = new Baozi();
    new BaoziShop(bz).start();
    new Customer(bz).start();
    /*
      开始做包子
      正在做薄皮猪肉包子
      包子做好了
      薄皮猪肉包子做好了,请慢用
      开始吃薄皮猪肉包子
      薄皮猪肉包子吃好了
      ==============================
      开始做包子
      正在做蟹黄三鲜包子
      包子做好了
      蟹黄三鲜包子做好了,请慢用
      开始吃蟹黄三鲜包子
      蟹黄三鲜包子吃好了
      ==============================
      开始做包子
      正在做薄皮猪肉包子
      包子做好了
      薄皮猪肉包子做好了,请慢用
      开始吃薄皮猪肉包子
      薄皮猪肉包子吃好了
      ==============================
      开始做包子
      正在做蟹黄三鲜包子
      包子做好了
      蟹黄三鲜包子做好了,请慢用
      开始吃蟹黄三鲜包子
      蟹黄三鲜包子吃好了
      ==============================
    */
  }
}

5、线程池

(1)使用一个线程就去创建一个线程,如果并发的线程很多且每个线程只执行很短的时间就结束了,这会浪费很多时间,使系统效率降低,因为创建和销毁线程需要一定的时间
(2)线程池就是一个容器,可以使用LinkedList,其有序,且增删块
(3)程序第一次启动时,创建一些线程存进集合,要用时取出来,用完归还。JDK1.5之后可以直接使用内置线程池,不用自己创建集合
   Executors类 生产线程池的工厂类
     静态方法static ExecutorService newFixedThreadPool(int nThreads)
   ExecutorService是一个接口,返回的是其实现类对象
     submit(Runnable task) 提交一个Runnable任务用于执行
     void shutdown() 关闭/销毁线程池
(4)使用步骤:
   使用Executors类的静态方法newFixedThreadPool(int nThreads)生成一个线程池
   创建一个类,实现Runnable接口,重写run方法,设置线程任务
   调用ExecutorService的方法submit(Runnable task),传递执行任务,也就是上面的实现类对象,开启线程,执行run方法
   调用ExecutorService的方法shutdown()销毁线程池(不建议)
Runnable 的实现类RunnableImpl
public class RunnableImpl implements Runnable {
  @Override
  public void run() {
    System.out.println(“创建了一个新线程:” + Thread.currentThread().getName());
  }
}
测试类
public class Demo01ThreadPool {
  public static void main(String[] args) {
    //线程池会一直开启,程序不会停止
    ExecutorService e = Executors.newFixedThreadPool(2);
    e.submit(new RunnableImpl());//创建了一个新线程:pool-1-thread-1
    e.submit(new RunnableImpl());//创建了一个新线程:pool-1-thread-2
    e.submit(new RunnableImpl());//创建了一个新线程:pool-1-thread-1
    e.shutdown();//销毁线程池
    e.submit(new RunnableImpl());//线程池销毁后再使用会抛出异常RejectedExecutionException
  }
}

  • 0
    点赞
  • 1
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

评论 1 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页

打赏作者

Prayer96

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值