一:异常处理:
如图:
1.1.Error
和 Exception
Throwable
(异常体系的根类)可分为两类:Error 和 Exception。分别对应着 java.lang.Error
与java.lang.Exception
两个类。
Error
:
Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。
• 例如:StackOverflowError
(栈内存溢出)和 OutOfMemoryError
P(堆内存溢出,简称 OOM
)。
Exception
:
其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。
例如:
• 空指针访问
• 试图读取不存在的文件
• 网络连接中断
• 数组角标越界
说明:
• 无论是 Error 还是 Exception,还有很多子类,异常的类型非常丰富。当代码运行出现异常时,特别是我们不熟悉的异常时,不要紧张,把异常的简单类名,拷贝到 API 中去查去认识它即可。
• 我们本章讲的异常处理,其实针对的就是 Exception。
在Java中,异常分为两种主要类型:编译时异常(Checked Exceptions)和运行时异常(Runtime Exceptions)。
下面我们来举例看看:
3.2 运行时异常
package com.atguigu.exception;
import org.junit.Test;
import java.util.Scanner;
public class TestRuntimeException {
@Test
public void test01(){
//NullPointerException
int[][] arr = new int[3][];
System.out.println(arr[0].length);
}
@Test
public void test02(){
//ClassCastException
Object obj = 15;
String str = (String) obj;
}
@Test
public void test03(){
//ArrayIndexOutOfBoundsException
int[] arr = new int[5];
for (int i = 1; i <= 5; i++) {
System.out.println(arr[i]);
}
}
@Test
public void test04(){
//InputMismatchException
Scanner input = new Scanner(System.in);
System.out.print("请输入一个整数:");//输入非整数
int num = input.nextInt();
input.close();
}
@Test
public void test05(){
int a = 1;
int b = 0;
//ArithmeticException
System.out.println(a/b);
}
}
3.3 编译时异常
package com.atguigu.exception;
import org.junit.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class TestCheckedException {
@Test
public void test06() {
Thread.sleep(1000);//休眠 1 秒 InterruptedException
}
@Test
public void test07(){
Class c =
Class.forName("java.lang.String");//ClassNotFoundException
}
@Test
public void test08() {
Connection conn = DriverManager.getConnection("....");
//SQLException
}
@Test
public void test09() {
FileInputStream fis = new FileInputStream("尚硅谷 Java秘籍.txt"); //FileNotFoundException
}
@Test
public void test10() {
File file = new File("尚硅谷 Java秘籍.txt");
FileInputStream fis = newFileInputStream(file);//FileNotFoundException
int b = fis.read();//IOException
while(b != -1){
System.out.print((char)b);
b = fis.read();//IOException
}
fis.close();//IOException
}
}
3.4.异常处理:
主要有下面两种方法:
3.4.1. try-catch-finally
基本格式:
捕获异常语法如下:
try{
...... //可能产生异常的代码
}
catch(
异常类型 1 e ){
...... //当产生异常类型 1 型异常时的处置措施
}
catch(
异常类型 2 e ){
...... //当产生异常类型 2 型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}
举例:
try {// 可能会抛出异常的代码int result = 10 / 0; // 除零操作会抛出ArithmeticException
} catch (ArithmeticException e) {// 捕获并处理异常System.out.println("除零异常:" + e.getMessage());
}
注意,范围大的要写在下面。即子类异常要写在父类上面。
try中声明的变量,出了try结构后,就不能调用了。
try-catch可以嵌套使用。
3.4.2.finally
的使用:
finally内部的代码一定会执行,除非强制退出。finally与catch是可选的,但是finally无法单独存在。
Q:为什么要finally呢,异常处理后后面的代码还是会执行,那岂不是可以不用finally?
A:因为在catch中可能也有错,此时用finally可以保证它的执行。
、
即使要输出返回值,也是会先输出test结束。
例题分析:
如果return ++num;
没有注释掉,如果传入10,那么返回值应该是11,而现在返回值是10,因为在内存中,操作是一步一步压入栈中的,而在return num
中,已经决定了返回值为10。
比如,把流的关闭放在finally中。
3.4.3.throws
:
如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通过,但是在当前方法体中可能不适合处理或无法给出合理的处理方式,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。就像把问题一层一层往上面丢,在当前层不报错,但是在上一层调用还是会报错,直到某一行彻底解决该问题。
注意,在main方法时该问题必须解决,不然就会抛给虚拟机栈,就会出错。
如果父类没有抛出异常,那么子类一定不能抛出异常,因为子类抛出的异常必须小于父类,所以在实际的开发中,有的明明没有异常的父类却抛出异常,是为了让子类能够抛出异常。
比如我们用多态性来解释一下原因,我们用person类来接收一个student类,我们对该对象调用的方法进行异常处理,因为运行看右边,编译看左边,所以我们针对编译的时候的报错,我们要catch父类person的错误,但是实际运行的时候我们是遇到了右边student的错误,如果子类的错误还大于了父类的错误,那么这个异常处理显然是盖不住的。
注意,这是针对编译异常的,运行异常写不写都无所谓。
声明异常格式:
修饰符 返回值类型 方法名(参数) throws异常类名 1,异常类名 2…{ },在 throws 后面可以写多个异常类型,用逗号隔开。
举例:
public void readFile(String file) throws FileNotFoundException,IOException {
...
// 读文件的操作可能产生 FileNotFoundException 或 IOException 类型的异
常
FileInputStream fis = new FileInputStream(file);
//...
}
3.4.4.手动抛出异常对象:throw:
Java 中异常对象的生成有两种方式:
• 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,那么针对当前代码,就会在后台自动创建一个对应异常类的实例对象并抛出。
• 由开发人员手动创建:new异常类型([实参列表]);,如果创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,但是一旦 throw 抛出,就会对程序运行产生影响了。
格式:
throw new异常类名(参数);
throw 语句抛出的异常对象,和 JVM
自动创建和抛出的异常对象一样。
• 如果是编译时异常类型的对象,同样需要使用 throws 或者 try…catch 处理,否则编译不通过。
• 如果是运行时异常类型的对象,编译器不提示。
• 可以抛出的异常必须是 Throwable
或其子类的实例。下面的语句在编译时将会产生语法错误:
throw new String("want to throw");
throw就相当于按照实际情况,自己抛出一个异常,因此,也是需要被解决的。如果当前方法没有 try…catch 处理这个异常对象,throw 语句就会代替 return语句提前终止当前方法的执行,并返回一个异常对象给调用者。因此,throw后面的代码无法被执行,直接就出去了。
public class TestThrow {
public static void main(String[] args) {
try {System.out.println(max(4,2,31,1));
} catch (Exception e) {e.printStackTrace();
}
try {System.out.println(max(4));
} catch (Exception e) {e.printStackTrace();
}
try {System.out.println(max());
} catch (Exception e) {e.printStackTrace();
}
}
public static int max(int... nums){if(nums == null || nums.length==0){throw new IllegalArgumentException("没有传入任何整数,无法获取最大值");}
int max = nums[0];
for (int i = 1; i < nums.length; i++) {if(nums[i] > max){max = nums[i];}}return max;}
}
IllegalArgumentException
是Java中的一个标准异常类,是Java API
的一部分,可直接在Java使用,无需额外导入。当参数不合法时,Java系统会自动抛出IllegalArgumentException
异常。
e.printStackTrace()
是Java中Throwable
类的一个方法,它用于打印异常的堆栈信息到标准错误流。当异常发生时,可以使用e.printStackTrace()
方法将异常的详细信息打印到控制台
3.4.5.自定义异常:
举例:
//自定义异常类
class MyException extends Exception {static final long serialVersionUID = 23423423435L;private int idnumber;public MyException(String message, int id) {super(message);this.idnumber = id;
}public int getId() {return idnumber;}
}public class MyExpTest {public void regist(int num) throws MyException {if (num < 0) throw new MyException("人数为负值,不合理", 3);else System.out.println("登记人数" + num);}public void manager() {
try {
regist(100);
} catch (MyException e) {
System.out.print("登记失败,出错种类" + e.getId());
}
System.out.print("本次登记操作结束");
}//main方法
public static void main(String args[]) {MyExpTest t = new MyExpTest();t.manager();}
}
在 regist
方法中,当 num
小于0时,会抛出 MyException
异常。但是,MyException
类本身并不解决异常,它只是定义了异常的类型和包含的信息(在这个例子中,是一个字符串消息和一个id号)。
实际处理这个异常的是 manager
方法。在 manager
方法中,我们有一个 try-catch 块,它尝试调用 regist
方法,并捕获可能抛出的 MyException
异常。如果 MyException
被抛出,那么 catch 块就会执行,打印出 “登记失败,出错种类” 和异常的 id。这就是异常的处理方式。
所以,虽然 MyException
并没有解决异常,但它提供了异常的详细信息,而处理异常的责任在于调用可能抛出异常的方法的代码。
二:多线程:
1.程序、进程与线程:
• 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
• 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的 QQ,运行中的网易音乐播放器。
– 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
– 程序是静态的,进程是动态的。
– 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
– 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos 窗口等软件。
• 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
– 一个进程同一时间若并行执行多个线程,就是支持多线程的。
– 线程作为 CPU调度和执行的最小单位。
– 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
– 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
2.并行与并发:
并行和并发是计算机科学中两个重要的概念。
并行是指两个或多个任务同时进行的能力。在计算机系统中,这意味着多个处理器或核心同时执行多个任务。并行计算可以显著提高计算速度和系统的性能。
并发是指多个任务在同一时间段内交替执行的能力。在计算机系统中,这意味着系统能够同时处理多个任务,即使实际上只有一个处理器或核心。并发可以提高系统的效率和资源利用率。
并行和并发的区别在于并行是指多个任务同时执行,而并发是指多个任务在同一时间段内交替执行。在实际应用中,这两个概念经常会同时出现,例如在多核处理器上同时执行多个线程就是并行和并发的结合。
总的来说,并行和并发都是为了提高计算机系统的性能和效率,但它们的实现方式和应用场景有所不同。
3.创建线程:
方式 1:继承 Thread 类:
Java 通过继承 Thread 类来创建并启动多线程的步骤如下:
1. 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程
需要完成的任务
2. 创建 Thread 子类的实例,即创建了线程对象
3. 调用线程对象的 start()方法来启动该线程
代码如下:
package com.atguigu.thread;
//自定义线程类public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的 String 参数的构造方法,指定线程的名称super(name);
}
/**
* 重写 run 方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);}}
}
如果此时main中有其他的方法执行,那么输出是交替的,体现了并发的特点。
注意,要调用start方法,而不是run方法。并且start只能调用一次!!!
或者这么写:
new MyThread(){public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);}}
}.test();
方式 2:实现 Runnable 接口:
Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法
步骤如下:
1.定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体
同样是该线程的线程执行体。
2.创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建
3.调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。
代码如下:
package com.atguigu.thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " "+ i);}}
}
测试类:
package com.atguigu.thread;
public class TestMyRunnable {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "长江");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("黄河 " + i);
}
}
}
第一个参数是实现了Runnable接口的对象,它包含了要在新线程中执行的任务(即run方法)。第二个参数是线程的名称,它可以用来标识线程。也可以只传入一个变量,即接口的实现类。
通过实现 Runnable 接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在 run 方法里面。
在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnabletarget) 构造出对象,然后调用 Thread 对象的 start()方法来运行多线程代码。
实际上,所有的多线程代码都是通过运行 Thread 的 start()方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。
说明:Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run()方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是该Thread 线程负责执行其 target 的 run()方法。
当然,它也可以像第一种方法一样用匿名对象:
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" +i);}}
}).start();
4.thread的常用方法:
4.1:常用结构:
4.2.常用方法:
• public void run() :此线程要执行的任务在此处定义代码。
• public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
• public String getName() :获取当前线程名称。
• public void setName(String name):设置该线程名称。
• public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在
Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
• public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
• public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。
• public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚
未终止,则为活动状态。
• void join() :等待该线程终止。
void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时
间到,将不再等待。
void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 +
nanos 纳秒。
代码如下:
// 创建一个新的线程
Thread thread = new Thread();// 启动线程
thread.start();// 使线程进入休眠状态
try {Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {e.printStackTrace();
}// 暂停当前正在执行的线程对象,并执行其他线程
thread.yield();// 在其他线程中调用当前线程的join方法,会将当前线程加入到调用者线程中,直到当前线程执行完毕,调用者才能继续执行
try {thread.join();
} catch (InterruptedException e) {e.printStackTrace();
}// 返回对当前正在执行的线程对象的引用
Thread currentThread = Thread.currentThread();// 返回此线程的标识符
long id = thread.getId();// 测试线程是否处于活动状态
boolean isAlive = thread.isAlive();
// 创建一个新的线程
Thread thread = new Thread();// 设置线程的名字
thread.setName("MyThread");// 获取线程的名字
String name = thread.getName();System.out.println("Thread name is: " + name);
4.3.优先级:
代码如下:
// 创建一个新的线程
Thread thread = new Thread();// 设置线程的优先级
thread.setPriority(Thread.MAX_PRIORITY);// 获取线程的优先级
int priority = thread.getPriority();System.out.println("Thread priority is: " + priority);
4.4.生命周期:
JDK5之前:
JDK5之后:
5.线程安全问题与解决:
举例:
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共 100 个(即,只能出售 100 张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。
错误代码:
class TicketSaleThread extends Thread {
private static int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,票号:" +ticket);
ticket--;
}}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();}
}
会有重票的情况,因为当一张票被卖出后,还没有减去,别的线路又执行了。
注意,如果是继承的方法,那么票这个变量必须static修饰,如果是接口的方法,就可以不修饰。
解决方法:
1.同步代码块:
举例:
接口与同步代码块
public class TicketSeller implements Runnable {private int tickets = 100; // 总共100张票@Overridepublic void run() {while (tickets > 0) {// 使用同步代码块确保线程安全synchronized (this) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");tickets--;}}}}public static void main(String[] args) {TicketSeller ticketSeller = new TicketSeller();Thread t1 = new Thread(ticketSeller, "窗口一");Thread t2 = new Thread(ticketSeller, "窗口二");Thread t3 = new Thread(ticketSeller, "窗口三");t1.start();t2.start();t3.start();}
}
有三点注意:
-
synchronized (this)
,括号中可以写任一唯一对象,只是因为方便且合适,所以用this,因为this只new了一个。 -
接口式的共享资源不用静态化,因为只new了一个对象,它们已经共享。
-
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
继承与同步代码块:
public class TicketSeller extends Thread {private static int tickets = 100; // 总共100张票@Overridepublic void run() {while (tickets > 0) {// 使用同步代码块确保线程安全synchronized (TicketSeller.class) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");tickets--;}}}}public static void main(String[] args) {TicketSeller seller1 = new TicketSeller();TicketSeller seller2 = new TicketSeller();TicketSeller seller3 = new TicketSeller();seller1.start();seller2.start();seller3.start();}
}
有三点注意:
-
synchronized ()
,括号中要写任一唯一对象,如果用this,因为this只new了多个,所以会出错。 -
继承的共享资源必须静态化,因为new了多个对象,它们还没有共享。
-
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
2.同步方法:
举例:
同步方法与接口:
public class TicketSeller implements Runnable {private int tickets = 100; // 总共100张票public synchronized void sellTicket() {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "售出了第" + tickets + "张票");tickets--;}}@Overridepublic void run() {while (tickets > 0) {sellTicket();}}public static void main(String[] args) {TicketSeller ticketSeller = new TicketSeller();Thread t1 = new Thread(ticketSeller, "窗口一");Thread t2 = new Thread(ticketSeller, "窗口二");Thread t3 = new Thread(ticketSeller, "窗口三");t1.start();t2.start();t3.start();}
}
有三点注意:
-
synchronized
的接口实现类,默认就有this。 -
接口式的共享资源不用静态化,因为只new了一个对象,它们已经共享。
-
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
同步方法与继承:
public class TicketSeller extends Thread {private static int tickets = 100; // 总共100张票public static synchronized void sellTicket() {if (tickets > 0) {System.out.println(getName() + "售出了第" + tickets + "张票");tickets--;}}@Overridepublic void run() {while (tickets > 0) {sellTicket();}}public static void main(String[] args) {TicketSeller seller1 = new TicketSeller();TicketSeller seller2 = new TicketSeller();TicketSeller seller3 = new TicketSeller();seller1.start();seller2.start();seller3.start();}
}
有三点注意:
-
synchronized
,要有任一唯一对象,所以方法要加static。 -
继承的共享资源必须静态化,因为new了多个对象,它们还没有共享。
-
synchronized
不能写在while循环外面,这样当一个线程抢占到锁之后,会一直持续到循环结束。
注意,不要看到继承就肯定要有static,可能用单独的一个类来储存金额,然后只new一次,传参都用这个对象即可。
3.synchronized
的锁是什么:
同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:
• 静态方法:当前类的 Class 对象(类名.class)
• 非静态方法:this
6.死锁:
懒汉式安全问题的解决:
public class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
注意,可能会有指令重排问题,可以加入volatile关键字处理。此外,在静态方法中,无法使用 this
关键字来获取类的 Class 对象。
死锁:
死锁是指在多个进程或线程争夺资源时,由于彼此之间的互相等待对方释放资源而导致的一种僵局状态。当多个进程或线程同时持有一些资源,并且彼此都在等待对方释放资源时,就会发生死锁。
死锁的发生通常是由于资源的竞争和分配不当所导致的。当一个进程或线程无法继续执行,因为它需要的资源正被其他进程或线程持有,并且这些进程或线程又在等待该进程或线程所持有的资源时,就会发生死锁。
下面是一个简单的Java代码示例,用于演示死锁的情况:
public class DeadlockExample {private static Object lock1 = new Object();private static Object lock2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock1) {System.out.println("Thread 1: Holding lock 1...");try {Thread.sleep(100);} catch (InterruptedException e) {}System.out.println("Thread 1: Waiting for lock 2...");synchronized (lock2) {System.out.println("Thread 1: Holding lock 1 and lock 2...");}}}});Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock2) {System.out.println("Thread 2: Holding lock 2...");try {Thread.sleep(100);} catch (InterruptedException e) {}System.out.println("Thread 2: Waiting for lock 1...");synchronized (lock1) {System.out.println("Thread 2: Holding lock 2 and lock 1...");}}}});thread1.start();thread2.start();}
}
在这个示例中,线程1尝试获取lock1,然后等待获取lock2。而线程2尝试获取lock2,然后等待获取lock1。由于两个线程的获取顺序相反,它们会相互等待对方释放资源,从而导致死锁。
这个例子展示了死锁的原因:当多个线程相互等待对方释放资源时,就会发生死锁。要避免死锁,需要谨慎设计多线程的资源获取顺序,并且确保在获取资源后及时释放。
Lock锁:
class Window implements Runnable{
int ticket = 100;
//1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try{
//2. 调动 lock(),实现需共享的代码的锁定
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else{
break;
}
}finally{
//3. 调用 unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
}
public class ThreadLock {
public static void main(String[] args) {
Window t = new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
注意,为了确保解锁,可以把该步骤写在finally里面。
7.线程的通信:
![image-20240205224459683](https://img-blog.csdnimg.cn/img_convert/96caa1ae0ca7e4d1118ea642dc0a8166.png)
例题:使用两个线程打印 1-100。线程 1, 线程 2 交替打印
class Communication implements Runnable {
int i = 1;
public void run() {while (true) {synchronized (this) {notify();if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);} elsebreak;try {wait();} catch (InterruptedException e) {e.printStackTrace();}
} }
}
}
由于在run方法中使用了wait()和notify()来实现线程间的通信,每个线程在打印完一个数字后会调用wait()方法进入等待状态,并释放锁。然后另一个线程会被唤醒并继续执行,打印下一个数字。接着它也会调用wait()方法进入等待状态,释放锁,让另一个线程继续执行。如此交替进行,直到1-100全部打印完成。
实际情况举例:
public class ConsumerProducerTest {public static void main(String[] args) {Clerk clerk = new Clerk();Producer p1 = new Producer(clerk);Consumer c1 = new Consumer(clerk);Consumer c2 = new Consumer(clerk);p1.setName("生产者 1");c1.setName("消费者 1");c2.setName("消费者 2");p1.start();c1.start();c2.start();}
}// 生产者
class Producer extends Thread {private Clerk clerk;public Producer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {System.out.println("=========生产者开始生产产品========");while (true) {try {Thread.sleep(40);} catch (InterruptedException e) {e.printStackTrace();}// 要求 clerk 去增加产品clerk.addProduct();}}
}// 消费者
class Consumer extends Thread {private Clerk clerk;public Consumer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {System.out.println("=========消费者开始消费产品========");while (true) {try {Thread.sleep(90);} catch (InterruptedException e) {e.printStackTrace();}// 要求 clerk 去减少产品clerk.minusProduct();}}
}// 资源类
class Clerk {private int productNum = 0; // 产品数量private static final int MAX_PRODUCT = 20;private static final int MIN_PRODUCT = 1;// 增加产品public synchronized void addProduct() {if (productNum < MAX_PRODUCT) {productNum++;System.out.println(Thread.currentThread().getName() + "生产了第" + productNum + "个产品");// 唤醒消费者this.notifyAll();} else {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}}// 减少产品public synchronized void minusProduct() {if (productNum >= MIN_PRODUCT) {System.out.println(Thread.currentThread().getName() + "消费了第" + productNum + "个产品");productNum--;// 唤醒生产者this.notifyAll();} else {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}}
}
- 首先,创建一个
Clerk
对象,它代表了共享资源。在这个例子中,共享资源是产品数量productNum
。 - 然后,创建一个生产者线程
Producer
和两个消费者线程Consumer
。他们都需要Clerk
对象来进行操作。 - 当生产者线程开始运行时,它会尝试生产产品。如果当前产品数量
productNum
小于最大值MAX_PRODUCT
,生产者就会生产一个产品,并将产品数量productNum
加一。然后,它会调用notifyAll()
方法唤醒所有在Clerk
对象上等待的线程。 - 如果当前产品数量
productNum
已经达到最大值MAX_PRODUCT
,生产者线程就不能再生产产品。此时,它会调用wait()
方法让自己进入等待状态,并释放Clerk
对象的锁。 - 当消费者线程开始运行时,它会尝试消费产品。如果当前产品数量
productNum
大于等于最小值MIN_PRODUCT
,消费者就会消费一个产品,并将产品数量productNum
减一。然后,它会调用notifyAll()
方法唤醒所有在Clerk
对象上等待的线程。 - 如果当前产品数量
productNum
小于最小值MIN_PRODUCT
,消费者线程就不能再消费产品。此时,它会调用wait()
方法让自己进入等待状态,并释放Clerk
对象的锁。 - 这个过程会一直重复,直到程序结束。
8.callable与线程池:
举例:
// 1. 创建一个实现 Callable 的实现类
class NumThread implements Callable {// 2. 实现 call 方法,将此线程需要执行的操作声明在 call()中@Overridepublic Object call() throws Exception {int sum = 0;for (int i = 1; i <= 100; i++) {if (i % 2 == 0) {System.out.println(i);sum += i;}}return sum;}
}public class CallableTest {public static void main(String[] args) {// 3. 创建 Callable 接口实现类的对象NumThread numThread = new NumThread();// 4. 将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象FutureTask futureTask = new FutureTask(numThread);// 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()new Thread(futureTask).start();// 接收返回值try {// 6. 获取 Callable 中 call 方法的返回值// get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值。Object sum = futureTask.get();System.out.println("总和为:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
下面是一个使用线程池的Java代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class ThreadPoolExample {public static void main(String[] args) {// 创建一个固定大小的线程池ExecutorService executorService = Executors.newFixedThreadPool(5);// 提交任务到线程池for (int i = 0; i < 10; i++) {final int taskID = i;executorService.submit(new Runnable() {public void run() {System.out.println("正在执行任务 " + taskID);try {TimeUnit.SECONDS.sleep(2); // 模拟任务执行时间} catch (InterruptedException e) {e.printStackTrace();}System.out.println("任务 " + taskID + " 执行完毕");}});}// 关闭线程池executorService.shutdown();}
}