塈百日而求新,念三番未发,其三
线程简介
多任务:一个人同时干多件事情
多线程:通过道路划分,提高车辆运行效率
程序:是一个静态的概念,指令和数据的有序集合
进程/Process:程序的一次执行过程,系统必定会分配的,是指操作系统中运行的程序,比如QQ、游戏等
线程/Thread:
- 比如游戏中的声音、图像、弹幕
- 多线程需要多核——多个CPU,因为同一时间一个CPU只能执行一行代码
- 线程之间相互独立
- 线程的运行由调度器安排,不能人为干预
- 对同一份资源操作时存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销:CPU调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程实现(重点)
线程创建
- Thread class(重点):继承Thread类
- Runnable接口(重点):实现Runnable接口
- Callable接口:实现Callable接口
Thread(重点)
如上图所示:有run()就先执行;有start()就同步执行
范例:
package com.qf.Demo01;
//1. 继承Thread类
public class TestThread1 extends Thread{
//2. 重写run方法
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("我在看代码--------"+i);
}
}
//主线程
public static void main(String[] args) {
//3. 调用start方法开启线程
TestThread1 testThread1 = new TestThread1();
testThread1.start();
for (int i = 0; i < 2000; i++) {
System.out.println("我在学习多线程-------"+i);
}
}
}
结果如下图所示:
也就能看出
- start()是同步进行
- 线程开启后不一定立即执行,由CPU调度执行
实现多线程同步下载图片
先下载commons-io
创建Package,取名lib,复制过来该文件
然后再右键选择Add as Library,就可以进行使用了,如下图所示:
先理顺一下思路:先写下载器WebDownloader→实现线程类/继承Thread,并且用构造器放入url和name→重写run()→通过构造器创建t1~t3三个线程,然后启动
代码范例:
package com.qf.Demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class TestThread2 extends Thread {
private String url; //网络图片地址
private String name; //保存的文件名
//写构造器
public TestThread2(String url, String name) {
this.url = url;
this.name = name;
}
//重写之后的run()变为下载图片线程的执行体
@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("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
"1.jpg");
TestThread2 t2 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
"2.jpg");
TestThread2 t3 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
"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方法出现问题");
}
}
}
结果:
下载了文件名为:1.jpg
下载了文件名为:3.jpg
下载了文件名为:2.jpg
图片下载顺序结果和操作顺序不同也说明线程同时执行。
查看下载的图片:
Runnable(重点)
范例:
package com.qf.Demo01;
public class TestThread3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 2000; 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 < 2000; i++) {
System.out.println("我在学习多线程-------"+i);
}
}
}
结果如下图所示:
同样也能看出两个线程是同时进行的
对比继承Thread和Runnable接口
- 两者的重写run()是相同的
- 继承Thread是直接调用start();Runnable接口则需要将该类的实现对象放入线程对象当中,作为其参数,然后再调用start()。
同样的,如果将继承Thread改成实现Runnable接口只需要更改两处地方:
小结:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3sLDWAXT-1661861385967)(C:\Users\86156\Desktop\image-20220607132025759.png)]
多个线程同时操作同一个对象
范例1:
package com.qf.Demo01;
//买火车票
public class TestThread4 implements Runnable{
//票数
private int ticketnumbers = 10;
//重写run()
@Override
public void run() {
while (true){
if(ticketnumbers<=0){
break;
}
//利用Thread.sleep()模拟抢票的延时情况,然后捕获异常
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketnumbers--+"张票"); //该方法可以获得当前正在执行的线程的名字
}
}
public static void main(String[] args) {
//因为实现了Runnable接口,所以只需要创建一个类对象,然后让多个线程一起start即可
TestThread4 ticket = new TestThread4();
new Thread(ticket,"小明").start(); //重写run()中的getName(),这里可以赋予name具体值;如果不赋予,最终结果就是"线程0~2“
new Thread(ticket,"小红").start();
new Thread(ticket,"小绿").start();
}
}
结果:
小红-->拿到了第9张票
小绿-->拿到了第10张票
小明-->拿到了第8张票
小明-->拿到了第6张票
小红-->拿到了第5张票
小绿-->拿到了第7张票
小红-->拿到了第3张票
小明-->拿到了第2张票
小绿-->拿到了第4张票
小明-->拿到了第1张票
小绿-->拿到了第1张票
小红-->拿到了第0张票
结果的倒数第二、第三行出现了小明和小绿抢了同一张票,所以多个线程操作同一个资源的情况下,线程不安全,数据紊乱
范例2:
先理清一下思路:距离为100步的for循环→判断比赛是否结束gameOver(),并且打印出胜利者→比赛开始start()→模拟兔子睡觉→乌龟获胜
package com.qf.Demo01;
//龟兔赛跑
public class Race implements Runnable{
//胜利者 只有一个,可以使用静态常量static
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
//模拟兔子休息 让"兔子"没跑11步睡一会
if(Thread.currentThread().getName().equals("兔子") && 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;
}
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,"乌龟").start();
new Thread(race,"兔子").start();
}
}
结果如下图所示:
Callable接口(了解)
特点
-
实现Callable接口,需要返回值类型
-
重写call方法,需要抛出异常
-
创建执行服务:
-
ExecutorService ser = Executors.newFixedThreadPool();
-
-
提交执行
-
Future<Boolean> r1 = ser.submit(t1);
-
-
获取结果
-
boolean rs1 = r1.get();
-
-
关闭服务
-
ser.shutdownNow();
-
用Callable方法实现下载图片,和Runnable接口进行比较:
总结:
Callab的好处:
- 可以定义返回值
- 可以抛出异常
静态代理模式
范例:
package com.qf;
public class StaticProxy {
public static void main(String[] args) {
//别忘了写main函数 0.0
//如果不使用静态代理模式,就要像下面两行代码这样运行
//You you = new You(); //你要结婚
//you.HappyMarry();
WeddingCompany weddingCompany = new WeddingCompany(new You());
weddingCompany.HappyMarry();
//以上两行代码=下面这一行代码
//new WeddingCompany(new You()).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 after() {
System.out.println("结婚之后,收尾款");
}
private void before() {
System.out.println("结婚之前,布置现场");
}
}
结果:
结婚之前,布置现场
百年好合
结婚之后,收尾款
总结:
- 真实对象和代理对象都要实现同一个接口
- 代理对象要代理真实角色/要传递一个参数
好处:
- 代理对象可以做很多真实对象做不了的事情
- 真实对象可以专注做自己的事情(上班族要结婚,就将这项任务交给婚庆公司,不耽误自己正常工作)
Lambda表达式
为什么要用Lambda表达式:
- 避免匿名内部类定义过多
- 简洁化代码
- 精简化代码
Lambda表达式重要性:实现多线程方法中有一个非常重要的接口——Runnable接口,它里面就是一个interface+run(),也就是说如果Runnable接口里的实现类简单,可以用Lambda表达式简化
先了解函数式接口:只包含唯一一个抽象方法,如下例所示:
public interface Runnable{
public abstract void run();
}
对于函数式接口,就可以通过lambda表达式来创建该接口的对象
范例1:
package com.qf.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 ilike = new Like(); //注意这里ILike是抽象的,无法实例化,注意写法
ilike.lambda();
//ilike 是定义的一个接口,不需要再次定义
ilike = new Like2();
ilike.lambda();
//4. 使用局部内部类进一步优化
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("i like lambda3");
}
}
ilike = new Like3();
ilike.lambda();
//5. 使用匿名内部类继续优化 匿名也就是没有名字,必须借助接口或者父类来实现 然后把核心代码放入匿名内部类当中即可
//所以这里new ILike()
ilike = new ILike() {
@Override
public void lambda() {
System.out.println("i like lambda4");
}
};
ilike.lambda();
//6. 用lambda简化 因为类和方法都是一样的,所以直接省略
ilike = ()-> {
System.out.println("i like lambda5");
};
ilike.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
i like lambda5
范例2:
package com.qf.lambda;
//Ctrl+Shift+/ 可以将选定的部分全部进行多行注释
public class TestLambda2 {
public static void main(String[] args) {
//不用Lambda一般都是这样:首先先实现Ilove接口 (new 实现类) → 然后调用love实现类中重写的love方法
//使用lambda
//Ilove love = (int a) -> { System.out.println("i love you ->"+a); };
//Lambda表达式简化
//为了方便,先将Ilove love 这部分省略掉
Ilove love =null;
//1. 参数类型
//love = (a) -> { System.out.println("i love you ->"+a); };
//2. 简化括号
//love = a -> { System.out.println("i love you ->"+a); };
//3. 去掉花括号
love = a -> System.out.println("i love you ->"+a);
love.love(522);
}
}
//接口
interface Ilove{
void love(int a);
}
结果:
i love you ->522
总结:
-
Lambda表达式只能将一行表达式简化为一行代码,如果有多行,就用代码块包裹({})
-
前提:接口是函数式接口
-
多个参数:①要去掉函数类型就一起去掉;②保留括号
线程状态
线程停止
范例:
package com.qf.state;
//① 建议线程正常停止-->利用次数,不建议死循环
//② 建议使用标志位
//③ 不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class TestStop implements Runnable{
//1. 设置一个标志位
private boolean flag = true;
//重写run(),同时也是线程体
@Override
public 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 < 1000; i++) {
System.out.println("main"+i); //main 是主线程
//调用自己写的stop()转换标志位,不管主线程还是辅线程,只要i==900,就停止辅线程
if(i==900){
testStop.stop();
System.out.println("线程该停止了");
}
}
}
}
结果如下图所示:当然也可以看出主辅两个线程是"同时"运行的
线程休眠
- sleep(时间) 指当前线程阻塞的毫秒数(1/1000秒)
- 存在异常,需要抛出
- sleep时间结束后,线程进入就绪状态
- sleep可以模拟网络延时、倒计时等,放大问题的发生性,让其更加真实
- 每个对象都有一把锁,sleep不会释放锁
范例1:
package com.qf.state;
//模拟倒计时
public class TestSleep2 {
public static void tenDown() throws InterruptedException {
int num = 10;
while (true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0) break;
}
}
public static void main(String[] args) {
try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
10
9
8
7
6
5
4
3
2
1
范例2:
package com.qf.state;
import java.text.SimpleDateFormat;
import java.util.Date;
//模拟倒计时
public class TestSleep2 {
public static void tenDown() throws InterruptedException {
int num = 10;
while (true){
Thread.sleep(1000);
System.out.println(num--);
if(num<=0) break;
}
}
public static void main(String[] args) {
//打印当前系统时间
Date startTime = new Date(System.currentTimeMillis());
while (true){
try {
Thread.sleep(1000);
//每休眠1s,打印一次时间
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
//更新时间
startTime = new Date(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果如下图所示: 没过1s打印一次系统时间
线程礼让
如上图所示:线程A和线程B都是就绪状态,当A进入CPU开始执行时如果进行线程礼让,A和B就会回到同一起跑线/就绪态,等待CPU重新调度。但是此时礼让是否成功/CPU是否调度B是不一定的,看心情。
范例:
package com.qf.state;
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{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"线程停止执行");
}
}
结果:
a线程开始执行
b线程开始执行
a线程停止执行
b线程停止执行
线程强制执行
理解为插队,Join将制定线程执行完成之后再执行其他线程,其他线程暂时堵塞。
范例:
package com.qf.state;
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程VIP来了");
}
}
public static void main(String[] args) throws InterruptedException {
//启动我们的插队线程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主线程
for (int i = 0; i < 1000; i++) {
if(i==200){
thread.join(); //插队
}
System.out.println("main"+i);
}
}
}
结果如下图所示:
线程状态观测
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED _WAITING
- TERMINATED
范例:
package com.qf.state;
public class TestState {
public static void main(String[] args) throws InterruptedException {
//以Lambda表达式形式创建线程
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(); //先写 thread.getState() 然后使用Alt+Enter来快捷生成变量对象
System.out.println(state); //NEW
//观察启动后
thread.start();
state = thread.getState(); //不用每次都创建一个新的state,可以直接使用上一次创建好的,节省空间
System.out.println(state); //RUN
while (state != Thread.State.TERMINATED){ //只要线程不终止,就一直输出状态
Thread.sleep(100);
state = thread.getState(); //更新while()里的线程状态
System.out.println(state);
}
thread.start(); //线程不能启动两次
}
}
结果:
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
//
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:710)
at com.qf.state.TestState.main(TestState.java:31)
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
优先级高了,权重、分配的资源就高,就更可能先执行。
package com.qf.state;
public class TestPriority {
public static void main(String[] args) {
//打印主线程的默认优先级
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
//创建多个对象并且把线程体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);
//先设置优先级,再启动
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(4);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY); //10
t4.start();
t5.setPriority(-1); //不合法
t5.start();
t6.setPriority(11); //不合法
t6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
}
}
结果:
main-->5
Thread-2-->4
Thread-3-->10
Thread-0-->5
Thread-1-->1
Exception in thread "main" java.lang.IllegalArgumentException
at java.lang.Thread.setPriority(Thread.java:1094)
at com.qf.state.TestPriority.main(TestPriority.java:27)
守护线程
- 线程分为用户线程(main) 和 守护线程(gc/垃圾回收)
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
范例:
package com.qf.state;
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); //.setDaemon()默认是false/用户线程
thread.start();//上帝守护线程启动
new Thread(you).start(); //你 用户线程启动
}
}
//上帝
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝保佑着你");
}
}
}
//你
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("一生都开心的活着");
}
System.out.println("去天堂");
}
}
结果如下图所示:
注意:理论上在用户线程结束/去天堂之后,就不会继续执行守护线程,但是虚拟机关闭需要一定的时间。
线程同步(重点)
多个线程操作同一个资源/并发,可能出现票数为负数的情况,这时候就需要进行线程同步。线程同步就是一个等待机制,要进行操作的线程要进入这个对象的等待池形成队列,依次执行。
线程同步的形成条件/解决安全性的条件:队列 + 锁
锁机制:synchronized,保证线程安全。当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
锁机制也会引起一下问题:
- 一个线程持有锁会导致其他相关线程挂起,虽然安全,但是加锁、释放锁的操作会降低性能
- 优先级高的线程等待低优先级的线程释放锁,导致导致优先级导致/性能问题
不安全范例
范例1:
package com.qf.syn;
//不安全买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
new Thread(station,"1").start();
new Thread(station,"2").start();
new Thread(station,"3").start();
}
}
class BuyTicket implements Runnable{
//票数 private为了安全
private int ticketnumbers = 10;
boolean flag = true; //外部停止方式
@Override
public void run() {
//买票
while (flag){
buy();
}
}
private void buy(){
//判断是否有票
if(ticketnumbers<=0){
flag = false;
return;
}
//有票
System.out.println(Thread.currentThread().getName()+"拿到"+ticketnumbers--);
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
1拿到9
3拿到8
2拿到10
2拿到7
1拿到7
3拿到6
2拿到5
1拿到5
3拿到4
1拿到3
2拿到2
3拿到1
拿到相同的两张票(还可能出现票数为-1的情况)→线程不安全
范例2:
package com.qf.syn;
//不安全的取钱
public class UnsafeBank {
public static void main(String[] args) {
//账户
Account account = new Account(100,"零花钱"); //如果在Account类中,不添加constructor,在这里就会报错
Drawing you = new Drawing(account,50,"你");
Drawing yourgirlfriend = new Drawing(account,100,"yourgirlfriend");
you.start();
yourgirlfriend.start();
}
}
//账户
class Account{
//因为需要在银行类中使用这两个变量,不写private更方便调用
int money; //余额
String name; //卡名
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款 因为涉及到多个线程操作同一个对象所以extends Thread
class Drawing extends Thread{
Account account; //账户
int drawingMoney; //要取多少钱
int nowMoney; //现在还有多少钱
//因为继承了Thread类,无法通过Alt+Insert来快捷生成构造方法,所以需要手动填写
public Drawing(Account account,int drawingMoney,String name){
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if(account.money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
return; //钱不够就退出了
}
//sleep放大问题的发生性
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 余额 - 你取的钱
account.money -= drawingMoney;
//你手里的钱
nowMoney += drawingMoney;
//打印账户余额
System.out.println(account.name+"余额为:"+account.money);
//打印手里的钱 这里Thread.currentThread().getName()=this.getName()
System.out.println(this.getName()+"手里的钱"+nowMoney);
}
}
结果:
零花钱余额为:0
yourgirlfriend手里的钱100
零花钱余额为:-50
你手里的钱50
通过结果的余额为负就能看出线程的不安全
这里有个比较有趣的点:
该行代码放在Drawing方法体里的第一行时是真确的,但是放在第三行就会出现错误。提示上说“对于super()的申明必须作为结构体的第一个声明”,按照提示上来即可。
范例3:
package com.qf.syn;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
public static void main(String[] args) {
//先实例化一个ArrayList型的List
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() ->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
结果:
997
结果不是1000→不安全。原因也很简单,因为将名字加入list中时,多个线程可能添加到同一个位置/操作对象的位置相同,也就覆盖掉了,总数也就少了。
同步方法及同步块
在封装中通过使用private关键字和get/set来使对象安全。相应的,也就有synchronized关键字(分为方法和块,两者的运行原理相同)。
每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的所才能执行,否则线程就会阻塞,方法执行时独占该锁,知道方法返回才释放锁,后面被阻塞的线程才能获得锁来继续执行。
缺点:如果一个大的方法申明synchronized就会影响效率。
代码中只读部分是每个人都可以读取的,同步只需要同步修改部分的代码,否则就会浪费资源。也就是synchronized块。
修改火车站买票(只有一个操作对象):
修改两人银行取钱(有两个操作对象):
当锁执行方法——run()之后发现还是会出错,这是因为由于默认锁的是自己这个类本身(this),也就是Drawing,而增删改查的对象是account账户。需要使用同步块来锁定变化的量(同步块可以锁定任何对象):
将原来的代码移到synchronized (account){}中即可
修改线程不安全的范例:
CopyOnWriteArrayList
package com.qf.syn;
import java.util.concurrent.CopyOnWriteArrayList;
//测试JUC安全类型的集合
public class TestJUC {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); //这个类是安全类,理解成已经加号加synchronized锁的类
for (int i = 0; i < 1000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
结果:
1000
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。 当某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生死锁。
范例:
package com.qf.syn.thread;
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"小红");
Makeup g2 = new Makeup(10,"小绿");
g1.start();
g2.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 choice, String girlname) {
this.choice = choice;
this.girlname = girlname;
}
@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.girlname + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) { //1s后想获得镜子
System.out.println(this.girlname + "获得镜子的锁");
}
}
} else {
synchronized (mirror) { //获得镜子的锁
System.out.println(this.girlname + "获得镜子的锁");
Thread.sleep(1000);
synchronized (lipstick) { //1s后想获得口红
System.out.println(this.girlname + "获得口红的锁");
}
}
}
}
}
结果:
小绿获得镜子的锁
小红获得口红的锁
出现了死锁,只需要分别将一个锁移出代码块即可:
Lock
ReentrantLock(可重入锁)类实现了Lock,比较常用,需要实现接口,可以显示加锁、释放锁
范例:
package com.qf.syn.thread.gaoji;
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{
int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}
}
}
结果:
10
8
9
7
6
5
4
3
2
1
-1
0
出现-1,是不安全的。
使用Lock进行调整:
synchronized与Lock对比
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,除了作用域自动释放
- Lock只有代码块锁
- Lock锁:JVM将花费较少的时间来调度进程,性能更好,并且有更多的子类(ReentrantLock)
生产者消费者问题
使用wait()和notify()实现通信
管程法
范例:
package com.qf.syn.thread.gaoji;
//生产者消费者模型,利用缓冲区解决
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生产者
class Productor extends Thread{ //将Productor存在词典里就不会有下波浪线了
//生产者和消费者都需要一个容器
SynContainer container;
//构造方法
public Productor(SynContainer container){
this.container = container;
}
//生产方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
//消费过程
@Override
public 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{
//容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了,就需要等待消费者消费
if(count == chickens.length){
//生产等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有满,我们就需要丢入产品
chickens [count] = chicken;
count++;
//可以通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
//判断能否消费
if(count == 0){
//等待生产者生产、消费者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消费
count--;
Chicken chicken = chickens[count];
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
结果如下图所示:
必须生产之后才能消费,符合规律。
信号灯法
package com.qf.syn.thread.gaoji;
//测试生产者消费者问题2:信号灯法,标志位解决
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生产者 -->演员
class Player extends Thread{
//都需要"节目"
TV tv;
//构造方法
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if(i%2==0){
this.tv.play("快乐大本营播放中");
}else {
this.tv.play("抖音:记录美好生活");
}
}
}
}
//消费者 -->观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
tv.watch();
}
}
}
//产品 -->节目
class TV{
//演员表演,观众等待 T
//观众观看,演员等待 F
String voice; //表演的节目
boolean flag = true; //标志位
//表演
public synchronized void play(String voice){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了"+voice);
//通知观众观看
this.notifyAll(); //通知唤醒
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了:"+voice);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
结果:
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
演员表演了抖音:记录美好生活
观看了:抖音:记录美好生活
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
演员表演了抖音:记录美好生活
观看了:抖音:记录美好生活
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
线程池
由于经常创建和销毁重复性线程对性能影响很大,所以可以提前创建好多个线程,放入线程池中,要用就拿,用完放回去,理解成共享单车。
好处:
- 提高响应速度(减少了创建新县城的时间)
- 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
- 核心池的大小/能存放多少个线程
- 最大线程数/能同时跑多少个线程
- 线程没任务时最多保持多长时间终止
package com.qf.syn.thread.gaoji;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//测试线程池
public class TestPool {
public static void main(String[] args) {
//1. 创建服务 创建线程池
//newFixedThreadPool():参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
//2. 执行 把线程丢进去,execute可以执行Runnable接口的实现类
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//3. 关闭链接
service.shutdownNow();
}
}
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
pool-1-thread-4
总结
创建线程方法:
- 1.继承Thread类
- 2.实现Runnable接口
- 3.实现Callable接口