本博客开始记录黑马教学的Java入门新视频,从d168开始,博客只做代码记录,方便后续快速复习,视频链接
36 多线程
36.1 多线程的创建
- 方式一:继承Thread类
- 方式二:实现Runnable接口
- 方式三:JDK 5.0新增,实现Callable接口
例:方法一
- 继承Thread类
- 重写run方法
- 创建线程对象
- 调用start()方法启动
package d1_create; /** * 目标:多线程的创建爱你方式一:继承Thread类实现 * */ public class ThreadDemo1 { public static void main(String[] args) { //3. new一个新线程对象 Thread t = new MyThread(); // 4. 调用start方法启动线程(执行的还是run方法) t.start(); for(int i = 0; i < 5; i++){ System.out.println("主线程执行输出:" + i); } } } //1. 定义一个线程类继承Thread类 class MyThread extends Thread{ // 2. 重写run方法,里面是定义线程以后要干啥 @Override public void run(){ for(int i = 0; i < 5; i++){ System.out.println("子线程执行输出:" + i); } } }
为什么调用run,而不是调用start:run会当成单线程执行
优点:代码简单
缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展
方法二:实现runnable接口
- 定义一个线程人物类MyRunnable实现接口,重写run()方法
- 创建MyRunnable对象
- 把MyRunnable任务对象交给Thread线程对象处理
- 调用线程对象的start()方法启动线程
package d1_create; /* * 目标:学会线程的创建方式二 * */ public class ThreadDemo2 { public static void main(String[] args) { // 3. 创建一个任务对象 Runnable target = new MyRunnable(); //4. 把任务对象交给Thread处理 Thread t = new Thread(target); //5. 启动线程 t.start(); for(int i = 0; i < 10; i++){ System.out.println("主线程执行输出:" + i); } } } //1. 定义一个线程任务类,实现Runnable接口 class MyRunnable implements Runnable{ // 2. 重写run方法,定义线程的执行任务 @Override public void run(){ for(int i = 0; i < 10; i++){ System.out.println("子线程执行输出:" + i); } } }
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的
package d1_create; /* * 目标:学会线程的创建方式二(匿名内部类方式实现,语法形式) * */ public class ThreadDemo2Other { public static void main(String[] args) { // 3. 创建一个任务对象 Runnable target = new Runnable(){ @Override public void run(){ for(int i = 0; i < 10; i++){ System.out.println("子线程执行输出:" + i); } } }; //4. 把任务对象交给Thread处理 Thread t = new Thread(target); //5. 启动线程 t.start(); for(int i = 0; i < 10; i++){ System.out.println("主线程执行输出:" + i); } } }
其他写法:
package d1_create; /* * 目标:学会线程的创建方式二(匿名内部类方式实现,语法形式) * */ public class ThreadDemo2Other { public static void main(String[] args) { // 3. 创建一个任务对象 Runnable target = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("子线程1执行输出:" + i); } } }; Thread t = new Thread(target); t.start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("子线程2执行输出:" + i); } } }).start(); new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("子线程3执行输出:" + i); } }).start(); for (int i = 0; i < 10; i++) { System.out.println("主线程执行输出:" + i); } } }
前两种线程创建方式都存在一个问题:
- 它们重写的run方法均不能直接返回结果
- 不适合需要返回线程执行结果的业务场景
方案三:利用Callable、FutureTask接口实现
- 得到任务对象
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务
- 线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果
package d1_create; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; /* * 目标:方式三,实现Callable接口,结合FutureTask完成 * */ public class ThreadDemo3 { public static void main(String[] args) { // 3. 创建Callable任务对象 Callable<String> call = new MyCallable(100); // 4. 把Callable任务对象 交给 FutureTask对象 // FutureTask对象的作用1:是Runnable的对象(实现了Runnable接口),可以交给Thread了 // FUtureTask对象的作用2:可以在线程执行完毕之后通过调用其get方法得到线程执行完毕的结果 FutureTask<String> f1 = new FutureTask<>(call); // 5. 交给线程处理 Thread t1 = new Thread(f1); // 6. 启动线程 t1.start(); Callable<String> call2 = new MyCallable(200); FutureTask<String> f2 = new FutureTask<>(call2); Thread t2 = new Thread(f2); t2.start(); try { //如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果 String rs1 = f1.get(); System.out.println("第一个结果:" + rs1); } catch (Exception e){ e.printStackTrace(); } try { //如果f2任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果 String rs2 = f2.get(); System.out.println("第一个结果:" + rs2); } catch (Exception e){ e.printStackTrace(); } } } //1. 定义一个人物类,实现Callable接口 应该声明线程任务执行完毕后的结果的数据类型 class MyCallable implements Callable<String>{ private int n; public MyCallable(int n) { this.n = n; } //2. 重写call方法 @Override public String call() throws Exception{ int sum = 0; for (int i = 0; i <= n; i++) { sum += i; } return "子线程的结果是:" + sum; } }
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。可以在线程执行完毕之后获取线程执行的结果。
缺点:编程复杂
36.2 Thread的常用方法
MyThread.java
package d1_create; public class MyThread extends Thread{ @Override public void run(){ for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "输出:" + i); } } }
ThreadDemo1.java
package d1_create; /** * 目标:多线程的创建爱你方式一:继承Thread类实现 * */ public class ThreadDemo1 { public static void main(String[] args) { Thread t1 = new MyThread(); //t1.setName("1号"); t1.start(); System.out.println(t1.getName()); Thread t2 = new MyThread(); t2.setName("2号"); t2.start(); System.out.println(t2.getName()); //哪个线程执行它,它就得到哪个线程对象 //主线程的名称叫main Thread m = Thread.currentThread(); System.out.println(m.getName()); for(int i = 0; i < 5; i++){ System.out.println("main线程输出:" + i); } } }
sleep()
package d1_create; public class ThreadDemo2 { public static void main(String[] args) throws Exception{ for (int i = 0; i <= 5; i++) { System.out.println("输出" + i); if(i == 3){ Thread.sleep(3000); } } } }
36.3 线程安全
线程安全问题出现的原因?
- 存在多线程并发
- 同时访问共享资源
- 存在修改共享资源
案例:取钱业务
Account.java
package d3_thread_safe; public class Account { private String cardId; private double money; //账户余额 public Account(){} public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } //小红 小明 public void drawMoney(double money){ // 0. 获取是谁来取钱,线程名就是人名 String name = Thread.currentThread().getName(); //1. 判断账户是否够钱 if(this.money >= money){ //2. 取钱 System.out.println(name + "来取钱成功,吐出: " + money); //3. 更新余额 this.money -= money; System.out.println(name + "取钱后剩余:" + this.money); } else{ //4. 余额不足 System.out.println(name + "来取钱,余额不足!"); } } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } }
DrawThread.java
package d3_thread_safe; /* * 取钱的线程类 * */ public class DrawThread extends Thread{ private Account acc; public DrawThread(Account acc, String name){ super(name); this.acc = acc; } @Override public void run(){ //小明 小红: 取钱 acc.drawMoney(1000000); } }
ThreadDemo.java
package d3_thread_safe; public class ThreadDemo { public static void main(String[] args) { //1. 定义线程类,创建一个共享的账户对象 Account acc = new Account("ICBC-111", 1000000); //2. 创建2个线程对象,代表小明和小红同时进来了 new DrawThread(acc, "小明").start(); new DrawThread(acc, "小红").start(); } }
结果出现线程安全问题:
36.4 线程同步
加锁: 把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
方式一:同步代码块。把出现线程安全问题的核心代码给上锁;每次只能一个线程进入,执行完毕后自动解锁,其他线程才可进来执行
选中代码块,Ctrl+Alt+T键
小明来取钱成功,吐出: 1000000.0 小明取钱后剩余:0.0 小红来取钱,余额不足!
锁对象的规范要求:
- 规范上,建议使用共享资源作为锁对象
- 对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
方式二:同步方法。把出现线程安全的核心方法给上锁;每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
//小红 小明 public synchronized void drawMoney(double money){ //锁加在方法上 // 0. 获取是谁来取钱,线程名就是人名 String name = Thread.currentThread().getName(); //1. 判断账户是否够钱 if(this.money >= money){ //2. 取钱 System.out.println(name + "来取钱成功,吐出: " + money); //3. 更新余额 this.money -= money; System.out.println(name + "取钱后剩余:" + this.money); } else{ //4. 余额不足 System.out.println(name + "来取钱,余额不足!"); } }
方法三:lock锁
//final修饰后:锁对象是唯一不可替换的 private final Lock lock = new ReentrantLock(); public void drawMoney(double money){ // 0. 获取是谁来取钱,线程名就是人名 String name = Thread.currentThread().getName(); lock.lock(); try { //1. 判断账户是否够钱 if(this.money >= money){ //2. 取钱 System.out.println(name + "来取钱成功,吐出: " + money); //3. 更新余额 this.money -= money; System.out.println(name + "取钱后剩余:" + this.money); } else{ //4. 余额不足 System.out.println(name + "来取钱,余额不足!"); } } finally { lock.unlock(); } }
黑马全套Java教程(九):网络编程(二)+https://developer.aliyun.com/article/1556497