13 - Java多线程
多线程详解
Java.Thread
1. 线程简介
任务、进程、线程、多线程
程序:是指令和数据的有序集合,其本身没有任何运行的意义,是一个静态的概念。
进程;是执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位。
线程:通常一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。
注:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换得很快,所以就有同时执行的错觉。
核心概念:
-
线程就是独立的执行路径;
-
在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程;
-
main()称之为主线程,为系统的入口,用于执行整个程序;
-
在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,线程执行先后顺序是不能人为干预的。
-
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
-
线程会带来额外的开销,如CPU调度时间,并发控制开销;
-
每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
2. 线程实现(重点)
线程创建
三种创建方式:
①Thread class:继承Thread类(重点)
②Runnable接口:实现Runnable接口(重点)
③Callable接口:实现Callable接口(了解)
方式一:继承Thread类
步骤:①自定义线程类继承Thread类;②重写run()方法,编写线程执行体;③创建线程对象,调用start()方法启动线程(不能调用run()方法)。
package com.wang.multithreading; //创建线程方式一:继承Thread类;重写run()方法;调用start()方法 public class TestThread1 extends Thread{ //run方法线程 @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("正在执行run方法线程-->" + i); } } //main线程,主线程 public static void main(String[] args) { //创建一个线程对象 TestThread1 testThread1 = new TestThread1(); //调用start()方法开启线程 testThread1.start(); for (int i = 0; i < 20; i++) { System.out.println("正在执行main线程-->" + i); } } } //执行结果:主线程和run方法线程交替进行 正在执行main线程-->0 正在执行main线程-->1 正在执行run方法线程-->0 正在执行main线程-->2 正在执行run方法线程-->1 正在执行main线程-->3 正在执行run方法线程-->2 正在执行run方法线程-->3 正在执行run方法线程-->4 正在执行run方法线程-->5 正在执行run方法线程-->6 正在执行run方法线程-->7 正在执行run方法线程-->8 正在执行run方法线程-->9 正在执行run方法线程-->10 正在执行run方法线程-->11 正在执行run方法线程-->12 正在执行run方法线程-->13 正在执行run方法线程-->14 正在执行run方法线程-->15 正在执行run方法线程-->16 正在执行run方法线程-->17 正在执行run方法线程-->18 正在执行run方法线程-->19 正在执行main线程-->4 正在执行main线程-->5 正在执行main线程-->6 正在执行main线程-->7 正在执行main线程-->8 正在执行main线程-->9 正在执行main线程-->10 正在执行main线程-->11 正在执行main线程-->12 正在执行main线程-->13 正在执行main线程-->14 正在执行main线程-->15 正在执行main线程-->16 正在执行main线程-->17 正在执行main线程-->18 正在执行main线程-->19
总结:注意!线程开启不一定立即执行,由CPU调度执行。
案例一:多线程图片下载
package com.wang.multithreading; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; //练习Thread,实现多线程同步下载图片 public class TestThread2 extends Thread{ private String url; //网络图片地址 private String name; //保存的文件名 public TestThread2(String url, String name) { this.url = url; this.name = name; } //下载图片线程的执行体 @Override public void run() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载了文件名为:" + name); } public static void main(String[] args) { TestThread2 t1 = new TestThread2("http://www.runoob.com/wp-content/uploads/2013/12/java.jpg","1.jpg"); TestThread2 t2 = new TestThread2("https://img.mianfeiwendang.com/pic/cf2c073be88daca65df15101/1-396-png_6_0_0_166_567_698_445_893.25_1263.375-646-0-0-646.jpg", "2.jpg"); TestThread2 t3 = new TestThread2("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2586163933,2526500780&fm=26&gp=0.jpg", "3.jpg"); t1.start(); t2.start(); t3.start(); } } //下载器 class WebDownloader{ //下载方法 public void downloader(String url, String name){ try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println("IO异常,downloader方法出现问题!"); } } } //执行结果:CPU调度线程,图片下载顺序每次执行可能会不一样。 下载了文件名为:1.jpg 下载了文件名为:3.jpg 下载了文件名为:2.jpg
方式二:实现Runnable
步骤:①定义MyRunnable类实现Runnable接口;②实现run()方法,编写线程执行体;③创建线程对象,调用start()方法启动线程。
package com.wang.multithreading; //创建线程方式二:实现Runnable接口;重写run()方法;执行线程需要丢入Runnable接口实现类,调用start()方法 public class TestThread3 implements Runnable{ @Override public void run() { //run方法线程 for (int i = 0; i < 20; i++) { System.out.println("正在执行run方法线程-->" + i); } } public static void main(String[] args) { //创建Runnable接口的实现类对象 TestThread3 testThread3 = new TestThread3(); //创建线程对象,通过线程对象来开启线程(代理) // Thread thread = new Thread(testThread3); // thread.start(); new Thread(testThread3).start(); for (int i = 0; i < 20; i++) { System.out.println("正在执行main线程-->" + i); } } } //执行结果:主线程和run方法线程交替进行 正在执行main线程-->0 正在执行run方法线程-->0 正在执行main线程-->1 正在执行run方法线程-->1 正在执行main线程-->2 正在执行run方法线程-->2 正在执行main线程-->3 正在执行run方法线程-->3 正在执行main线程-->4 正在执行run方法线程-->4 正在执行main线程-->5 正在执行run方法线程-->5 正在执行main线程-->6 正在执行run方法线程-->6 正在执行main线程-->7 正在执行run方法线程-->7 正在执行main线程-->8 正在执行run方法线程-->8 正在执行main线程-->9 正在执行run方法线程-->9 正在执行main线程-->10 正在执行run方法线程-->10 正在执行main线程-->11 正在执行run方法线程-->11 正在执行main线程-->12 正在执行run方法线程-->12 正在执行main线程-->13 正在执行run方法线程-->13 正在执行run方法线程-->14 正在执行main线程-->14 正在执行run方法线程-->15 正在执行main线程-->15 正在执行run方法线程-->16 正在执行main线程-->16 正在执行run方法线程-->17 正在执行main线程-->17 正在执行run方法线程-->18 正在执行main线程-->18 正在执行run方法线程-->19 正在执行main线程-->19
小结:
-
继承Thread类:
子类继承Thread类具备多线程能力;
启动线程:子类对象.start();
不建议使用:避免OOP单继承局限性。
-
实现Runnable接口:
实现接口Runnable具备多线程能力;
启动线程:传入目标对象+Thread对象.start();
推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。
//一份资源 StartThread station = new StartThread(); //多个代理 new Thread(station, "小明").start(); new Thread(station, "小红").start(); new Thread(station, "小芳").start();
案例二:抢购火车票
package com.wang.multithreading; //多个线程同时操作一个对象 //例:买火车票 //存在问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱(并发问题) public class TestThread4 implements Runnable{ //票数 private int ticketNums = 10; @Override public void run() { while (true){ if (ticketNums<=0){ break; } //模拟延时 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "-->拿到了第" + ticketNums-- + "张票"); } } public static void main(String[] args) { TestThread4 ticket = new TestThread4(); new Thread(ticket, "JungKook").start(); new Thread(ticket, "Cooky").start(); new Thread(ticket, "BTS").start(); } } //执行结果:会存在同一张票被多次购买的情况,并发问题出现 JungKook-->拿到了第10张票 Cooky-->拿到了第8张票 BTS-->拿到了第9张票 Cooky-->拿到了第5张票 JungKook-->拿到了第7张票 BTS-->拿到了第6张票 Cooky-->拿到了第4张票 JungKook-->拿到了第3张票 BTS-->拿到了第2张票 BTS-->拿到了第1张票 Cooky-->拿到了第0张票 JungKook-->拿到了第1张票
案例三:龟兔赛跑-Race
package com.wang.multithreading; //模拟龟兔赛跑 public class Race implements Runnable{ //胜利者 private static String winner; @Override public void run() { for (int i = 0; i <= 100; i++) { //模拟兔子休息 if (Thread.currentThread().getName().equals("rabbit") && i%10==0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } //判断比赛是否结束 boolean flag = gameOver(i); //如果比赛结束了,就停止程序 if (flag){ break; } System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步"); } } //判断是否完成比赛 private boolean gameOver(int steps){ //判断是否有胜利者 if (winner!=null){ return true; }else{ if (steps == 100){ winner = Thread.currentThread().getName(); System.out.println("Winner is " + winner); return true; } } return false; } public static void main(String[] args) { Race race = new Race(); new Thread(race, "rabbit").start(); new Thread(race, "turtle").start(); } }
方式三:实现Callable
步骤:①实现Callable接口,需要返回值类型;②重写call方法,需要抛出异常;③创建目标对象;④创建执行服务;⑤提交执行;⑥获取结果;⑦关闭服务。
案例四:多线程图片下载(实现Callable接口方法)
package com.wang.multithreading; import java.util.concurrent.*; // 线程创建方式三:实现Callable接口 public class TestCallable implements Callable<Boolean> { private String url; //网络图片地址 private String name; //保存的文件名 public TestCallable(String url, String name) { this.url = url; this.name = name; } // 下载图片线程的执行体 @Override public Boolean call() throws Exception { //WebDownloader类和案例一中的一样 WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载了文件名为:" + name); return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { TestCallable t1 = new TestCallable("http://www.runoob.com/wp-content/uploads/2013/12/java.jpg","1.jpg"); TestCallable t2 = new TestCallable("https://img.mianfeiwendang.com/pic/cf2c073be88daca65df15101/1-396-png_6_0_0_166_567_698_445_893.25_1263.375-646-0-0-646.jpg", "2.jpg"); TestCallable t3 = new TestCallable("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2586163933,2526500780&fm=26&gp=0.jpg", "3.jpg"); // 创建执行服务 ExecutorService ser = Executors.newFixedThreadPool(3); // 提交执行 Future<Boolean> r1 = ser.submit(t1); Future<Boolean> r2 = ser.submit(t2); Future<Boolean> r3 = ser.submit(t3); // 获取结果 boolean rs1 = r1.get(); boolean rs2 = r2.get(); boolean rs3 = r3.get(); // 关闭服务 ser.shutdown(); } } // 执行结果: 下载了文件名为:1.jpg 下载了文件名为:3.jpg 下载了文件名为:2.jpg
优势:①可以定义返回值;②可以抛出异常。
Lambda表达式
避免匿名内部类定义过多;其实质属于函数式编程的概念。
(params) -> expression[表达式] (params) -> statement[语句] (params) -> {statements}
为什么要使用lambda表达式:①避免匿名内部类定义过多;②可以让代码看起来更简洁;③去掉一些没有意义的代码,只留下核心逻辑。
理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在。
函数式接口的定义:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。如:
public interface Runnable{ public abstract void run(); }
对于函数式接口,可以通过lambda表达式来创建该接口的对象。
Lambda表达式的推导:
package com.wang.lambda; // 推到lambda表达式 public class TestLambda1 { // 3.静态内部类 static class Like2 implements ILike{ @Override public void lambda() { System.out.println("静态内部类 --> i like lambda2!"); } } public static void main(String[] args) { ILike like = new Like(); like.lambda(); Like2 like2 = new Like2(); like2.lambda(); // 4.局部内部类 class Like3 implements ILike{ @Override public void lambda() { System.out.println("局部内部类 --> i like lambda3!"); } } Like3 like3 = new Like3(); like3.lambda(); // 5.匿名内部类,没有类的名称,必须借助接口或父类 ILike like4 = new ILike() { @Override public void lambda() { System.out.println("匿名内部类 --> i like lambda4!"); } }; like4.lambda(); // 6.使用lambda简化 // 6.使用lambda简化 ILike like5 = ()->System.out.println("lambda表达式 --> i like lambda5!"); like5.lambda(); } } // 1.定义一个函数式接口 interface ILike{ void lambda(); } // 2.接口的实现类 class Like implements ILike{ @Override public void lambda() { System.out.println("外部类 --> i like lambda!"); } } // 执行结果: 外部类 --> i like lambda! 静态内部类 --> i like lambda2! 局部内部类 --> i like lambda3! 匿名内部类 --> i like lambda4! lambda表达式 --> i like lambda5!
更具体地,外部类实现:
package com.wang.lambda; public class TestLambda2 { public static void main(String[] args) { ILove love = new Love(); love.love(97); } } interface ILove{ void love(int a); } class Love implements ILove { @Override public void love(int a) { System.out.println("i love JungKook!" + a); } } // 执行结果: i love JungKook!97
静态内部类实现:
package com.wang.lambda; public class TestLambda2 { static class Love implements ILove { @Override public void love(int a) { System.out.println("i love JungKook!" + a); } } public static void main(String[] args) { ILove love = new Love(); love.love(97); } } interface ILove{ void love(int a); } // 执行结果: i love JungKook!97
局部内部类实现:
package com.wang.lambda; public class TestLambda2 { public static void main(String[] args) { class Love implements ILove { @Override public void love(int a) { System.out.println("i love JungKook!" + a); } } ILove love = new Love(); love.love(97); } } interface ILove{ void love(int a); } // 执行结果: i love JungKook!97
匿名内部类实现:
package com.wang.lambda; public class TestLambda2 { public static void main(String[] args) { ILove love = new ILove() { @Override public void love(int a) { System.out.println("i love JungKook!" + a); } }; love.love(97); } } interface ILove{ void love(int a); } //执行结果: i love JungKook!97
Lambda表达式实现及其简化:
package com.wang.lambda; public class TestLambda2 { public static void main(String[] args) { ILove love = null; // lambda表达式 love = (int a)-> { System.out.println("i love JungKook!" + a); }; // 简化一:参数类型简化 love = (a)-> { System.out.println("i love JungKook!" + a); }; // 简化二:括号简化 love = a-> { System.out.println("i love JungKook!" + a); }; // 简化三:去掉花括号 love = a-> System.out.println("i love JungKook!" + a); love.love(97); } } interface ILove{ void love(int a); } // 执行结果: i love JungKook!97
lambda表达式总结:
①lambda表达式在只有一行代码的情况下才能简化为一行,如果有多行,那么就用代码块(花括号)包裹。
②前提是接口为函数式接口;
③多个参数也可以去掉参数类型,但是要去掉的话都要去掉,且必须加上括号。
静态代理
案例五:婚庆公司
package com.wang.multithreading; public class StaticProxy { public static void main(String[] args) { You you = new You(); // 真实对象 WeddingCompany weddingCompany = new WeddingCompany(you); weddingCompany.HappyMarry(); } } interface Marry{ void HappyMarry(); } // 真实角色 class You implements Marry{ @Override public void HappyMarry() { System.out.println("当事人结婚!"); } } // 代理角色 class WeddingCompany implements Marry{ // 代理谁--> 真实目标角色 private Marry target; public WeddingCompany(Marry target) { this.target = target; } @Override public void HappyMarry() { before(); this.target.HappyMarry(); // 真实对象 after(); } private void before() { System.out.println("结婚前"); } private void after() { System.out.println("结婚后"); } } // 执行结果: 结婚前 当事人结婚! 结婚后
静态代理模式总结:
①真实对象和代理对象都要实现同一个接口;
②代理对象要代理真实角色。
好处:①代理对象可以做很多真实对象做不了的事情;②真实对象专注做自己的事情。
3. 线程状态
五大状态:创建状态、就绪状态、阻塞状态、运行状态、死亡状态。
线程方法:
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程(不要使用这个方式) |
boolean isAlive() | 测试线程是否处于活动状态 |
停止线程
不推荐使用JDK提供的stop()、destroy()方法(已废弃);推荐线程自己停止下来;建议使用一个标志位作为终止变量,当flag = false时则终止线程运行。
package com.wang.threadstates; // 测试stop // 1.建议线程正常终止:利用循环次数,不建议死循环; // 2.建议使用标志位:设置一个标志位 // 3.不要使用stop或destroy等过时或者JDK不建议使用的方法 public class TestStop implements Runnable{ // 1.设置一个标志位 private boolean flag = true; @Override public void run() { int i = 0; while (flag){ System.out.println("Thread is running... -->" + i++); } } // 2.设置一个公开的方法终止线程,转换标志位 public void stop(){ this.flag = false; } public static void main(String[] args) { TestStop testStop = new TestStop(); new Thread(testStop).start(); for (int i = 0; i < 1000; i++) { System.out.println("main function" + i); if (i==900){ // 调用stop方法切换标志位,终止线程 testStop.stop(); System.out.println("The thread was terminated!"); } } } }
线程休眠(sleep)
sleep(时间)指定当前线程阻塞的毫秒数;sleep存在异常InterruptedException;sleep时间达到后线程进入就绪状态;sleep可以模拟网络延迟(可以参考案例二:抢购火车票,模拟网络延时以放大问题的发生性),倒计时等;每一个对象都有一个锁,sleep不会释放锁。
案例六:倒计时&打印系统当前时间
package com.wang.threadstates; import java.text.SimpleDateFormat; import java.util.Date; // 模拟倒计时 public class TestSleep { public static void main(String[] args) { // 倒计时 try { tenDown(); } catch (InterruptedException e) { e.printStackTrace(); } // 打印当前时间 try { printCurrentTime(); } catch (InterruptedException e) { e.printStackTrace(); } } // 模拟10秒倒计时 public static void tenDown() throws InterruptedException { int count = 10; while (true){ Thread.sleep(1000); System.out.println(count--); if (count<=0){ break; } } } // 打印系统当前时间 public static void printCurrentTime() throws InterruptedException { int count = 10; Date currentTime = new Date(System.currentTimeMillis()); // 获取系统当前时间 while (true){ Thread.sleep(1000); System.out.println(new SimpleDateFormat("HH:mm:ss").format(currentTime)); currentTime = new Date(System.currentTimeMillis()); // 更新当前时间 if (--count<=0){ break; } } } }
线程礼让(yield)
礼让线程,让当前正在执行的线程暂停,但不阻塞;将线程从运行状态转为就绪状态;让CPU重新调度,礼让不一定成功(看CPU)!
package com.wang.threadstates; // 测试线程礼让 // 礼让不一定成功,取决于CPU的调度 public class TestYield { public static void main(String[] args) { MyYield myYield = new MyYield(); new Thread(myYield, "NO.1").start(); new Thread(myYield, "NO.2").start(); } } class MyYield implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + "线程开始运行"); Thread.yield(); // 礼让 System.out.println(Thread.currentThread().getName() + "线程停止运行"); } } // 执行结果: // 礼让成功: NO.1线程开始运行 NO.2线程开始运行 NO.2线程停止运行 NO.1线程停止运行 // 礼让失败: NO.1线程开始运行 NO.1线程停止运行 NO.2线程开始运行 NO.2线程停止运行
Join
Join合并线程,待此线程执行完毕后,再执行其他线程,其他线程阻塞(可以想象为插队)。
package com.wang.threadstates; // 测试Join方法 public class TestJoin implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("VIP线程" + i); } } public static void main(String[] args) throws InterruptedException { // 开启子线程 TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); for (int i = 0; i < 100; i++) { if (i==50){ thread.join(); // 子线程插队 } System.out.println("主线程" + i); } } } // 执行结果:当主线程中i等于50时,一定会有子线程插队,其余时间两个线程可能存在交替执行的情况。
观测线程状态
线程状态可能处于以下状态之一(查看JDK帮助文档):
-
NEW:尚未启动的线程处于此状态;
-
RUNNABLE:在Java虚拟机中执行的线程处于此状态;
-
BLOCKED:被阻塞等待监视器锁定的线程处于此状态;
-
WAITING:正在等待另一个线程执行特定动作的线程处于此状态;
-
TIMED_WAITING:正在等待另一个线程执行动作达到指定时间的线程处于此状态;
-
TERMINATED:已退出的线程处于此状态。
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
package com.wang.threadstates; // 观察测试线程状态 public class TestState { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()-> { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("loop" + i); } }); // 观察状态,启动前 Thread.State state = thread.getState(); System.out.println(state); // NEW //启动后 thread.start(); state = thread.getState(); System.out.println(state); // RUNNABLE int count = 0; while (state != Thread.State.TERMINATED){ Thread.sleep(1000); System.out.println(count++ + "..."); state = thread.getState(); System.out.println(state); } } } // 执行结果(每次执行可能会不同): NEW RUNNABLE loop0 0... TIMED_WAITING loop1 1... TIMED_WAITING 2... BLOCKED loop2 3... TERMINATED
线程的优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程的优先级用数字表示,范围从1~10。
-
Thread.MIN_PRIORITY = 1;
-
Thread.MAX_PRIORITY = 10;
-
Thread.NORM_PRIORITY = 5.
使用getPriority()获取优先级;使用setPriority(int a)改变优先级。优先级的设定建议在start()调用前。
优先级低只意味着获取调度的概率低,并不是优先级低就不会被调度了,这取决于CPU的调度。
package com.wang.threadstates; // 测试线程的优先级 public class TestPriority { public static void main(String[] args) { // 主线程的默认优先级 System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority()); MyPriority myPriority = new MyPriority(); Thread t1 = new Thread(myPriority); Thread t2 = new Thread(myPriority); Thread t3 = new Thread(myPriority); Thread t4 = new Thread(myPriority); Thread t5 = new Thread(myPriority); Thread t6 = new Thread(myPriority); // 先设置优先级,再启动 t2.setPriority(8); t3.setPriority(6); t4.setPriority(9); t5.setPriority(Thread.MIN_PRIORITY); t6.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); t6.start(); } } class MyPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " --> " + Thread.currentThread().getPriority()); } } // 执行结果: main --> 5 Thread-1 --> 8 Thread-3 --> 9 Thread-0 --> 5 Thread-2 --> 6 Thread-5 --> 10 Thread-4 --> 1
守护线程
线程分为用户线程和守护线程;虚拟机必须确保用户线程执行完毕;虚拟机不用等待守护线程执行完毕。
如,后台记录操作日志、监控内存、垃圾回收等。
package com.wang.threadstates; // 测试守护线程 public class TestDaemon { public static void main(String[] args) { MyDaemon myDaemon = new MyDaemon(); MyUser myUser = new MyUser(); Thread thread = new Thread(myDaemon); thread.setDaemon(true); // 默认为false,即用户线程 thread.start(); // 守护线程启动 new Thread(myUser).start(); // 用户线程启动 } } class MyDaemon implements Runnable{ @Override public void run() { System.out.println("守护线程运行中..."); } } class MyUser implements Runnable{ @Override public void run() { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("用户线程运行中..."); } System.out.println("用户线程结束!"); } } // 执行结果: 守护线程运行中... 用户线程运行中... 用户线程运行中... 用户线程运行中... 用户线程运行中... 用户线程运行中... 用户线程结束!
4. 线程同步(重点)
多个线程操作同一个资源。
并发:用一个对象被多个线程同时操作。如上万人同时抢100张票,两个银行同时取钱。
线程同步:处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
形成条件:队列和锁(保证安全)
由于同一进程的多个线程共享同一块储存空间,在带来方便的同时也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁时,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:
-
一个线程持有锁会导致其他所有需要此锁的线程挂起;
-
在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和延时调度,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
三大不安全案例
①参考案例二:抢购火车票
②案例七:银行的不安全取钱
package com.wang.threadstates; // 不安全的取钱:两个人同时去银行取钱 public class UnsafeBank { public static void main(String[] args) { // 账户 Account account = new Account(100, "BTS"); Drawing jk = new Drawing(account, 50, "Jungkook"); Drawing cooky = new Drawing(account, 100, "Cooky"); jk.start(); cooky.start(); } } // 银行账户 class Account{ int money; // 余额 String name; // 账户名 public Account(int money, String name) { this.money = money; this.name = name; } } // 银行,模拟取款 class Drawing extends Thread{ Account account; // 账户 int drawingMoney; // 要取的金额 int nowMoney; // 手头上的金额 public Drawing(Account account, int drawingMoney, String name){ super(name); this.account = account; this.drawingMoney = drawingMoney; } // 取款 @Override public void run() { // 判断账户余额是否足够 if (account.money<drawingMoney){ System.out.println(Thread.currentThread().getName() + "账户余额不够!"); return; } // 模拟延时,sleep可以放大问题的发生性 try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } account.money = account.money - drawingMoney; // 更新账户余额 nowMoney = nowMoney + drawingMoney; // 更新手头上的金额 System.out.println(account.name + "账户余额为:" + account.money); // this.getName()与Thread.currentThread().getName()等价 System.out.println(this.getName() + "手头上的金额有:" + nowMoney); } } // 执行结果: BTS账户余额为:-50 Jungkook手头上的金额有:50 BTS账户余额为:-50 Cooky手头上的金额有:100
④线程不安全的集合
package com.wang.threadstates; import java.util.ArrayList; import java.util.List; // 线程不安全的集合 public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(()-> { list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } } // 执行结果(不是10000): 9996
同步方法及同步块
由于可以通过private关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种方法:synchronized方法和synchronized块。
同步方法
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面阻塞的线程才能获得这个锁,继续执行。
public synchronized void method(int args){}
缺陷:若将一个大的方法声明为synchronized将会影响效率。
案例二的安全方法(可对照案例二代码):在run()方法前加synchronized关键字即可。
synchronized同步方法,锁的时this对象!!!对于案例七的取款问题,如果对run()方法加synchronized关键字,锁的是Drawing,达不到预期的目的,需要锁Account才能实现同步,因此需要用到同步块。
同步块
synchronized(obj){}
obj称之为同步监视器。obj可以是任何对象,但是推荐使用共享资源作为同步监视器;同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class[反射中讲解]。
同步监视器的执行过程:
①第一个线程访问,锁定同步监视器,执行其中的代码;
②第二个线程访问,发现同步监视器被锁定,无法访问;
③第一个线程访问完毕,解锁同步监视器;
④第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
案例七的安全方法(可对照案例七代码),将run()方法改为如下:
// 取款 @Override public void run() { synchronized (account){ // 判断账户余额是否足够 if (account.money<drawingMoney){ System.out.println(Thread.currentThread().getName() + "账户余额不够!"); return; } // 模拟延时,sleep可以放大问题的发生性 try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } account.money = account.money - drawingMoney; // 更新账户余额 nowMoney = nowMoney + drawingMoney; // 更新手头上的金额 System.out.println(account.name + "账户余额为:" + account.money); // this.getName()与Thread.currentThread().getName()等价 System.out.println(this.getName() + "手头上的金额有:" + nowMoney); } } // 执行结果: BTS账户余额为:50 Jungkook手头上的金额有:50 Cooky账户余额不够!
同样的,对于线程不安全的集合,也可以将list对象作为同步监视器,再对其进行增加元素的操作。
CopyOnWriteArrayList(扩充)
属于并发编程领域。
package com.wang.threadstates; import java.util.concurrent.CopyOnWriteArrayList; // 测试JUC安全类型的集合 public class TestJUC { public static void main(String[] args) { CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); for (int i = 0; i < 10000; i++){ new Thread(()-> { list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } } // 执行结果: 10000
死锁
多个线程各自占有一些资源,并且相互等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情况。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
package com.wang.threadstates; // 死锁:多个线程互相抱着对方所需要的资源,然后形成僵持 public class DeadLock { public static void main(String[] args) { Makeup jungKook = new Makeup(0, "JungKook"); Makeup cooky = new Makeup(1, "Cooky"); jungKook.start(); cooky.start(); } } // 口红 class Lipstick{ } // 镜子 class Mirror{ } class Makeup extends Thread{ // 需要的资源只有一份,使用static修饰词来保证只有一份 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choice; // 选择 String name; // 选择的人 public Makeup(int choice, String name) { this.choice = choice; this.name = name; } @Override public void run() { // 化妆 try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } // 化妆,互相持有对方的锁,即需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice == 0){ synchronized (lipstick){ // 获得口红的锁 System.out.println(this.name + "获得口红的锁"); Thread.sleep(1000); synchronized (mirror){ // 一秒钟后想获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); } } }else{ synchronized (mirror){ // 获得镜子的锁 System.out.println(this.name + "获得镜子的锁"); Thread.sleep(2000); synchronized (lipstick){ // 两秒钟后想获得口红的锁 System.out.println(this.name + "获得口红的锁"); } } } } } // 执行结果(出现死锁情况): JungKook获得口红的锁 Cooky获得镜子的锁 Process finished with exit code -1 // 将嵌套在内的synchronized方法块移出后,死锁解除。执行结果: Cooky获得镜子的锁 JungKook获得口红的锁 Cooky获得口红的锁 JungKook获得镜子的锁 Process finished with exit code 0
产生死锁的四个必要条件:
①互斥条件:一个资源每次只能被一个进程使用。
②请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
③不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
④循环等待条件:若干进程之间形成一种首尾相接的循环等待资源关系。
死锁避免方法:上面列出的死锁的四个必要条件,只要突破其中任意一个或多个条件即可避免死锁发生。
Lock锁
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。基本格式如下:
class A{ private final ReentrantLock lock = new ReentrantLock(); public void m(){ lock.lock(); // 加锁 try{ // 需要保证线程安全的代码 }finally{ lock.unlock(); // 如果同步代码有异常,要将unlock()写入finally语句块 } } }
在案例二抢购火车票中,多个线程操作同一个资源会不安全。此处使用lock锁实现同步解决其安全问题:
package com.wang.threadstates; import java.util.concurrent.locks.ReentrantLock; // 测试lock锁 public class TestLock { public static void main(String[] args) { MyLock myLock = new MyLock(); new Thread(myLock, "first").start(); new Thread(myLock, "second").start(); new Thread(myLock, "third").start(); } } class MyLock implements Runnable{ int ticketNums = 10; // 定义Lock锁 private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true){ try { lock.lock(); // 加锁 if (ticketNums > 0){ System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--); }else{ break; } } finally { lock.unlock(); // 释放锁 } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 执行结果: first-->10 second-->9 third-->8 third-->7 first-->6 second-->5 third-->4 second-->3 first-->2 third-->1
synchronized和lock的对比:
-
Lock锁是显式锁(手动开启和关闭锁!);synchronized是隐式锁,出了作用域自动释放。
-
Lock只有代码块锁;synchronized有代码块锁和方法锁。
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能较好,并且具有更好的扩展性(提供更多的子类)。
优先使用顺序:Lock锁 > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
5. 线程通信问题
生产者消费者问题
应用场景:
-
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
-
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
-
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
分析:
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
-
对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品后,又需要马上通知消费者消费;
-
对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费;
-
在生产者消费者问题中,仅有synchronized是不够的:synchronized可以阻止并发更新同一个资源,实现了同步;synchronized不能用来实现不同线程之间的消息传递(通信)。
Java提供了几个方法解决线程之间的通信问题:
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wiat()方法的线程,优先级高的线程优先调度 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException。
解决:
方式一:并发协作模型"生产者/消费者模式" --> 管程法
-
生产者:负责输出数据的模块(可能是方法,对象,线程,进程);
-
消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
-
缓冲区:消费者不能直接使用生产者的数据,它们之间有个”缓冲区“。
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。
package com.wang.threadstates; // 测试:生产者/消费者模式 --> 利用缓冲区解决:管程法 // 生产者,消费者,产品,缓冲区 public class TestPC { public static void main(String[] args) { SynBuffer buffer = new SynBuffer(); new Producer(buffer).start(); new Consumer(buffer).start(); } } class Producer extends Thread{ SynBuffer buffer; public Producer(SynBuffer buffer){ this.buffer = buffer; } // 生产 @Override public void run() { for (int i = 0; i < 100; i++) { buffer.push(new Product(i)); System.out.println("生产了第" + i + "个产品,缓冲区还有" + buffer.count + "个产品"); } } } class Consumer extends Thread{ SynBuffer buffer; public Consumer(SynBuffer buffer){ this.buffer = buffer; } // 消费 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("消费了第" + buffer.pop().id + "个产品,缓冲区还有" + buffer.count + "个产品"); } } } class Product{ int id; // 产品编号 public Product(int id) { this.id = id; } } class SynBuffer{ // 缓冲容器 Product[] products = new Product[10]; // 容器计数器 int count = 0; // 生产者生产产品 public synchronized void push(Product product){ // 如果缓冲区满了,等待消费者消费 if (count == products.length){ // 通知消费者消费,生产者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } this.notifyAll(); } // 如果缓冲区未满,继续生产产品 products[count++] = product; // 通知消费者可以开始消费 this.notifyAll(); } // 消费者消费产品 public synchronized Product pop(){ // 如果缓冲区为空,等待生产者生产 if (count == 0){ // 通知生产者生产,消费者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } this.notifyAll(); } // 如果缓冲区不为空,继续消费产品 Product product = products[--count]; // 通知生产者可以开始生产 this.notifyAll(); return product; } }
方式二:并发协作模型”生产者/消费者模式“ --> 信号灯法
package com.wang.threadstates; // 测试:生产者消费者模式:信号灯法,标志位解决 public class TestPC2 { public static void main(String[] args) { Item item = new Item(); new P(item).start(); new C(item).start(); } } // 生产者 class P extends Thread{ Item item; public P(Item item){ this.item = item; } @Override public void run() { for (int i = 0; i < 20; i++) { if (i%2 == 0){ this.item.produce("奶茶"); }else{ this.item.produce("蛋糕"); } } } } // 消费者 class C extends Thread{ Item item; public C(Item item){ this.item = item; } @Override public void run() { for (int i = 0; i < 20; i++) { item.consume(); } } } // 产品 class Item{ // 生产者生产,消费者等待;消费者消费,生产者等待 String name; // 产品名称 boolean flag = true; // 生产 public synchronized void produce(String name){ if (!flag){ try { this.wait(); // 生产者等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("生产者生产了:" + name); // 通知消费者消费 this.notifyAll(); this.name = name; this.flag = !this.flag; } // 消费 public synchronized void consume(){ if (flag){ try { this.wait(); // 消费者等待 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("消费者消费了:" + name); // 通知生产者生产 this.notifyAll(); this.flag = !this.flag; } }
6. 高级主题
线程池
背景:经常创建和销毁大量的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
-
提高响应速度(减少了创建新线程的时间);
-
减低资源消耗(重复利用线程池中的线程,不需要每次都创建);
-
便于线程管理(corePoolSize:核心池的大小;maximumPoolSize:最大线程数;keepAliveTime:线程没有任务时最多保持多长时间后会终止)。
JDK 5.0起提供了线程池相关API:ExecutorService和Executors。
ExecutorService:真正的线程池接口。常见的子类ThreadPoolExecutor。
-
void execute(Runnable command):执行任务/命令,没有返回值一般用来执行Runnable;
-
<T>Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable;
-
void shutdown():关闭线程池。
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
package com.wang.threadstates; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 测试线程池 public class TestThreadPool { public static void main(String[] args) { // 1.创建服务,创建线程池 ExecutorService service = Executors.newFixedThreadPool(10); // 参数:线程池大小 // 2.执行 service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); // 3.关闭线程池 service.shutdown(); } } class MyThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } // 执行结果: pool-1-thread-1 pool-1-thread-3 pool-1-thread-2
总结
①继承Thread类;
②实现Runnable接口;
③实现Callable接口。
package com.wang.threadstates; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TestConclusion { public static void main(String[] args) { new MyThread1().start(); new Thread(new MyThread2()).start(); FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3()); new Thread(futureTask).start(); try { Integer integer = futureTask.get(); System.out.println(integer); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } // 1.继承Thread类 class MyThread1 extends Thread{ @Override public void run() { System.out.println("MyThread1"); } } // 2.实现Runnable接口 class MyThread2 implements Runnable{ @Override public void run() { System.out.println("MyThread2"); } } // 3.实现Callable接口 class MyThread3 implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("MyThread3"); return 1; } } // 执行结果: MyThread1 MyThread2 MyThread3 1