目录
概念
一、并发:指两个或多个事件在同一时间段内发生 比如 在一个时间段中 前一半时间执行任务1 后一半时间执行任务2 两个任务交替执行
二、并行:指两个或多个事件在同一时刻发生(同时发生) 比如 在一个时间段中 同时执行任务1和任务2
三、内存:所有的应用程序都需要进入到内存中执行 临时存储RAM
四、硬盘:永久存储ROM
五、点击应用程序执行 硬盘中的程序就会进入内存 占用一些内存执行 进入到内存的程序叫进程,在任务管理器中点击结束进程 就把进程从内存中清除
六、线程调度分为分时调度和抢占式调度
- 分时调度:所有线程轮流使用CPU的使用权 平均分配每个线程占用CPU的时间
- 抢占式调度:优先让优先级高的线程使用CPU 如果线程的优先级相同 那么随机选择一个(这称为线程随机) java使用的就是抢占式调度
七、主线程:执行主(main)方法的线程
八、单线程程序:java程序中只有一个线程,执行从main方法开始 从上到下依次执行
public class Concept {
public static void main(String[] args) {
Person p1 = new Person("小强");
p1.run();
Person p2 = new Person("旺财");
p2.run();
}
}
程序从主方法开始 从上到下依次执行以上四行代码 是单线程程序,JVM执行main方法 main方法会进入到栈内存 JVM会找操作系统开辟一条main方法通向CPU的执行路径,CPU就可以通过这个路径来执行main方法 而这个路径有一个名字 叫main(主)线程
异常
一、异常:程序在执行过程中 出现的非正常的情况 最终会导致JVM的非正常停止
二、在java等面向对象的编程语言 异常本身是一个类 产生异常就是创建异常对象并抛出了一个异常对象 java处理异常的方式是中断处理
三、异常指的并不是语法错误 语法错了 编译不通过 不会产生字节码文件 根本不能运行
四、异常机制其实是帮助我们找到程序中的问题
五、异常的根类是java.lang.Throwable
,其下有两个子类:java.lang.Error
与java.lang.Exception
六、Eroor
错误 相当于程序得了一个无法治愈的毛病(非典 艾滋等) 必须修改源代码程序才能继续执行
七、Exception
编译期异常 进行编译(写代码)java程序出现的问题 是由于使用不当导致的
RuntimeException
:运行期异常 java程序运行过程中出现的问题
八、异常就相当于程序得了一个小毛病(感冒发烧) 把异常处理掉 程序可以继续执行(吃点药 打点点滴 继续革命工作)
import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.xml.crypto.Data;
public class ThrowableClass {
public static void main(String[] args){
//Exception编译期异常
SimpleDateFormat sdfDateFormat = new SimpleDateFormat("yyyy-MM-dd");
//如果不写try catch会报错 这就是编译异常
try {
Date date = (Date) sdfDateFormat.parse("1999-09-09");
} catch (ParseException e) {
e.printStackTrace();
}
//RuntimeException:运行期异常
int[] arr = {1,2,3};
//System.out.println(arr[3]);会报错 索引越界异常 这就是在程序运行期间出现异常
//解决此方法:
try {
//可能会出现异常的代码
System.out.println(arr[3]);
} catch (Exception e) {
//异常的处理逻辑
System.out.println(e);
}
/* Eroor错误
* OutOfMemoryError:java heap space
* 内存溢出的错误 创建的数组太大了 超出了给JVM分配的内存
*/
//int[] arr2 = new int[1024*1024*1024];报错 内存溢出的错误 创建的数组太大了 超出了给JVM分配的内存
//必须修改代码 创建的数组小一点 否则程序没法运行
int[] arr2 = new int[1024*1024];
System.out.println("后续代码");
}
}
throw关键字
一、作用:可以使用throw
关键字在指定的方法中抛出指定的异常
二、使用格式:throw new xxxException("异常产生的原因");
三、注意:
throw
关键字必须写在方法的内部throw
关键字后边new的对象必须是Exception
或Exception
的子类对象throw
关键字抛出指定的异常对象 我们就必须处理这个异常对象
(1)throw
关键字后边创建的是RuntimeException
或RuntimeException
的子类对象 我们可以不处理 默认交给JVM处理(打印异常对象)
(2)throw
关键字后边创建的是编译异常(写代码的时候报错) 我们就必须处理这个异常 要么throw
要么try...catch
public class Throw {
public static void main(String[] args) {
int[] arr = null;
int e = getElement(arr, 0);
System.out.println(e);
}
/*
* 定义一个方法 获取数组指定索引处的元素
* 参数:
* int[] arr
* int index
* 以后工作中 我们首先必须对方法传递过来的参数进行合法性校验
* 如果参数不合法 那么我们就必须使用抛出异常的方式 告诉方法的调用者 传递的参数有问题
* 注意:NullPointerException是一个运行期异常 我们不用处理 默认交给JVM处理
* ArrayIndexOutOfBoundsException是一个运行期异常 我们不用处理 默认交给JVM处理
*/
private static int getElement(int[] arr, int index) {
/*
* 我们可以对传递过来的参数数组进行合法性校验 如果数组arr的值是null
* 那么我们就抛出空指针异常 告知方法的调用者“传递的数组的值是null”
*/
if (arr == null) {
throw new NullPointerException("传递的数组的值是null");
}
/*
* 我们可以对传递过来的参数index进行合法性校验 如果index的范围不在数组的索引范围内
* 那么我们就抛出数组索引越界异常 告知方法的调用者“传递的索引超出了数组的使用范围”
*/
if (index < 0 || index > arr.length - 1) {
throw new ArrayIndexOutOfBoundsException("传递的索引超出了数组的使用范围");
}
int ele = arr[index];
return ele;
}
}
throws关键字
一、throws
关键字:异常处理的第一种方式 交给别人处理
二、作用:当方法内部抛出异常对象时 我们就必须处理这个异常对象
三、可以使用throws
关键字处理异常对象 会把异常对象声明抛出给方法的调用者处理(自己不处理 给别人处理) 最终交给JVM处理 JVM采用中断处理
四、使用格式:在方法声明时使用
修饰符 返回值类型 方法名(参数列表) throws AAAException,BBBException...{//抛出了什么异常就写什么异常 抛出了几个异常就写几个异常
throw new AAAException("产生原因");
throw new BBBException("产生原因");
...
}
【注】:
throws
关键字必须写在方法声明处throws
关键字后边声明的异常必须是Exception
或者是Exception
的子类- 方法内部如果抛出了多个异常对象 那么
throws
后边必须也声明多个异常 如果抛出的多个异常对象有子父类关系 那么直接声明父类异常即可- 调用了一个声明抛出异常的方法 我们就必须处理声明的异常:要么继续使用
throws
声明抛出 交给方法的调用者处理 最终交给JVM 要么try...catch
自己处理异常- 使用
throws
声明抛出的弊端:因为JVM采取中断处理 所以抛出异常后面的代码就不执行了 我们可以使用try...catch
来解决这个问题
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.imageio.IIOException;
public class Throws {
/*FileNotFoundException extends IOException extends Exception
* 所以可以省略FileNotFoundException异常对象不声明
* 由于所有异常都是Exception的子类 所以也可以直接声明Exception
*/
public static void main(String[] args) throws /*FileNotFoundException,*/IOException {
readFile("d:\\a.tx");
}
/*
* 定义一个方法 对传递的文件路径进行合法性判断
* 如果路径不是“c:\\a.txt” 那么我们就抛出文件找不到异常对象 告知方法的调用者
* 【注】:
* FileNotFoundException是编译异常 抛出了编译异常 就必须处理这个异常
* 可以使用throws继续声明抛出FileNotFoundException这个异常对象 让方法的调用者处理
*/
public static void readFile(String filename) throws /*FileNotFoundException,*/IOException{
if (!filename.equals("c:\\a.txt")) {
throw new FileNotFoundException("传递的文件路径不是c:\\a.txt");
}
//抛出了上面这个异常 下面的代码就不执行了 在主函数中写readFile("d:\\a.tx");
//也不会抛出"文件的后缀名不对!"异常
/*
* 如果传递的路径不是.txt结尾
* 那么我们就抛出IO异常对象 告知方法的调用者 文件的后缀名不对
*/
if (!filename.endsWith(".txt")) {
throw new IOException("文件的后缀名不对!");
}
System.out.println("路径没有问题 读取文件");
}
}
try…catch
一、try...catch
:异常处理的第二种方式 自己处理
二、格式:
try{
可能产生异常的代码
}catch(定义一个异常的变量 用来接收try中抛出的异常对象){
异常的处理逻辑 产生异常对象之后 怎么处理异常对象
一般在工作中 会把异常的信息记录到一个日志中
}
...
catch(异常类名 变量名){//可以有多个catch
异常的处理逻辑
}
【注】:
try
中可能会抛出多个异常对象 那么就可以使用多个catch
来处理这些异常对象- 如果
try
中产生了异常 那么就会执行catch
中的异常处理逻辑 执行完毕catch
中的异常处理逻辑 继续执行try...catch
之后的代码,如果try
中没有产生异常 那么就不会执行catch
中异常的处理逻辑 执行完try
中的代码 继续执行try...catch
之后的代码try...catch
的弊端:当程序运行到try
中的readFile("c:\\a.tx");
时发现出现了这个异常,就把这个异常抛给catch
处理 而try
中readFile("c:\\a.tx");
的后续代码System.out.println("资源释放!");
不会被执行,为了解决这个问题 我们可以使用finally
代码块
import java.io.IOException;
public class TryCatch {
public static void main(String[] args) {
try {
readFile("c:\\a.tx");
System.out.println("资源释放!");
} catch (Exception e) {//try中抛出什么异常对象(或者看方法中抛出的是什么异常) catch就定义什么异常变量 用来接收这个异常对象
//异常的处理逻辑 产生异常对象之后 怎么处理异常对象
//System.out.println("catch - 传递的文件后缀不是.txt");//为演示Throwable类中定义了3个异常处理的方法 先将这行代码注释
/*
* Throwable类中定义了3个异常处理的方法:
* String getMessage():返回此throwable的简短描述
* String toString():返回此throwable的详细信息字符串
* void printStackTrace():JVM打印异常对象 默认此方法打印的异常信息最全面
*/
//System.out.println(e.getMessage());//文件的后缀名不对!
//System.out.println(e.toString());//java.io.IOException: 文件的后缀名不对! 重写了Object类的toString
//System.out.println(e);//java.io.IOException: 文件的后缀名不对!
/*
* java.io.IOException: 文件的后缀名不对!
at TryCatch.readFile(TryCatch.java:62)
at TryCatch.main(TryCatch.java:27)
*/
e.printStackTrace();
}
System.out.println("后续代码");
}
/*
* 定义一个方法 对传递的文件路径进行合法性判断
* 如果路径不是“c:\\a.txt” 那么我们就抛出文件找不到异常对象 告知方法的调用者
* 【注】:
* FileNotFoundException是编译异常 抛出了编译异常 就必须处理这个异常
* 可以使用throws继续声明抛出FileNotFoundException这个异常对象 让方法的调用者处理
*/
public static void readFile(String filename) throws /*FileNotFoundException,*/Exception{
/*
* 如果传递的路径不是.txt结尾
* 那么我们就抛出IO异常对象 告知方法的调用者 文件的后缀名不对
*/
if (!filename.endsWith(".txt")) {
throw new IOException("文件的后缀名不对!");
}
System.out.println("路径没有问题 读取文件");
}
}
finally代码块
一、格式:
try{
可能产生异常的代码
}catch(定义一个异常的变量 用来接收try中抛出的异常对象){
异常的处理逻辑 产生异常对象之后 怎么处理异常对象
一般在工作中 会把异常的信息记录到一个日志中
}
...
catch(异常类名 变量名){//可以有多个catch
异常的处理逻辑
}finally{
无论是否出现异常都会执行
}
【注】:
- finally不能单独使用 必须和try一起使用
- finally一般用于资源释放(资源回收) 无论程序是否出现异常 最后都要资源释放(IO)
import java.io.FileNotFoundException;
import java.io.IOException;
public class CatchFinally {
public static void main(String[] args) {
try {
readFile("d:\\a.tx");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
System.out.println("资源释放!");
}
}
/*
* 定义一个方法 对传递的文件路径进行合法性判断
* 如果路径不是“c:\\a.txt” 那么我们就抛出文件找不到异常对象 告知方法的调用者
* 【注】:
* FileNotFoundException是编译异常 抛出了编译异常 就必须处理这个异常
* 可以使用throws继续声明抛出FileNotFoundException这个异常对象 让方法的调用者处理
*/
public static void readFile(String filename) throws /*FileNotFoundException,*/IOException{
/*
* 如果传递的路径不是.txt结尾
* 那么我们就抛出IO异常对象 告知方法的调用者 文件的后缀名不对
*/
if (!filename.endsWith(".txt")) {
throw new IOException("文件的后缀名不对!");
}
System.out.println("路径没有问题 读取文件");
}
}
Exception
异常的产生过程解析 分析异常是怎么产生的 如何处理异常
public class ExceptionClass {
public static void main(String[] args) {
int[] arr = {1,2,3};//创建int类型的数组 并赋值
int e = getElement(arr,3);//ArrayIndexOutOfBoundsException报错
System.out.println(e);
}
/*
* 定义一个方法 获取数组指定索引处的元素
* 参数:
* int[] arr
* int index
*/
private static int getElement(int[] arr, int index) {
int ele = arr[index];
return ele;
}
}
- 当程序执行到
int e = getElement(arr,3);
这条语句时 会调用getElement
方法 并将参数arr,3
传递给该方法 - 程序会访问数组3索引 而数组是没有3索引的 这时 JVM就会检测出程序会出现异常 此时 JVM会做两件事:
(1)JVM根据异常产生的原因创建一个异常对象 这个异常对象包含了异常产生的(内容 原因 位置):new ArrayIndexOutOfBoundsException("3");
(2)在getElement
方法中 没有异常的处理逻辑(try...catch
) 那么JVM就会把异常对象抛出给getElement
方法的,调用者main来处理这个异常 - main方法接收到了这个异常对象
new ArrayIndexOutOfBoundsException("3");
,main
方法也没有异常的处理逻辑,继续把异常对象new ArrayIndexOutOfBoundsException("3");
,抛给main
方法的调用者JVM处理 - JVM接收到异常对象做了两件事:
(1)把异常对象(内容 原因 位置)以红色的字体打印在控制台
(2)JVM会终止当前正在执行的java程序 即中断处理
异常的注意事项
多个异常使用捕获又该如何处理:
- 多个异常分别处理
- 多个异常一次捕获 多次处理
- 多个异常一次捕获 一次处理
import java.util.List;
public class Exception2 {
public static void main(String[] args) {
//多个异常分别处理
// try {
// int[] arr = {1,2,3};
// System.out.println(arr[3]);
// } catch (ArrayIndexOutOfBoundsException e) {
// System.out.println(e);
// }
// try {
// List<Integer> list = List.of(1,2,3);
// System.out.println(list.get(3));
// } catch (IndexOutOfBoundsException e) {
// System.out.println(e);
// }
//多个异常一次捕获 多次处理
// try {
// int[] arr = {1,2,3};
// System.out.println(arr[3]);
// List<Integer> list = List.of(1,2,3);
// System.out.println(list.get(3));
// } catch (ArrayIndexOutOfBoundsException e) {
// System.out.println(e);
// }catch (IndexOutOfBoundsException e) {
// System.out.println(e);
// }
/*
* 一个try多个catch注意事项:
* catch里边定义定义的异常变量 如果有子父类关系 那么子类的异常变量必须写在上边 否则就会报错
* ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException
* 如果将上述代码改为以下代码 会报错:
* try {
int[] arr = {1,2,3};
System.out.println(arr[3]);
List<Integer> list = List.of(1,2,3);
System.out.println(list.get(3));
} catch (IndexOutOfBoundsException e) {
System.out.println(e);
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e);
}
* 报错原因:
* try中可能会产生的异常对象:
* new ArrayIndexOutOfBoundsException("3");
* new IndexOutOfBoundsException("3");
* try中如果出现了异常对象 会把异常对象抛出给catch处理 抛出的异常对象会从上到下依次赋值给catch中定义的异常变量
* 如果按照上面的代码来执行的话 执行过程如下:
* ArrayIndexOutOfBoundsException e = new ArrayIndexOutOfBoundsException("3");
* IndexOutOfBoundsException e = new IndexOutOfBoundsException("3");
* 如果按照下面的代码来执行的话 执行过程如下:
* IndexOutOfBoundsException e = new ArrayIndexOutOfBoundsException("3");//多态
* IndexOutOfBoundsException e = new IndexOutOfBoundsException("3");
* 因为ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException
* 所以IndexOutOfBoundsException可以接收IndexOutOfBoundsException("3");
* 也可以接收ArrayIndexOutOfBoundsException("3");
*/
//多个异常一次捕获 一次处理
try {
int[] arr = {1,2,3};
System.out.println(arr[3]);
List<Integer> list = List.of(1,2,3);
System.out.println(list.get(3));
} catch (IndexOutOfBoundsException e) {//catch定义的变量类型可以接收所有的异常
System.out.println(e);
}
//运行时异常被抛出可以不处理 即不捕获也不声明抛出
//默认给虚拟机处理 终止程序 什么时候不抛出运行时异常了 再继续执行程序
}
}
如果finally
中有return
语句 永远返回finally
中的结果 避免出现这种情况 即避免在finally
中写return
public class Exception3 {
public static void main(String[] args) {
int a = getA();
System.out.println(a);
}
public static int getA() {
int a = 10;
try {
return a;
} catch (Exception e) {
System.out.println(e);
}finally {
//一定会执行的代码
a = 100;
return a;
}
}
}
自定义异常类
一、自定义异常类:java提供的异常类不够我们使用 需要自己定义一些异常类
二、格式:
public class XXXException extends Exception(或RuntimeException){
添加一个空参数的构造方法
添加一个带异常信息的构造方法
}
【注】:
- 自定义异常类一般都是以
Exception
结尾 说明该类是一个异常类- 自定义异常类 必须继承
Exception
或者RuntimeException
(1)继承Exception
:那么自定义的异常类就是一个编译器异常 如果方法内部抛出了编译器异常 就必须处理这个异常 要么try...catch
要么throws
(2)继承RuntimeException
:那么自定义的异常类就是一个运行期异常 无需处理 交给虚拟机处理 虚拟机中断处理
public class RegisterException extends Exception{
//添加一个空参数的构造方法
public RegisterException() {
super();
}
/*
* 添加一个带异常信息的构造方法
* 查看源码发现 所有的异常类都会有一个带异常信息的构造方法 方法内部会调用父类带异常信息的构造方法 让父类来处理这个异常信息
*/
public RegisterException(String message) {
super(message);
}
}
子父类的异常
一、如果父类抛出了多个异常 子类重写父类方法时 抛出和父类相同的异常 或者是父类异常的子类或者不抛出异常
二、父类方法没有抛出异常 子类重写父类该方法时也不可以抛出异常 此时子类产生该异常 只能捕获处理 不能声明抛出
【注】:父类异常是什么样 子类异常就是什么样
public class Fu {
public void show01() throws NullPointerException,ClassCastException{}
public void show02() throws IndexOutOfBoundsException{}
public void show03() throws IndexOutOfBoundsException{}
public void show04() {}
}
class Zi extends Fu{
//如果父类抛出了多个异常 子类重写父类方法时 抛出和父类相同的异常
public void show01() throws NullPointerException,ClassCastException{}
//子类重写父类方法时 抛出父类异常的子类
//ArrayIndexOutOfBoundsException是IndexOutOfBoundsException的子类
public void show02() throws ArrayIndexOutOfBoundsException{}
//子类重写父类方法时 不抛出异常
//不管父类抛出什么异常 子类不抛出任何异常
public void show03() {}
//父类方法没有抛出异常 子类重写父类该方法时也不可以抛出异常
//public void show04() {}//为了演示后续代码而将其注释掉
//父类方法没有抛出异常 如果子类产生该异常 只能try...catch捕获处理 不能throws声明抛出
public void show04() {
try {
throw new Exception("编译器异常");
} catch (Exception e) {
e.printStackTrace();
}
}
}
Objects类中的静态方法
一、Objects类中的静态方法:public static <T> T requireNonNull(T obj)
:查看指定引用对象不是null
二、源码:
public static <T> T requireNonNull(T obj){
if(obj == null)//如果这个对象是null
throw new NullPointerException();//抛出异常
return obj;//如果这个对象不是null
}
import java.lang.reflect.Method;
public class Objects {
public static void main(String[] args) {
method(null);
}
public static void method(Object object) {
//对传递过来的参数进行合法性判断 判断是否为null
// if (object == null) {
// throw new NullPointerException("传递的对象的值是null");
// }
//对上面的代码进行简化
//Objects.requireNonNull(object);
Objects.requireNonNull(object,"传递的对象的值是null");//这个方法是上一个方法的重载方法
//效果和简化前的代码一致
}
}
多线程
创建多线程程序的方式
一、创建多线程程序的第一种方式:创建Thread类的子类
java.util.Thread
类:是描述线程的类 我们想要实现多线程程序 就必须继承Thread类- 实现步骤:
(1)创建一个Thread类的子类
(2)在Thread类的子类中重写Thread类中的run方法 设置线程任务(开启线程要做什么?)
(3)在main方法中创建Thread类的子类对象
(4)在main方法中调用Thread类中的方法start方法 开启新的线程 执行run方法
void start()
:使该线程开始执行 java虚拟机调用该线程的run方法
结果是两个线程并发的运行 当前线程(main线程)和另一个线程(创建的新线程 执行其run方法)
多次启动一个线程是非法的 特别是当线程已经结束执行后 不能再重新启动
java程序属于抢占式调度 那个线程的优先级高 哪个线程优先执行 同一个优先级随机选择一个执行
public class ThreadClass {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用Thread类中的方法start方法 开启新的线程 执行run方法
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}
}
public class MyThread extends Thread{//1. 创建一个Thread类的子类
//2.在Thread类的子类中重写Thread类中的run方法 设置线程任务(开启线程要做什么?)
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run:" + i);
}
}
}
- 这个多线程程序的执行过程:
(1)JVM执行main方法 找操作系统开辟一条main方法通向淳朴的路径 这个路径叫main(主)线程 CPU通过这个线程,这个路径可以执行main方法
(2)然后执行MyThread mt = new MyThread();
因为创建了一个对象 所以有开辟了一条通向CPU的路径,这条路径通过mt.start();
执行run方法 对于CPU而言 此时就有了两条执行的路径 CPU就有了选择的权利 CPU想执行哪条路径就执行哪条路径 我们控制不了CPU 于是就有了程序的随机打印结果 反过来说 两个线程 一个main线程 一个新线程 一起抢夺CPU的执行权(也叫执行时间)谁抢到了谁执行对应的代码 - 多线程的好处:多个线程之间互不影响 因为他们在不同的栈空间
二、创建多线程程序的第二种方式:实现Runnable
接口:java.lang.Runnable
Runnable
接口应该由那些打算通过某一线程执行其实例的类来实现 类必须定义一个成为run的无参数方法java.lang.Thread
类的构造方法:
(1)Thread(Runnable target)
:分配新的Thread
对象
(2)Thread(Runnable target,String name)
:分配新的Thread
对象- 实现步骤:
(1)创建一个Runnable
接口的实现类
(2)在实现类中重写Runnable
接口的run
方法 设置线程任务
(3)创建一个Runnable
接口的实现类对象
(4)创建Thread
类对象 构造方法中传递Runnable
接口的实现类对象
(5)调用Thread
类中的start
方法 开启新的线程执行run
方法 - 实现
Runnable
接口创建多线程程序的好处:
(1)避免了单继承的局限性:一个类只能继承一个类(一个人只能有一个亲爹) 类继承了Thread
类就不能继承其他的类,实现了Runnable
接口 还可以继承其他的类 实现其他的接口
(2)增强了程序的扩展性 降低了程序的耦合性(解耦),实现Runnable
接口的方式 把设置线程任务和开启新线程进行了分离(解耦),实现类中 重写了run
方法:用来设置线程任务,创建Thread
类对象 调用start
方法:用来开启新线程
public class RunnableClass {
public static void main(String[] args) {
//3.创建一个Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//4.创建Thread类对象 构造方法中传递Runnable接口的实现类对象
Thread thread = new Thread(run);
//5.调用Thread类中的start方法 开启新的线程执行run方法
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
//1.创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable{
//2.在实现类中重写Runnable接口的run方法 设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
设置线程的名称
一、使用Thread类中的方法setName(名字):void setName(String name)
:改变线程名称 使之与参数name相同
二、创建一个带参数的构造方法 参数传递线程的名称 调用父类的带参构造方法 把线程名称传递给父类 让父类(Thread)给子线程起一个名字:Thread(String name)
:分配新的Thread对象
public class ThreadMethod2 {
public static void main(String[] args) {
new Method2MyThread("旺财").start();//开启多线程
Method2MyThread mt = new Method2MyThread();
mt.setName("小强");
mt.start();
}
}
public class Method2MyThread extends Thread{
public Method2MyThread() {}
public Method2MyThread(String name) {
super(name);//把线程名称传递给父类 让父类(Thread)给子线程起一个名字
}
@Override
public void run() {
//获取线程名称
System.out.println(Thread.currentThread().getName());
}
}
获取线程的名称
一、使用Thread类中的方法getName()
:String getName()
:返回该线程的名称
二、线程的名称:
- 主线程:
main
- 新线程:
Thread-0
,Thread-1
…
三、可以先获取到当前正在执行的线程 使用线程中的方法getName()
获取线程的名称:static Thread currentThread()
:返回对当前正在执行的线程对象的引用
public class ThreadMethod1 {
public static void main(String[] args) {
// //3.创建Thread类的子类对象
// MethodMyThread mt = new MethodMyThread();
// //4.调用Thread类中的方法start方法 开启新的线程 执行run方法
// mt.start();
// new MethodMyThread().start();
// new MethodMyThread().start();
//可以使用链式编程直接获取名称 上面的所有代码可以优化成一行代码 为演示现将他们全部注释掉
System.out.println(Thread.currentThread().getName());
/*
* 获取主线程的名称 不能直接通过getName方法获取 因为ThreadMethod1类没有继承Thread类 没有getName方法
* 必须先获取当前正在执行的线程 再使用getName()获取
*/
}
}
//1.创建一个Thread类的子类
public class MethodMyThread extends Thread{
//2.在Thread类的子类中重写Thread类中的run方法 设置线程任务(开启线程要做什么?)
@Override
public void run() {
//获取线程名称
// String nameString = getName();
// System.out.println(nameString);
//为演示另一个获取线程名称的代码而将上述代码注释
Thread thread = Thread.currentThread();//获取当前正在执行的线程的名称
System.out.println(thread);
String nameString = thread.getName();//使用当前正在执行的线程中的方法getName()获取当前线程的名称
System.out.println(nameString);
}
}
暂停线程
public static void sleep(long millis)
:使用当前正在执行的线程以指定的毫秒数暂停(暂时停止运行),毫秒数结束后 线程继续执行
public class ThreadMethod3 {
public static void main(String[] args) {
//模拟秒表
for (int i = 0; i <= 60; i++) {
System.out.println(i);
//使用Thread类的sleep方法让程序睡眠一秒钟
try {
Thread.sleep(1000);//暂停1000毫秒 也就是一秒
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
唤醒的方法
一、void notify()
:唤醒在此对象监视器上等待的单个线程
二、void notifyAll()
:唤醒在此对象监视器上等待的所有线程
public class Notify {
public static void main(String[] args) {
//创建锁对象 保证唯一
Object object = new Object();
//创建一个顾客线程(消费者)
new Thread() {
@Override
public void run() {
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("顾客1告知老板要的包子的种类和数量");
}
try {
object.wait();
} catch (Exception e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子做好了 顾客1开吃!");
System.out.println("=============================");
}
}.start();
//创建一个顾客线程(消费者)
new Thread() {
@Override
public void run() {
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("顾客2告知老板要的包子的种类和数量");
}
try {
object.wait();
} catch (Exception e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子做好了 顾客2开吃!");
System.out.println("=============================");
}
}.start();
// 创建一个老板线程(生产者)
new Thread() {
@Override
public void run() {
//一直做包子
//花5秒做包子
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("老板5秒之后做好包子 告知顾客 可以吃包子了");
//做好包子之后 调用notify方法 唤醒顾客吃包子
//object.notify();//依次唤醒顾客1 顾客2
object.notifyAll();//一次性唤醒顾客1 顾客2
}
}
}.start();
}
}
模拟卖票案例
一、多线程访问了共享的数据 会产生线程安全问题
二、模拟卖票案例:创建三个线程 同时开启 对共享的票进行出售
- 运行程序后出现了线程安全问题:卖票出现了重复的票和不存在的票
(1)出现了重复的票:t0
,t1
,t2
同时执行到了打印输出正在卖第+“ticket”张票
这时ticket
还没有执行到--
(2)出现不存在的票:假设t0
线程抢到了CPU的执行权 进入到run
方法中执行 执行到if
语句 就失去了CPU的执行权,然后t2
线程抢到了CPU的执行权 进入到run
方法中执行 执行到if
语句 就失去了CPU的执行权,然后t1
线程抢到了CPU的执行权 进入到run
方法中执行 执行到if
语句 就失去了CPU的执行权,然后t2
线程又抢到了CPU的执行权 继续执行卖票 输出打印正在卖第+“ticke”张票
假设此时ticket=1
, 那么ticket--
后,ticket=0
继续判断ticket>0
不成立 不再执行,然后t1
线程又抢到了CPU的执行权 继续执行卖票 输出打印正在卖第0张票
那么ticket--
后ticket=-1
继续判断ticket>0
不成立 不再执行。然后t0
线程又抢到了CPU的执行权 继续执行卖票 输出打印正在卖第-1张票
那么ticket--
后ticket=-2
继续判断ticket>0
不成立 不再执行
注意:线程安全问题是不能产生的 我们可以让一个线程在访问共享数据时 无论是否失去了CPU执行权 让其他的线程只能等待 等待当前线程卖完票 其他线程再进行卖票 一定要保证使用一个线程在卖票
- 为了解决这个问题 有三种方法:
(1)同步代码块
(2)同步方法
(3)锁机制
public class Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
TicketRunnableImpl runnableImpl = new TicketRunnableImpl();
System.out.println("run:" + runnableImpl);//run:TicketRunnableImpl@15db9742
//创建thread类对象 构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(runnableImpl);
Thread t1 = new Thread(runnableImpl);
Thread t2 = new Thread(runnableImpl);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
实现卖票案例
一、解决线程安全问题的一种方案:使用同步代码块
- 格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象 可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用:把同步代码块锁住 只让一个线程在同步代码块中执行
二、解决线程安全问题的二种方案:使用同步方法
- 使用步骤:
(1)把访问了共享数据的代码抽取出来 放到一个方法中
(2)在方法上添加synchronized
修饰符
(3)格式:定义方法的格式
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
三、解决线程安全问题的二种方案:使用lock
锁
java.util.concurrent.locks.lock
接口lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作lock
接口中的方法:
(1)void lock()
获取锁
(2)void unlock()
释放锁- 使用步骤:
(1)在成员位置创建一个ReentrantLock
对象
(2)在可能会出现安全问题的代码前调用Lock
接口中的方法Lock
获取锁
(3)在可能会出现安全问题的代码后电泳Lock
接口中的方法unLock
释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketRunnableImpl implements Runnable{
//定义一个多线程共享的票源
//private int ticket = 100;//同步代码块和同步方法使用这个
private static int ticket = 100;//静态方法时使用这个
//1.在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
//创建一个锁对象
Object object = new Object();
//设置线程任务:卖票
@Override
public void run() {
System.out.println("this:" + this);//this:TicketRunnableImpl@15db9742
//使用死循环 让卖票操作重复执行
while(true) {
// //先判断票是否存在
// if (ticket > 0) {
// //票存在 卖票 ticket--
// System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
// ticket--;
// }
// }
//以上代码会导致线程安全问题 现做如下优化
//同步代码块
//可能会出现线程安全问题的代码(访问了共享数据的代码):ticket是共享数据 所有跟ticket有关的代码都是
// synchronized (object) {
// //先判断票是否存在
// if (ticket > 0) {
// //票存在 卖票 ticket--
// System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
// ticket--;
// }
// }
//payTicket();//调用同步方法
//payTicketStatic();//调用静态方法
//2.在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
l.lock();
//先判断票是否存在
// if (ticket > 0) {
// //票存在 卖票 ticket--
// System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
// ticket--;
// }
// //3.在可能会出现安全问题的代码后电泳Lock接口中的方法unLock释放锁
// l.unlock();
//以上代码可以被优化
if (ticket > 0) {
try {
//票存在 卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
//3.在可能会出现安全问题的代码后电泳Lock接口中的方法unLock释放锁
l.unlock();//无论程序是否异常 都会把锁释放
}
}
}
}
/*同步技术的原理:
* 使用了一个锁对象 这个锁对象脚同步锁 也叫对象锁 也叫对象监视器
* 3个线程一起抢夺CPU的执行权 谁抢到了谁执行run方法进行卖票
* 假设t0抢到了CPU的执行权 执行run方法 遇到了synchronized代码块
* 这时t0会检查synchronized代码块是否有锁对象 发现有 就会获取到锁对象(把锁对象拿走) 进入到同步中执行
* 假设此时t1也抢到了CPU的执行权 执行run方法 遇到了synchronized代码块
* 这时t1会检查synchronized代码块是否有锁对象 发现没有 t1就会进入阻塞状态 会一直等待t0线程归还锁对象
* 一直到t0线程执行完同步中的代码 会把锁对象归还给同步代码块 t1才能获取到锁对象 进入到同步中执行
* 总结:同步中的线程 没有执行完毕不会释放锁 同步外的线程没有锁进不去同步
*/
//为演示第二种方法 现将上述代码注释
/*定义一个同步方法
* 同步方法也会把方法内部的代码锁住 只让一个线程执行 同步方法的锁对象是谁?就是实现类对象 new RunnableImpl()
* 也就是this
*/
// public synchronized void payTicket() {
// //先判断票是否存在
// if (ticket > 0) {
// //票存在 卖票 ticket--
// System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
// ticket--;
// }
// }
//为演示另一种方法 现将上述代码注释
/*
* 静态的同步方法
* 锁对象是谁?不能是this this是创建对象之后产生的 静态方法优先于对象 静态方法的锁对象是本类的class属性 也就是class文件对象(反射)
*/
public static synchronized void payTicketStatic() {
//先判断票是否存在
if (ticket > 0) {
//票存在 卖票 ticket--
System.out.println(Thread.currentThread().getName() + "-->" +"正在卖第" + ticket + "张票");
ticket--;
}
}
}
卖包子案例
一、等待唤醒案例:线程之间的通信
二、实现卖包子案例:
- 创建一个顾客线程(消费者):告知老板要的包子的种类和数量 调用
wait
方法 放弃CPU的执行 进入到WAITING
状态(无限等待) - 创建一个老板线程(生产者):花了5秒做包子 做好包子之后 调用
notify
方法 唤醒顾客吃包子
注意:
- 顾客和老板线程必须使用同步代码块包裹起来 保证等待和唤醒只有一个在执行
- 同步使用的锁对象必须保证唯一
- 只有锁对象才能调用wait和notify方法
三、Objects类中的方法:
void wait()
:在其他线程调用此对象的notify()
方法或notifyAll()
方法前 导致当前线程等待void notify()
:唤醒在此对象监视器上等待的单个线程 会继续执行wait
方法之后的代码
public class WaitAndNotify {
public static void main(String[] args) {
//创建锁对象 保证唯一
Object object = new Object();
//创建一个顾客线程(消费者)
new Thread() {
@Override
public void run() {
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("告知老板要的包子的种类和数量");
}
try {
object.wait();
} catch (Exception e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子做好了 开吃!");
System.out.println("=============================");
}
}.start();
// 创建一个老板线程(生产者)
new Thread() {
@Override
public void run() {
//一直做包子
//花5秒做包子
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("老板5秒之后做好包子 告知顾客 可以吃包子了");
//做好包子之后 调用notify方法 唤醒顾客吃包子
object.notify();
}
}
}.start();
}
}
四、进入到TimeWaiting(计时等待)
有两种方式:
- 使用
sleep(long m)
方法 在毫秒值结束之后 线程睡醒进入到Runnable/Blocked
状态 - 使用
wait(long m)
方法wait
方法如果在毫秒值结束之后 还没有被notify
唤醒 就会自动醒来
public class WaitAndNotify2 {
public static void main(String[] args) {
//创建锁对象 保证唯一
Object object = new Object();
//创建一个顾客线程(消费者)
new Thread() {
@Override
public void run() {
//保证等待和唤醒只有一个在执行 需要使用同步技术
synchronized (object) {
System.out.println("告知老板要的包子的种类和数量");
}
try {
object.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子做好了 开吃!");
System.out.println("=============================");
}
}.start();
}
}
线程间通信
一、线程间通信:多个线程在处理同一个资源 但是处理的动作(线程任务)却不相同,比如:线程1(包子铺)生产包子 线程2(吃货)吃包子 包子可以理解为同一资源 线程1和线程2处理的动作 一个是生产一个是消费 那么线程1和线程2之间就存在线程通信问题
二、用等待唤醒机制来保证线程间通信有效利用资源
三、等待与唤醒机制:线程之间的通信
四、重点:有效的利用资源(生产一个包子 吃一个包子 再生产一个包子 再吃一个包子……)
五、通信:对包子的状态进行判断
- 没有包子–》吃货线程唤醒包子铺线程–》吃货线程等待–》包子铺线程做包子–》做好包子–》修改包子的状态为有
- 有包子–》包子铺线程唤醒吃货线程–》包子铺线程等待–》吃货吃包子–》吃完包子–》修改包子的状态为没有
六、等待唤醒中的方法: wait
:线程不再活动 不再参与调度 进入wait
set
中 因此不会浪费CPU资源 也不会去竞争锁 这时的线程状态是waiting
,他还要等着别的线程执行一个特别的动作——notify
也就是通知(notify
)在这个对象上等待的线程从wait
set
中释放出来 重新进入到调度队列(ready
queue
)中notify
:选取所通知对象的wait
set
中的一个线程释放 如:餐馆有空位后 等餐最久的顾客先入座notifyAll
:释放所通知对象的wait
set
上的全部线程
【注】:哪怕只通知了一个等待的线程 被通知线程也不能立即恢复执行 因为他当初中断的地方是在同步块内 而此刻他已经不持有锁,所以他需要再次尝试去获取锁(很可能面临其他线程的竞争) 成功后才能在当初调用
wait
方法之后的地方恢复执行
七、总结:如果能获取锁 线程就从等待状态变成执行状态 否则 从wait
set
出来 又进入entry
set
线程就从等待状态又变成阻塞状态
八、调用wait
和notify
方法需要注意的细节:
wait
方法和notify
方法必须要由同一个锁对象调用 因为对应的锁对象可以通过notify
唤醒使用同一个锁对象调用的wait
方法后的线程wait
方法与notify
方法是属于Object
类的方法的 因为锁对象可以是任意对象 而任意对象的所属类都是继承了Object
类的对象wait
方法与notify
方法必须要在同步代码块或者同步函数中使用 因为必须通过锁对象调用者两个方法
九、等待唤醒机制需求分析:需要哪些类
- 资源类:包子类
设置包子的属性:皮 馅
包子的状态:有true 没有false - 生产者(包子铺)类:是一个线程类 可以继承
Thread
设置线程任务(run
):生产包子
对包子的状态进行判断:
true
:有包子 包子铺调用wait
方法进入等待状态
false
:没有包子 包子铺交替生产两种包子 有两种状态(i%2==0
) 包子铺生产好了包子 修改包子的状态为true
有 唤醒吃货线程 让吃货线程吃包子 - 消费者(吃货)类:是一个线程类 可以继承
Thread
设置线程任务(run
):吃包子
对包子的状态进行判断:
false
:没有包子 吃货线程调用wait
方法进入等待状态
true
:有包子 吃货吃包子 吃货吃完包子 修改包子的状态为false
没有 吃货唤醒包子铺线程 生产包子 - 测试类:
包含main
方法 程序执行的入口 启动程序
创建包子对象:创建包子铺线程 开启 创建吃货线程 开启
public class WaitAndNotify {
public static void main(String[] args) {
//创建包子对象
BaoZi bz = new BaoZi();
//创建包子铺线程 开启 生产包子
new BaoZiPu(bz).start();
//创建吃货线程 开启 吃包子
new ChiHuo(bz).start();
}
}
public class BaoZi {
String pi;//皮
String xian;//馅
//包子的状态:有true 没有false 设置初始值为false没有包子
boolean flag = false;
}
/*
* 包子铺和包子的线程关系:互斥关系 通信关系。必须同时同步技术保证两个线程只能有一个在执行
* 锁对象必须保证唯一 可以使用包子对象作为锁对象
* 包子铺类和吃货类就需要把包子对象作为参数传递进来:
* 1.需要在成员位置创建一个包子变量
* 2.使用带参数构造方法 为这个包子变量赋值
*/
public class BaoZiPu extends Thread{
//1.需要在成员位置创建一个包子变量
private BaoZi bz;
// 2.使用带参数构造方法 为这个包子变量赋值
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
//设置线程任务run:生产包子
@Override
public void run() {
int count = 0;
//让包子铺一直生产包子
while (true) {
//必须同时同步技术保证两个线程只能有一个在执行
synchronized (bz) {
//对包子的状态进行判断
if(bz.flag == true) {
//包子铺调用wait方法进入等待状态
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("包子铺正在生产:" + bz.pi + bz.xian + "包子");
//生产包子需要3秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//包子铺生产好了包子
//修改包子的状态为true有
bz.flag = true;
//唤醒吃货线程 让吃货线程吃包子
bz.notify();
System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货可以开始吃了");
}
}
}
}
public class ChiHuo extends Thread{
//1.需要在成员位置创建一个包子变量
private BaoZi bz;
// 2.使用带参数构造方法 为这个包子变量赋值
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
//设置线程任务run:生产包子
@Override
public void run() {
while (true) {
//必须同时同步技术保证两个线程只能有一个在执行
synchronized (bz) {
//对包子的状态进行判断
if(bz.flag == false) {
//包子铺调用wait方法进入等待状态
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码:吃包子
System.out.println("吃货正在吃:" + bz.pi +bz.xian + "的包子");
//吃货吃完包子修改包子的状态为false没有
bz.flag = false;
//吃货唤醒包子铺线程 生产包子
bz.notify();
System.out.println("吃货已经把" + bz.pi +bz.xian + "的包子吃完了,包子铺开始生产包子");
System.out.println("==========================================================");
}
}
}
}
线程池
一、java.util.concurrent.Executors
:线程池的工厂类 用来生成线程池
二、Executors
类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads)
:创建一个可重用固定线程数的线程池
(1)参数:int nThreads
:创建线程池中包含的线程数量
(2)返回值:ExecutorService
接口 返回的是ExecutorService
接口的实现类对象 我们可以使用ExecutorService
接口接收(面向接口编程)- java.util.concurrent.ExecutorService:线程池接口
- submit(Runnable task):提交一个Runnable任务用于执行 用来从线程池中获取线程 调用start方法 执行线程任务
- void shutdown():关闭/销毁线程池的方法
三、线程池的使用步骤:
- 使用线程池的工厂类
Executors
里面提供的静态方法newFixedThreadPool
生产一个指定线程数量的线程池 - 创建一个类 实现
Runnable
接口 重写run
方法 设置线程任务 - 调用
ExecutorService
中的方法submit
传递线程任务(实现类) 开启线程 执行run
方法 - 调用
ExecutorService
中的方法shutdown
销毁线程池(不建议)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPool {
public static void main(String[] args) {
// * 1.使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
// * 3.调用ExecutorService中的方法submit 传递线程任务(实现类) 开启线程 执行run方法
//我们刚才只创建了一个容量为2的线程池 而我们现在有三个任务 所以有一个任务就等待有任务归还给线程池 归还完了他再使用
//线程池会一直开启 使用完了线程 会自动把线程归还给线程池 线程可以继续使用
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
es.submit(new RunnableImpl());
// * 4.调用ExecutorService中的方法shutdown销毁线程池(不建议)
es.shutdown();
//es.submit(new RunnableImpl());//错误!线程池已经被销毁了 不能再执行任务
}
}
public class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "创建了一个新线程");
}
}