本文主要是介绍【学习日记2023.4.12】之Thread的实现方式和解决线程安全问题的三种办法以及线程通讯问题和Thread的生命周期,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录 1. 多线程相关概念 2. 多线程的实现方式 2.1 继承Thread 2.2 实现Runnable接口 2.3 实现Runnable接口(匿名内部类) 2.4 实现Callable接口 2.5 三种方式的使用选择 3. Thread常用方法 4. 线程安全问题 5. 解决线程安全问题 5.1 同步代码块 5.2 同步方法 5.3 Lock 5.4 线程安全问题小结 6. 线程间通讯 7. 线程的生命周期 8. 总结
1. 多线程相关概念
并发和并行 并发:在同一时刻,有多个指令在CPU单个核心上 交替 执行 并行:在同一时刻,有多个指令在CPU多个核心上 同时 执行 进程和线程 进程:操作系统中正在运行的一个应用程序。 线程:应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾 线程 单线程:程序中如果只有一条执行流程,那这个程序就是单线程的程序。 多线程:程序中如果有多条执行流程,那这个程序就是多线程的程序。 好处和应用场景 好处 应用场景 桌面应用程序 web服务器应用程序
2. 多线程的实现方式
2.1 继承Thread
步骤 定义一个类,继承Thread类 重写run方法 创建对象 调用start()方法,启动线程 /*演示Java语言创建多线程方式一:继承Thread类注意事项:直接调用run方法,不会启动线程start():1.启动线程2.在新启动的线程中,调用run方法继承Thread类的优缺点:优点:代码书写简单缺点:已经继承了一个类,无法再继承其他类,降低了程序的扩展性*/
public class Demo1 {public static void main(String[] args) {//3.创建线程对象final MyThread myThread = new MyThread();//4.调用start方法,启动新的线程myThread.start();for (int i = 1; i <= 5; i++) {System.out.println("主方法中的代码执行了" + i);}}
}//1.定义类继承Thread类
class MyThread extends Thread{//2.重写run方法@Overridepublic void run() {//run方法中的内容,启动线程后,执行的任务for (int i = 1; i <= 5; i++) {System.out.println("新县城执行了" + i);}}
}
注意事项和优缺点 注意事项 启动线程必须是调用start方法,而不是直接调用run方法 直接调用run方法,不会启动线程 start方法的作用 优点: 代码书写简单 缺点: 自定义类继承Thread,无法继续继承其他类,不利于功能的扩展 内存原理 官网:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.5.2 每个Java虚拟机线程都有一个与线程同时创建的专用Java虚拟机栈。 内存图
2.2 实现Runnable接口
步骤 定义任务类,实现Runnable接口 重写run方法 创建任务对象 把任务对象交给一个线程对象处理 public class Demo2 {public static void main(String[] args) {//3.创建线程对象MyRunnable r = new MyRunnable();//4.把任务类的对象,交给线程类Thread t = new Thread(r);//5.通过线程对象(Thread),调用start方法,启动新的线程t.start();for (int i = 1; i <= 5; i++) {System.out.println("主方法中的代码执行了" + i);}}
}//1.自定义类(任务类)实现Runnable接口
class MyRunnable implements Runnable{//2.重写run方法@Overridepublic void run() {//线程启动后要执行的任务for (int i = 1; i <= 5; i++) {System.out.println("新线程执行了" + i);}}
}
优缺点 优点: 任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强 缺点:需要多一个任务对象
2.3 实现Runnable接口(匿名内部类)
public class Demo3 { public static void main ( String [ ] args) { Thread t1 = new Thread ( new Runnable ( ) { @Override public void run ( ) { for ( int i = 1 ; i <= 5 ; i++ ) { System . out. println ( "新线程一执行了" + i) ; } } } ) ; t1. start ( ) ; Thread t2 = new Thread ( ( ) -> { for ( int i = 1 ; i <= 5 ; i++ ) { System . out. println ( "新线程二执行了" + i) ; } } ) ; t2. start ( ) ; for ( int i = 1 ; i <= 5 ; i++ ) { System . out. println ( "主方法中的代码执行了" + i) ; } }
}
2.4 实现Callable接口
问题 问题: 假如线程执行完毕后有一些数据需要返回,前两种方式重写的run方法均不能返回结果 解决: JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式) 第三种创建方式的优点: 可以返回线程执行完毕后的结果 步骤 创建任务对象 定义一个类实现Callable接口,重写call方法(call方法中的内容,就是线程开启后要执行的任务,并且该方法有返回值) 把Callable类型的对象交给FutureTask FutureTask构造方法 说明 public FutureTask<>(Callable call) 把Callable对象交给成FutureTask
FutureTask交给交给Thread 调用Thread对象的start方法启动线程 线程执行完毕后,通过FutureTask对象的get方法获取线程任务的执行结果 FutureTask提供的方法 说明 public V get() throws Exception 获取线程执行call方法返回的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*注意事项及其优缺点:get获取线程执行完成结束后的结果如果线程没结束,死等(阻塞),所以get方法一定要写在start()之后优点:能够获取线程结束后的结果缺点:编码复杂*/
public class Demo1 {public static void main(String[] args) throws ExecutionException, InterruptedException {//3.创建Callable实现类对象MyCallable c = new MyCallable();//4.Callable实现类对象 交给FutureTaskFutureTask<String> f1 = new FutureTask<>(c);//lambda表达式FutureTask<String> f2 = new FutureTask(()->{for (int i = 1; i <= 50; i++) {System.out.println("B向女孩表白的第" + i + "次");}return "喔同意了!";});//5.FutureTask交给ThreadThread t1 = new Thread(f1);Thread t2 = new Thread(f2);t1.start();//开启线程t2.start();//开启线程//6.FutureTask调用get方法获取call方法的返回值final String rusult1 = f1.get();//等第一个线程结束后的结果final String rusult2 = f2.get();//等第二个线程结束后的结果System.out.println("女孩回应A:" + rusult1);System.out.println("女孩回应B:" + rusult2);}
}//1.定义类实现Callable接口
//Callable接口中的泛型,和线程结束后得到结果的类型一致
class MyCallable implements Callable{//2.重写call方法@Overridepublic String call() throws Exception {//是线程开启后执行的任务,并且带有返回值的//模拟向女孩表白for (int i = 1; i <= 50; i++) {System.out.println("A向女孩表白的第" + i + "次");}return "其实你是个好人!";}
}
注意事项和优缺点 注意事项: FutureTask中的get方法会等待线程运行结束,再获取结果.所以不能写到线程开启start方法之前 优点: 实现接口,可以继续继承其他类、实现其他接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果 缺点: 编码复杂
2.5 三种方式的使用选择
前两种方式,哪种方便用哪种 如果需要获取线程执行后的返回结果,只能用第三种方式
3. Thread常用方法
Thread提供的常用方法 说明 public void start() 启动线程 public String getName() 获取线程的名字,线程名字默认Thread-编号 public void setName(String name) 为线程设置名字 public static Thread currentThread() 获取当前执行的线程对象 public static void sleep(long time) 让当前线程的线程休眠多少毫秒后,再继续执行 public final void join() 让调用当前这个方法的线程先执行完
Thread提供的构造方法 说明 public Thread(String name) 指定线程名字 public Thread(Runnable target) 封装Runnable对象成为线程对象 public Thread(Runnable target,String name) 封装Runnable对象成为线程对象,并指定线程名字
public class Demo1 {public static void main(String[] args) throws ExecutionException, InterruptedException {//3.创建Callable实现类对象MyCallable c = new MyCallable();//4.Callable实现类对象 交给FutureTaskFutureTask<String> f1 = new FutureTask<>(c);//lambda表达式FutureTask<String> f2 = new FutureTask(()->{for (int i = 1; i <= 50; i++) {System.out.println(Thread.currentThread().getName() + "向女孩表白的第" + i + "次");}return "喔同意了!";});//5.FutureTask交给ThreadThread t1 = new Thread(f1,"男孩A");Thread t2 = new Thread(f2);t2.setName("男孩B");t1.start();//开启线程t1.join();//让此线程执行完,再执行其他线程t2.start();//开启线程//6.FutureTask调用get方法获取call方法的返回值final String rusult1 = f1.get();//等第一个线程结束后的结果final String rusult2 = f2.get();//等第二个线程结束后的结果System.out.println("女孩回应" + t1.getName() + ":" + rusult1);System.out.println("女孩回应" + t2.getName() + ":" + rusult2);}
}//1.定义类实现Callable接口
//Callable接口中的泛型,和线程结束后得到结果的类型一致
class MyCallable implements Callable{//2.重写call方法@Overridepublic String call() throws Exception {//是线程开启后执行的任务,并且带有返回值的//模拟向女孩表白for (int i = 1; i <= 50; i++) {System.out.println(Thread.currentThread().getName() + "向女孩表白的第" + i + "次");}return "其实你是个好人!";}
}
4. 线程安全问题
4.1 案例
需求: 电影院,一共100张票,三个窗口【同时】售票,用多线程模拟电影院卖票 public class Demo1 {public static void main(String[] args) {MyThread t1 = new MyThread("窗口一");t1.start();MyThread t2 = new MyThread("窗口二");t2.start();MyThread t3 = new MyThread("窗口三");t3.start();}
}class MyThread extends Thread{private static int ticket = 100;//总票数public MyThread() {}public MyThread(String name) {super(name);}//如果父类(接口)中定义的方法声明上没有 throws//重写的方法就不能使用throws来处理异常,不能使用try…catch@Overridepublic void run() {//线程开启后执行的任务,卖票//不断的卖票,死循环while (true){if (ticket <= 0){break;}else {try {Thread.sleep(100);//模拟出票延迟时间} catch (InterruptedException e) {e.printStackTrace();}ticket--;System.out.println(getName() + "开始卖票了,还剩:【" + ticket + "】张票");}}}
}
4.2 问题
模拟售票时间,卖票时 线程休眠100毫秒 现象: 出现负数票,重复票 原因: 多个线程操作了共享资源 什么是线程安全问题: 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
4.3 如何理解共享数据
5. 解决线程安全问题
5.1 同步代码块
格式 synchronized(锁对象) {
// 操作共享数据的代码
}
作用 底层:线程自动获得锁,释放锁 当有线程进入同步代码块时,锁住(该线程获得锁) 当同步代码块中没有线程执行,锁打开(释放锁) 作用:保证同时只有一个线程在操作共享数据 public class Demo1 {public static void main(String[] args) {MyThread t1 = new MyThread("窗口1");t1.start();MyThread t2 = new MyThread("窗口2");t2.start();MyThread t3 = new MyThread("窗口3");t3.start();}
}
class MyThread extends Thread{private static int ticket = 100;//总票数private static Object obj = new Object();//锁对象必须要求是唯一的public MyThread() {}public MyThread(String name) {super(name);}//如果父类(接口)中定义的方法声明上没有 throws//重写的方法就不能使用throws来处理异常,不能使用try…catch@Overridepublic void run() {//线程开启后执行的任务,卖票//不断的卖票,死循环while (true){synchronized (obj) {if (ticket <= 0){break;}else {ticket--;System.out.println(getName() + "开始卖票了,还剩:【" + ticket + "】张票");}}try {Thread.sleep(100);//模拟出票延迟时间} catch (InterruptedException e) {e.printStackTrace();}}}
}
锁对象唯一 private static Object obj = new Object ( ) ;
5.2 同步方法
synchronized修饰普通方法:锁对象是this public synchronized void method(){// 操作共享数据的代码
}
synchronized修饰静态方法:锁对象是类名.class public static synchronized void method(){// 操作共享数据的代码
}
public class Demo2 { public static void main ( String [ ] args) { MyRunnable m = new MyRunnable ( ) ; Thread t1 = new Thread ( m, "12306官网" ) ; t1. start ( ) ; Thread t2 = new Thread ( m, "携程官网" ) ; t2. start ( ) ; Thread t3 = new Thread ( m, "飞猪官网" ) ; t3. start ( ) ; }
} class MyRunnable implements Runnable { private int ticket = 100 ;
@Override public void run ( ) { while ( true ) { boolean b = getTicketResult ( ) ; if ( b) { break ; } } } public synchronized boolean getTicketResult ( ) { Thread thread = Thread . currentThread ( ) ; if ( ticket == 0 ) { return true ; } else { try { Thread . sleep ( 10 ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } ticket-- ;
System . out. println ( thread. getName ( ) + "开始卖票了,还剩" + ticket + "张票" ) ; return false ; } }
}
5.3 Lock
JDK5提供的锁对象,显示的加锁和释放锁 Lock为接口 ReentrantLock为具体的实现类 格式 Lock lock = new ReentrantLock();
lock.lock(); // 显式加锁
// 操作共享数据的代码
lock.unlock(); // 显式释放锁
注意: ReentrantLock对象要唯一 unlock() 要放到finally代码块中保证一定能释放锁 public class Demo3 {public static void main(String[] args) {MyThread3 t1 = new MyThread3("携程官网");t1.start();MyThread3 t2 = new MyThread3("同程官网");t2.start();MyThread3 t3 = new MyThread3("飞猪官网");t3.start();}
}class MyThread3 extends Thread{private static int ticket = 100;//总票数static Lock lock = new ReentrantLock();//static保证lock锁的唯一性private int count = 0;//各个窗口售票张数public MyThread3() {}public MyThread3(String name) {super(name);}//如果父类(接口)中定义的方法声明上没有 throws//重写的方法就不能使用throws来处理异常,不能使用try…catch@Overridepublic void run() {//线程开启后执行的任务,卖票//不断的卖票,死循环while (true){try {lock.lock();// 显示加锁,获得锁if (ticket == 0){break;}else {try {Thread.sleep(10);//模拟出票延迟时间} catch (InterruptedException e) {e.printStackTrace();}ticket--;count++;System.out.println(getName() + "已经卖了" + count + "张票,还剩:【" + ticket + "】张票");}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock(); // 显示的释放锁}}}
}
5.4 线程安全问题小结
6. 线程间通讯
6.1 线程间通讯
当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源竞争 线程通讯的方法(等待,唤醒) Object中的方法 说明 void wait() 让当前线程等待并释放所占锁,直到其他线程调用notify()方法或者notifyAll()方法 void notify() 唤醒正在等待的单个线程 void notifyAll() 唤醒正在等待的所有线程
public class Demo1 {private static Object obj = new Object();public static void main(String[] args) {Thread t1 = new Thread(() ->{synchronized (obj) {Thread thread = Thread.currentThread();//获取当前线程对象System.out.println(thread.getName() + "开始执行了");try {obj.wait();//当前线程在这等待System.out.println(thread.getName() + "结束了");} catch (InterruptedException e) {e.printStackTrace();}}});t1.setName("线程1");t1.start();Thread t3 = new Thread(() ->{synchronized (obj) {Thread thread = Thread.currentThread();//获取当前线程对象System.out.println(thread.getName() + "开始执行了");try {obj.wait();//当前线程在这等待System.out.println(thread.getName() + "结束了");} catch (InterruptedException e) {e.printStackTrace();}}});t3.setName("线程3");t3.start();//try {Thread.sleep(1000);//睡眠确保线程2在线程1和3启动后才行,防止有没有启动导致无法唤醒睡眠} catch (InterruptedException e) {e.printStackTrace();}Thread t2 = new Thread(() ->{synchronized (obj) {Thread thread = Thread.currentThread();//获取当前线程对象System.out.println(thread.getName() + "开始执行了");
// obj.notify();//唤醒任意等待的一个线程obj.notifyAll();//唤醒所有线程System.out.println(thread.getName() + "结束了");}});t2.setName("线程2");t2.start();}
}
6.2 线程通讯的常见模型
生产者消费模型 生产者线程负责生产数据 消费者线程负责消费生产者产生的数据 案例 厨师线程(生产者)不断的做包子: 吃货线程(消费者)不断的吃包子 桌子上放有盘子(容器) public class Desk { List < String > list = new ArrayList < > ( ) ; public synchronized void put ( ) { if ( list. size ( ) == 10 ) { this . notifyAll ( ) ; try { this . wait ( ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } } else { Thread thread = Thread . currentThread ( ) ; list. add ( "包子" ) ; System . out. println ( thread. getName ( ) + "做了一个包子,目前包子数为:" + list. size ( ) ) ; } } public synchronized void gut ( ) { if ( list. size ( ) == 0 ) { this . notifyAll ( ) ; try { this . wait ( ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } } else { Thread thread = Thread . currentThread ( ) ; list. remove ( 0 ) ; System . out. println ( thread. getName ( ) + "吃了一个包子,目前包子数为:" + list. size ( ) ) ; } }
}
public class Demo1 {public static void main(String[] args) {//0.创建桌子的对象Desk desk = new Desk();//1.创建厨师线程Thread chef1 = new Thread(() -> {while (true){desk.put();try {Thread.sleep(400);} catch (InterruptedException e) {e.printStackTrace();}}});chef1.setName("厨师1");chef1.start();Thread chef2 = new Thread(() -> {while (true){desk.put();try {Thread.sleep(700);} catch (InterruptedException e) {e.printStackTrace();}}});chef2.setName("厨师2");chef2.start();Thread chef3 = new Thread(() -> {while (true){desk.put();try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}}});chef3.setName("厨师3");chef3.start();//2.创建吃货线程Thread foodie1 = new Thread(() -> {while (true){desk.gut();try {Thread.sleep(600);} catch (InterruptedException e) {e.printStackTrace();}}});foodie1.setName("吃货1");foodie1.start();Thread foodie2 = new Thread(() -> {while (true){desk.gut();try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}});foodie2.setName("吃货2");foodie2.start();}
}
7. 线程的生命周期
线程的六种状态和相互转换
Thread.State 内部枚举类
状态枚举项 状态说明 阶段 NEW 新建 线程刚被创建,但是并未启动。 RUNNABLE 可运行 线程已经调用了start() BLOCKED 阻塞 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态; WAITING 无限等待 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 TIMED_WAITING 计时等待 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态 TERMINATED 结束 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
线程对象调用getState()方法可以获取线程的状态 public class Demo { public static void main ( String [ ] args) throws InterruptedException { Thread t1 = new Thread ( ( ) -> { System . out. println ( "线程创建了" ) ; try { Thread . sleep ( 1000 ) ; } catch ( InterruptedException e) { e. printStackTrace ( ) ; } } ) ; System . out. println ( t1. getState ( ) ) ; t1. start ( ) ; System . out. println ( t1. getState ( ) ) ; Thread . sleep ( 500 ) ; System . out. println ( t1. getState ( ) ) ; }
}
8. 总结
这篇关于【学习日记2023.4.12】之Thread的实现方式和解决线程安全问题的三种办法以及线程通讯问题和Thread的生命周期的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!