Java多线程详解[狂神说Java]
文章目录
- 多线程01: 线程的生命周期
- 多线程02:创建线程:继承Thread类
- 案例:下载图片
- 多线程03: 创建线程:实现Runnable接口
- 案例:多线程操作同一资源对象
- 案例:龟兔赛跑
- 多线程04:创建线程:实现Callable接口
- 多线程05:lambda表达式
- 多线程06:线程状态
- 多线程07:线程的优先级
- 多线程08:守护线程(daemon)
- 多线程09:线程同步
- 多线程10:死锁
- 多线程11:线程协作
- 参考资料
Java支持多线程编程。线程的概念是什么呢?一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
那什么是进程呢? 进程是操作系统分配资源的最小单位,一个进程包括由操作系统分配的内存空间、一个或多个线程。注意,线程不能单独存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。注:守护线程(Daemon,发音:英 [ˈdiːmən] )
多线程能满足程序员编写高效率的程序来达到充分利用cpu的目的。
多线程是多任务的一种特别形式,但多线程使用了更小的资源开销。
多线程01: 线程的生命周期
线程是一个动态执行的过程,它有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期:
来源:菜鸟教程
线程的五大状态
新建状态
使用new
关键字和Thread
类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序start()
这个线程。就绪状态
当线程对象调用了start()
方法之后,该线程就进入了就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。运行状态
如果就绪状态的线程获取CPU的资源,就可以执行run()
方法,此时线程便处于运行状态。处于运行状态的线程有多种变化方式,它可以变为阻塞状态、就绪状态和死亡状态。阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占有的资源后,该线程就从运行状态转为阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
1 . 等待阻塞:运行状态下的线程执行wait()
方法,使线程进入到等待队列。
2 . 同步阻塞:线程在获得synchronized
同步锁失败(因为同步锁被其他线程占用)。
3 . 其他阻塞:通过调用线程的sleep()
或join()
发出I/O请求时,线程就会进入阻塞状态。当sleep()
状态超时, 或join()
等待线程终止或超时,或者I/O处理完毕,线程重新转入就绪状态。死亡状态
一个运行状态的线程 完成任务或其他终止条件发生时,该线程就切换到终止状态。
多线程02:创建线程:继承Thread类
Java提供了三种创建线程的方法:
- 通过继承
Thread
类本身。 - 通过实现
Runnable
接口。 - 通过
Callable
和Future
创建线程。
辅助帮助文档:Class Thread
来源:Java Platform SE 8
Thread
三个步骤:1.自定义线程类Thread
类;2.重写run()
方法,编写线程结构体;3.创建线程对象,调用start()
方法启动线程。
线程不一定立即执行,CPU安排调度。
package lishizheng.demo01;//创建线程方式1:继承Thread类,重写run方法,调用start开启线程
public class TestThread1 extends Thread {@Overridepublic void run() {//run方法线程体for (int i = 0; i < 20; i++) {System.out.println("我在看代码:" + i);}}public static void main(String[] args) {//main线程,主线程//创建一个线程对象TestThread1 testThread1 = new TestThread1();//调用start方法开启线程testThread1.start();for (int i = 0; i < 1000; i++) {System.out.println("我在学习多线程: " + i);}}
}
案例:下载图片
首先下载名为commons-io的jar包:Download Apache Commons IO
解压之后如下图:
复制上图中的jar包,在IDEA项目中新建文件夹lib
,然后选中它,然后Ctrl+V
,将jar包导入项目。
导入后,右击lib
文件夹,选择Add As Library
弹出如下界面:点击OK即可。
代码如下:
package lishizheng.demo01;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;}//下载图片线程的执行体@Overridepublic void run() {WebDownLoader webDownLoader = new WebDownLoader();webDownLoader.downloader(url,name);System.out.println("已下载文件名为:" + name + "的图片");}public static void main(String[] args) {//确保图片ulr是正确的TestThread2 t1 = new TestThread2("https://img-blog.csdnimg.cn/20210210194321356.png","我的博客图片01.png");TestThread2 t2 = new TestThread2("https://img-blog.csdnimg.cn/20210228094648502.png","我的博客图片02.png");TestThread2 t3 = new TestThread2("https://img-blog.csdnimg.cn/20210303104522231.png","我的博客图片03.png");t1.start(); //启动线程t2.start();t3.start();}
}//下载器
class WebDownLoader{//下载方法public void downloader(String url, String name){try {FileUtils.copyURLToFile(new URL(url), new File(name)); //调用FileUtils类中的copyURLToFile方法实现下载} catch (IOException e) {e.printStackTrace();System.out.println("IO异常:downloader方法出现问题");}}
}
/*
执行结果:
已下载文件名为:我的博客图片01.png的图片
已下载文件名为:我的博客图片02.png的图片
已下载文件名为:我的博客图片03.png的图片Process finished with exit code 0*/
多线程03: 创建线程:实现Runnable接口
实现Runnable
步骤:1.定义MyRunnable
类实现Runnable接口;2.实现run()
方法,编写线程执行体;3.创建线程对象,调用start()
方法启动线程。
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。如下:
public class TestThread3 implements Runnable{}
在创建一个实现 Runnable 接口的类之后可以在类中实例化一个线程对象:
//创建Runnable接口的实现类对象TestThread3 testThread3 = new TestThread3();//创建线程对象,通过线程对象来开启我们的线程,代理Thread thread = new Thread(testThread3);
新线程创建之后,许哟啊调用它的 start() 方法它才会运行:
thread.start();
创建对象并调用start方法可以用一句代码实现:
new Thread(testThread3).start();//等价写法
全部代码举例如下:
package lishizheng.demo01;//创建线程方式2:实现Runnable接口,重写run方法,执行线程需要丢入实现Runnable接口的实现类
public class TestThread3 implements Runnable {@Overridepublic void run() {//run方法线程体for (int i = 0; i < 20; i++) {System.out.println("我在看代码:" + 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 < 1000; i++) {System.out.println("我在学习多线程: " + i);}}
}/*
运行代码其中一部分:可以发现线程的执行顺序是由CPU来调度的!!!并不完全按照代码书写的顺序。
我在学习多线程: 0
我在看代码:0
我在学习多线程: 1
我在学习多线程: 2
我在学习多线程: 3
我在学习多线程: 4
我在学习多线程: 5
我在学习多线程: 6
我在学习多线程: 7
我在学习多线程: 8
我在学习多线程: 9
我在学习多线程: 10
我在学习多线程: 11
我在学习多线程: 12
我在学习多线程: 13
我在学习多线程: 14
我在学习多线程: 15
我在看代码:1
我在学习多线程: 16
我在看代码:2
我在学习多线程: 17
我在看代码:3
我在学习多线程: 18*/
小结:
- 继承Thread类
- 子类继承Thread类具有多线程能力
- 启动线程:子类对象.start()
- 不建议使用:避免OOP单继承局限性
- 实现Runnable接口
- 实现接口Runnable具有多线程能力
- 启动线程: 传入目标对象+Thread对象.start()
- 推荐使用: 避免单继承局限性,灵活方便,方便同一对象被多个线程使用
案例:多线程操作同一资源对象
下面的代码实现抢票的功能:共有10张火车票,三个人来抢。目的是学习并体会多线程对同一个资源对象操作的情况。
补充Thread.currentThread().getName()
获取线程的名字
package lishizheng.demo01;//多个线程同时操作同一个对象
//买火车票的例子//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread4 implements Runnable {private int ticketNums = 10;@Override//重写run方法public void run() {while (true){if(ticketNums <= 0) break;System.out.println( Thread.currentThread().getName() + "拿到了第 " + ticketNums-- +"张票");}}public static void main(String[] args) {TestThread4 testThread4 = new TestThread4();// 一个对象,三个线程 new Thread(testThread4, "小明").start();new Thread(testThread4,"老师").start();new Thread(testThread4,"黄牛").start();}
}
/*
输出结果如下:
黄牛拿到了第 10张票
老师拿到了第 8张票
小明拿到了第 9张票
老师拿到了第 6张票
黄牛拿到了第 7张票
老师拿到了第 4张票
小明拿到了第 5张票
老师拿到了第 2张票
黄牛拿到了第 3张票
小明拿到了第 1张票Process finished with exit code 0*/
案例:龟兔赛跑
模拟龟兔赛跑,两者在同一条跑道,先跑到100步者为胜利者。
用意:体会多线程竞争资源。
下面使用到下图中的两个方法:
来源:菜鸟教程
package lishizheng.demo01;public class Race implements Runnable{private static String winner;@Overridepublic void run() {for (int i = 0; i <= 100; i++) {//模拟兔子休息if(Thread.currentThread().getName().equals("兔子") && i % 30 == 0){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}//判断比赛是否结束boolean result = gameOver(i);if(result) break;System.out.println(Thread.currentThread().getName() + " 跑了 " + i + "步");}}//判断是否完成比赛private boolean gameOver(int steps){//已经存在胜利者if(winner != null){return true;}if( steps >= 100){winner = Thread.currentThread().getName();System.out.println("Winner is : " + winner);return true;}return false;}public static void main(String[] args) {//new 一个赛道Race race = new Race();//两个线程同时竞争同一个赛道new Thread(race ,"兔子").start();new Thread(race ,"乌龟").start();}
}
/*
输出结果,其中一部分:
兔子 跑了 22步
乌龟 跑了 98步
兔子 跑了 23步
兔子 跑了 24步
乌龟 跑了 99步
兔子 跑了 25步
Winner is : 乌龟Process finished with exit code 0*/
多线程04:创建线程:实现Callable接口
实现Callable接口的步骤:
- 实现
Callable
接口,需要返回值类型 - 重写
call()
方法,需要抛出异常 - 创建目标对象
- 创建执行服务:
ExecutorService ser = Executor.newFixedThreadPool(1);
- 提交执行:
Future<Boolean> result1 = ser.submit(t1);
- 获取结果:
boolean r1 = result1.get();
- 关闭服务:
ser.shutdownNow();
利用Callable修改上面下载图片案例
package lishizheng.demo02;import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;//线程创建3:实现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;}//下载图片线程的执行体@Overridepublic Boolean call() {WebDownLoader webDownLoader = new WebDownLoader();webDownLoader.downloader(url,name);System.out.println("已下载文件名为:" + name + "的图片");return true;}public static void main(String[] args) throws ExecutionException, InterruptedException {//确保图片ulr是正确的TestCallable t2 = new TestCallable("https://img-blog.csdnimg.cn/20210228094648502.png","我的博客图片02.png");TestCallable t3 = new TestCallable("https://img-blog.csdnimg.cn/20210303104522231.png","我的博客图片03.png");TestCallable t1 = new TestCallable("https://img-blog.csdnimg.cn/20210210194321356.png","我的博客图片01.png");//创建执行服务ExecutorService ser = Executors.newFixedThreadPool(3);//提交执行Future<Boolean> r1 = ser.submit(t1);Future<Boolean> r2 = ser.submit(t2);Future<Boolean> r3 = ser.submit(t3);//获取结果boolean res1 = r1.get();boolean res2 = r2.get();boolean res3 = r3.get();//关闭服务ser.shutdown();}
}//下载器
class WebDownLoader{//下载方法public void downloader(String url, String name){try {FileUtils.copyURLToFile(new URL(url), new File(name)); //调用FileUtils类中的copyURLToFile方法实现下载} catch (IOException e) {e.printStackTrace();System.out.println("IO异常:downloader方法出现问题");}}
}/*
已下载文件名为:我的博客图片01.png的图片
已下载文件名为:我的博客图片03.png的图片
已下载文件名为:我的博客图片02.png的图片Process finished with exit code 0
*/
多线程05:lambda表达式
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
语法:
(parameters) -> expression
或
(parameters) -> {statements;}
lambda表达式的重要特征:
可选类型声明
不需要声明参数类型,编译器可以统一识别参数值。
可选的参数圆括号
一个参数无需定义圆括号,但多个参数需要定义圆括号
可选的大括号
如果主体包含了一个语句,就不需要使用大括号
可选的返回关键字
如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明表达式返回了一个数值。
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
对于函数式接口我们可以使用lambda表达式来创建该接口的对象。
//1.定义一个函数式接口
interface ILike{void lambda();
}//6.用lambda简化ILike like5 = ()-> {System.out.println("I like Lambda5");};like5.lambda();/*
输出:I like Lambda5
*/
下面是推导lambda表达式的过程,一步一步化简,思路是先从用类实现接口,到静态内部类,到局部内部类,再到匿名内部类,一步步简化,到最后是lambda表达式。
package lishizheng.demo03;//推导lambda表达式
public class TestLambda {//3.静态内部类static class Like2 implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda2");}}public static void main(String[] args) {Like like = new Like();like.lambda();Like2 like2 = new Like2();like2.lambda();//4. 局部内部类class Like3 implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda3");}}Like3 like3 = new Like3();like3.lambda();//5.匿名内部类,没有类的名称,必须借助接口或者父类ILike like4 = new ILike() {@Overridepublic void lambda() {System.out.println("I like Lambda4");}};like4.lambda();//6.用lambda简化ILike like5 = ()-> {System.out.println("I like Lambda5");};like5.lambda();}
}//1.定义一个函数式接口
interface ILike{void lambda();
}//2.实现类
class Like implements ILike{@Overridepublic void lambda() {System.out.println("I like Lambda");}
}/*
输出结果:
I like Lambda
I like Lambda2
I like Lambda3
I like Lambda4
I like Lambda5Process finished with exit code 0
*/
静态代理模式
//真实对象和代理对象到实现同一个接口
//代理对象要代理真实对象
好处:代理对象可以做很多真实对象做不了的事情 ;真实对象专注于自己的事情
举例:下面通过婚庆公司代理你来组织婚礼来说明一下静态代理模式的功能。结婚之前要布置现场,然后主人结婚,结婚之后收尾款,这些方法都由婚庆公司对象来调用。
package lishizheng.demo04;//静态代理模式//真实对象和代理对象到实现同一个接口
//代理对象到代理真实对象//好处:代理对象可以做很多真实对象做不了的事情 ;真实对象专注于自己的事情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{@Overridepublic void happyMarry() {System.out.println("结婚,超开心");}
}//代理角色
class WeddingCompany implements Marry{//代理谁? 真实目标角色private Marry target;public WeddingCompany(Marry target) {this.target = target;}@Overridepublic void happyMarry() {before();this.target.happyMarry();//真实对象after();}private void after() {System.out.println("结婚之后收尾款");}private void before() {System.out.println("结婚之前布置现场");}
}
/*
输出结果:结婚之前布置现场
结婚,超开心
结婚之后收尾款Process finished with exit code 0
*/
和多线程有什么关系呢?
复习Thread调用方法的时候,原理是一样的,它本身是Runnable接口的代理。 下面通过婚庆公司和线程进行对比。
public class StaticProxy {public static void main(String[] args) {You you = new You();//Thread代理真实的Runnable接口,new Thread(() -> System.out.println("我爱你") ).start();new WeddingCompany(you).happyMarry();}
}
多线程06:线程状态
停止线程stop
- 不推荐使用JDK提供的stop方法,destroy方法
- 推荐让线程自己停止下来,建议使用标志位进行终止变量:当flag == false时,线程终止。
举例如下: 测试主线程和teststop线程的执行过程,运行期间让teststop停止。
package lishizheng.demo05;//测试stop
//1.建议线程正常停止:利用此时,不建议死循环
//2.建议使用标志位
public class TestStop implements Runnable {// 1.设置一个标志位private boolean flag = true;@Overridepublic void run() {int i = 0;while (flag){System.out.println("run...Thread: " + 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 < 30; i++) {System.out.println("main " +i);if( i == 20){//调用stop方法切换线程标志位,让线程停止testStop.stop();System.out.println("线程已停止");}}}
}/*
输出结果:
main 0
run...Thread: 0
main 1
run...Thread: 1
main 2
run...Thread: 2
main 3
run...Thread: 3
main 4
main 5
run...Thread: 4
main 6
run...Thread: 5
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
main 20
run...Thread: 6
线程已停止
main 21
main 22
main 23
main 24
main 25
main 26
main 27
main 28
main 29Process finished with exit code 0*/
线程休眠sleep
- sleep指定当前线程阻塞的毫秒数,时间到后线程进入就绪状态
- sleep存在异常
InterruptedException
,要抛出 - sleep可以模拟网络延迟,倒计时等
- 每个对象都有一个锁,sleep不会释放锁。
举例:sleep模拟网络延时,发现代码的漏洞:这里是多线程操作同一个对象,造成线程不安全。通过sleep延时可以发现程序的执行过程并不是我们预计的那样。
package lishizheng.demo05;//模拟网络延时:放大问题的可能性,容易发现问题
public class TestSleep implements Runnable{private int ticketNums = 10;@Overridepublic 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) {//线程不安全:多个线程操作同一个对象TestSleep testThread4 = new TestSleep();new Thread(testThread4, "小明").start();new Thread(testThread4,"老师").start();new Thread(testThread4,"黄牛").start();}
}/*
运行结果:
黄牛拿到了第 9张票
老师拿到了第 8张票
小明拿到了第 10张票
老师拿到了第 7张票
小明拿到了第 7张票
黄牛拿到了第 6张票
黄牛拿到了第 5张票
小明拿到了第 4张票
老师拿到了第 5张票
小明拿到了第 3张票
黄牛拿到了第 1张票
老师拿到了第 2张票Process finished with exit code 0
*/
举例
模拟倒计时和模拟获取系统时间
package lishizheng.demo05;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.SimpleTimeZone;//模拟倒计时
public class TestSleep2 {public static void main(String[] args) {// try {// timeDown();
// } catch (InterruptedException e) {//
// }//打印当前系统时间Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间while (true){try {Thread.sleep(1000);System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));startTime = new Date(System.currentTimeMillis());//更新时间} catch (InterruptedException e) {e.printStackTrace();}}}//模拟倒计时:10~1public static void timeDown() throws InterruptedException {int num = 10;while (true){Thread.sleep(1000);System.out.println(num--);if(num < 0) break;}}
}/*
输出:
12:02:16
12:02:17
12:02:18
12:02:19
12:02:20
12:02:21
12:02:22*/
线程礼让yield
线程礼让,让当前正在执行的线程暂停,但不阻塞;将线程从运行状态转为就绪状态。让CPU重新调度,礼让不一定成功,看CPU心情。
测试:
package lishizheng.demo05;//测试礼让线程
//礼让不一定成功,看CPU心情
public class TestYield {public static void main(String[] args) {MyYield myYield = new MyYield();new Thread(myYield,"a").start();new Thread(myYield,"b").start();}
}class MyYield implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"线程开始执行");Thread.yield();System.out.println(Thread.currentThread().getName()+"线程停止执行");}
}
/*a线程开始执行
b线程开始执行
a线程停止执行
b线程停止执行Process finished with exit code 0
*/
join
join相当于线程插队,执行完插队线程才能继续执行别的线程。
测试:
package lishizheng.demo05;//测试join方法
//想象为插队public class TestJoin implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; 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 < 500; i++) {if( i == 100){thread.join(); //插队,主线程等待插队线程运行结束}System.out.println("main "+i);}}
}
观察线程状态:
package lishizheng.demo05;//观察测试线程的状态
public class TestState {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() ->{for (int i = 0; i < 5; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("=============");});//观察状态Thread.State state = thread.getState();System.out.println("状态:"+ state);//观察启动后thread.start();state = thread.getState();System.out.println("启动后状态:"+state);while (state != Thread.State.TERMINATED){Thread.sleep(100);state = thread.getState();//更新线程状态System.out.println("现在的状态是:"+state);}}
}
/*
状态:NEW
启动后状态:RUNNABLE
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
现在的状态是:TIMED_WAITING
=============
现在的状态是:TERMINATEDProcess finished with exit code 0*/
多线程07:线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 ~10(Thread.MIN_PRIORITY ~Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。 优先级高的线程,先执行的概率大。
测试线程优先级
package lishizheng.demo05;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);//先设置优先级t1.start();t2.setPriority(1);t2.start();t3.setPriority(4);t3.start();t4.setPriority(Thread.MAX_PRIORITY);t4.start();}
}class MyPriority implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());}
}/*
main--->5
Thread-0--->5
Thread-2--->4
Thread-3--->10
Thread-1--->1Process finished with exit code 0
*/
多线程08:守护线程(daemon)
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
线程分为用户线程和守护线程,虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕,守护线程比如监控内存、垃圾回收等。
测试用例:守护线程thread.setDaemon(true);//默认是false,表示是用户线程
会在JVM结束后接着运行一段时间。
package lishizheng.demo05;//测试守护线程
public class TestDaemon {public static void main(String[] args) {God god = new God();You you = new You();Thread thread = new Thread(god);thread.setDaemon(true);//默认是false,表示是用户线程thread.start();//上帝线程new Thread(you).start();//用户线程启动}
}class God implements Runnable{@Overridepublic void run() {while (true){System.out.println("上帝保佑着你");}}
}class You implements Runnable{@Overridepublic void run() {for (int i = 1; i < 100; i++) {System.out.println("已经开心活过了"+ i +"年");}System.out.println("goodbye world");}
}/*
部分运行结果
已经开心活过了92年
已经开心活过了93年
已经开心活过了94年
已经开心活过了95年
已经开心活过了96年
已经开心活过了97年
已经开心活过了98年
已经开心活过了99年
goodbye world
上帝保佑着你
上帝保佑着你
上帝保佑着你
上帝保佑着你
上帝保佑着你
Process finished with exit code 0*/
多线程09:线程同步
线程同步是控制线程执行的先后顺序。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
线程不安全举例:
package lishizheng.demo06;//不安全地买票
public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket buyTicket = new BuyTicket();new Thread(buyTicket,"小米").start();new Thread(buyTicket,"小明").start();new Thread(buyTicket,"黄牛").start();}
}class BuyTicket implements Runnable{private int ticketNums = 10;boolean flag = true;@Overridepublic void run() {//模拟延时try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//买票while (flag){buy();}}private void buy(){if(ticketNums <= 0) {flag = false;return;}System.out.println(Thread.currentThread().getName() + " 拿到" + ticketNums--);}
}
同步方法
同步块:synchronized (Obj) {}
,Obj称为同步监视器。
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者是class
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问。
- 第一个线程访问完毕,解锁同步监视器。
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
扩充指示JUC:并发编程的安全性。
package lishizheng.demo06;import java.util.concurrent.CopyOnWriteArrayList;//测试JUC安全类型的集合
public class TestJUC {public static void main(String[] args) throws InterruptedException {CopyOnWriteArrayList<String > list = new CopyOnWriteArrayList<String >();for (int i = 0; i < 10000; i++) {new Thread(() -> {list.add(Thread.currentThread().getName());}).start();}Thread.sleep(3000);System.out.println(list.size());}
}
多线程10:死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止运行的情形。某个同步块拥有"两个以上对象的锁"时,就可能发生死锁。
死锁举例 :
灰姑娘和白雪公主都喜欢化妆,这里的化妆需要镜子和口红两者都具备才能完成。 当灰姑娘拿到口红,而白雪拿到镜子的时候,两者相互等待,这样就构成死锁。
程序进入死锁(卡死),如下图:
测试代码:
package lishizheng.demo06;//死锁:多个线程互相持有对方所需的资源,然后形成僵持。
public class DeadLock {public static void main(String[] args) {Makeup girl1 = new Makeup(0,"灰姑娘");Makeup girl2 = new Makeup(1,"白雪公主");//启动线程girl1.start();girl2.start();}
}//口红
class Lipstick{}//镜子
class Mirror{}class Makeup extends Thread{//需要的资源只有一份,用static来保证只有一份static Lipstick lipstick = new Lipstick();static Mirror mirror = new Mirror();int choice; // 选择String girlName; //使用化妆品的人Makeup(int myChoice, String myGirlName){choice = myChoice;girlName = myGirlName;}@Overridepublic void run() {//化妆try {makeup();} catch (InterruptedException e) {e.printStackTrace();}}//化妆,互相持有对方的锁,即需要拿到对方的资源private void makeup() throws InterruptedException {if(choice == 0){synchronized (lipstick){//获得口红的锁System.out.println(girlName + "获得口红的锁");Thread.sleep(1000);synchronized (mirror){System.out.println(girlName + "获得镜子的锁");}}}else{synchronized (mirror){System.out.println(girlName + "获得镜子的锁");Thread.sleep(2000);synchronized (lipstick){System.out.println(girlName + "获得口红的锁");}}}}
}
死锁避免的方法
产生死锁的四个必要条件:
- 互斥条件:一个资源一次只能被一个进程使用
- 请求和保护条件:一个进程因请求资源被阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用之前不能强制剥夺。
- 循环等待条件:若干进程之间形成一种头尾相连的循环等待关系
只要想办法破坏上面任意一个或者多个就可以避免死锁。
Lock(锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制:通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有1个对象对Lock对象加锁,线程开始访问共享资源之前要先获得Lock对象。
ReentrantLock类实现了Lock,它拥有了与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁。
测试代码:使用reentrantLock
显式定义锁和解锁。
package lishizheng.advance;import java.util.concurrent.locks.ReentrantLock;//测试Lock锁
public class TestLock {public static void main(String[] args) {TestLock2 testLock2 = new TestLock2();new Thread(testLock2).start();new Thread(testLock2).start();new Thread(testLock2).start();}
}class TestLock2 implements Runnable{private int ticketNums = 10;//定义lock锁//可重入锁 re + entrant + lock private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true){try {lock.lock();//加锁if(ticketNums > 0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticketNums--);}else break;}finally {//解锁lock.unlock();}}}
}
synchronized 和Lock对比
- Lock是显式锁,需要手动上锁和解锁;synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较小时间来调度线程,性能更好。并且具有更好的扩展性。
- 优先使用顺序:Lock 》同步代码块 》同步方法
多线程11:线程协作
生产者消费者问题
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
来源:维基百科
线程通信
Java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程的通知,会释放锁 |
wait(long tiimeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先被调度 |
注意:均为Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
.
解决方法1:管程法
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿数据。
package lishizheng.demo06;//测试生产者消费者,利用缓冲区解决,即管程法//what do we need?
//生产者,消费者,产品,缓冲区
public class TestPC {public static void main(String[] args) {SynContainer container = new SynContainer();new Producer(container).start();new Consumer(container).start();}
}//生产者
class Producer extends Thread{SynContainer container;public Producer( SynContainer container){this.container = container;}//生产@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("生产了 " + i + "只鸡");container.push(new Chicken(i));}}
}//消费者class Consumer extends Thread{SynContainer container;public Consumer( SynContainer container){this.container = container;}//消费@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("消费了第 " + container.pop().id + " 只鸡");}}
}//产品class Chicken{int id;public Chicken(int id) {this.id = id;}
}//缓冲区
class SynContainer{//容器大小10Chicken[] chickens = new Chicken[10];//容器计数器int count = 0;//生产者放入产品public synchronized void push(Chicken myChicken){//如果容器满了,等待消费者消费if(count == 10){//通知消费者消费,生产者等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果未满,则放入产品chickens[count] = myChicken;count++;//进程通信,通知 消费者消费this.notifyAll();}//消费者消费产品public synchronized Chicken pop(){//判断能否消费if(count == 0){//等待生产者生产,消费者等待try {wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果可以消费count --;Chicken chicken = chickens[count];//吃完了,通知生产者生产this.notifyAll();return chicken;}
}
解决方法2:信号灯法
暂无。
参考资料
[1]https://www.bilibili.com/video/BV1V4411p7EF?p=1
[2]https://www.runoob.com/java/java-multithreading.html
感谢您阅读到最后,祝您一切顺利。
Java多线程详解[狂神说Java]相关推荐
- 【运维能力提升计划-3】Java多线程详解
Java多线程详解 学习链接 Java.Thread 线程简介 线程 进程 多线程 线程实现 Thread 继承Thread类 调用run方法只有主线程一个线程,调用start方法生成子线程与主线程并 ...
- Java多线程详解(线程不安全案例)
嗨喽-小伙伴们我又来了, 通过前面两章的学习,我们了解了线程的基本概念和创建线程的四种方式. 附上链接: 1. Java多线程详解(基本概念) 2. Java多线程详解(如何创建线程) ...
- Java多线程详解(基本概念)
嗨喽-小伙伴们我来啦, 从本章开始,我们就要开始介绍Java中一个非常重要的概念-----多线程.线程化思想是计算机领域的重要思想,有了线程,咱编写的程序才能更为高效准确地运行起来. 首先,咱来了解一 ...
- Java 多线程详解(五)------线程的声明周期
Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...
- Java 多线程详解(四)------生产者和消费者
Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...
- Java 多线程详解(三)------线程的同步
Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 Java 多线程详解(二 ...
- Java 多线程详解(二)------如何创建进程和线程
Java 多线程详解(一)------概念的引入:https://blog.csdn.net/weixin_39816740/article/details/80089790 在上一篇博客中,我们已经 ...
- Java多线程详解(二)
评论区留下邮箱可获得<Java多线程设计模式详解> 转载请指明来源 1)后台线程 后台线程是为其他线程服务的一种线程,像JVM的垃圾回收线程就是一种后台线程.后台线程总是等到非后台线程死亡 ...
- Java 多线程详解(一)------概念的引入
这是讲解 Java 多线程的第一章,我们在进入讲解之前,需要对以下几个概念有所了解. 1.并发和并行 并行:指两个或多个时间在同一时刻发生(同时发生): 并发:指两个或多个事件在一个时间段内发生. 在 ...
最新文章
- 记忆的天空:智能进化三部曲
- 怎么查看jre版本_javac和java版本不一致问题
- 调试中的一些Python错误
- 接口测试基础之入门篇(待续)
- Apache详细介绍 - [ Apache v2.4.10 for Windows ]
- excel打开csv错误换行_「乱吐槽·乱学习」excel高手捷径:一招鲜,吃遍天③
- Python:闭包(简介、使用方法、nonlocal修改闭包内使用的外部变量)、装饰器(定义、作用、通用装饰器、多个装饰器、带参数的装饰器、类装饰器、装饰器方式添加WEB框架的路由)
- 蓝桥杯 ALGO-101 算法训练 图形显示
- Meta Learning | 加了元学习之后,少样本学习竟然可以变得这么简单!
- 【Verilog】verilog实现奇数次分频
- C#实现封装SPC过程能力工具类ProcessCababilityHelper
- 用python编程 商品打折怎么计算_折扣怎么算用计算公式
- 致远OA 组合getshell
- 搞渗透!还不会信息收集的看这里(大佬的秘籍)
- project下build.gradle文件和module下buil.gradle
- 基于人工鱼群优化可倒摆法(QIP)控制器附matlab代码
- linux操作系统未正常启动提示Entering emergency mode. Exit the shell to continue
- 编写 Linux shell 脚本 踩坑记录
- 软件本身呢对shellcode 检查 SEH Exploit学习
- (Easyx联合c++的通讯录系统)续
热门文章
- 键盘-App监听软键盘按键的三种方式
- YII 控件使用笔记
- Spring之DataSource注入
- html属性选择器怎么写,html – 具有“type”属性与make-up属性的CSS属性选择器和区分大小写...
- python库_计算机二级教程 Python语言程序设计,第10章python第三方库概览
- 神经网络的sigmoid激活函数是一种平方映射
- python split 倒数第一个_请教一个在python中该如何去掉split之后的第一个单词?
- 【控制】《复杂运动体系统的分布式协同控制与优化》-方浩老师-第3章-局部指数稳定的多欧拉-拉格朗日系统协同控制
- TCL with SNPS get_attributesget_lib_attributelist_attributsreport_attribute
- 【PC工具】复制翻译神器!有了这个开源免费的翻译软件,阅读英文文档变得再也不困难了...