【学习笔记】
1.线程的介绍
1.线程相关的概念
- 程序:是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码。
- 进程:进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
- 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。
2.什么是线程
线程由进程创建的,是进程的一个实体。一个进程可以拥有多个线程,如下图
坦克大战项目中会加入多线程!!!
3.单线程
单线程:同一个时刻,只允许执行一个线程
4.多线程
多线程:同一个时刻,可以执行多个线程,比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。
5.并发
并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。
6.并行
并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。
package com.cly.tankGame2;public class cpuNum {public static void main(String[] args) {Runtime runtime=Runtime.getRuntime();//过去当前电脑的cpu数量 核心数int cpuNums=runtime.availableProcessors();System.out.println("当前CPU个数:"+cpuNums);}
}
//当前CPU个数:8
2.线程使用
1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法
线程应用案例一 继承Thread类:
package com.cly.thread;public class Thread01 {public static void main(String[] args) throws InterruptedException {//创建Cat对象可以当作线程使用Cat cat = new Cat();cat.start();//启动线程//说明:当main线程启动一个子线程Thread-0,主线程不会阻塞,会继续执行//这时 主线程和子线程会交替执行。。。System.out.println("主线程继续执行不会阻塞 线程名称:"+Thread.currentThread().getName());for (int i = 0; i < 10; i++) {System.out.println("主线程 i="+i);//让主线程也休眠一下Thread.sleep(1000);}}
}//当一个类继承了thread类,那么该类就可以当作线程使用
//我们会重写run方法,写上自己的业务代码
//run Thread类 实现了 Runnable 接口的run方法
/*
* @Overridepublic void run() {if (target != null) {target.run();}}d
* */class Cat extends Thread{int times=0;@Overridepublic void run() {//重写run方法,写上自己的业务逻辑while(true) {//该线程每隔一秒钟,在控制台打印出"喵喵,我是小猫咪"System.out.println("喵喵,我是小猫咪"+(++times)+" 线程名称:"+Thread.currentThread().getName());//休眠try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (times==8){//当输出8次后退出该线程break;}}}
}
运行结果:
主线程继续执行不会阻塞 线程名称:main
主线程 i=0
喵喵,我是小猫咪1 线程名称:Thread-0
主线程 i=1
喵喵,我是小猫咪2 线程名称:Thread-0
主线程 i=2
喵喵,我是小猫咪3 线程名称:Thread-0
主线程 i=3
喵喵,我是小猫咪4 线程名称:Thread-0
喵喵,我是小猫咪5 线程名称:Thread-0
主线程 i=4
主线程 i=5
喵喵,我是小猫咪6 线程名称:Thread-0
主线程 i=6
喵喵,我是小猫咪7 线程名称:Thread-0
主线程 i=7
喵喵,我是小猫咪8 线程名称:Thread-0
主线程 i=8
主线程 i=9
为什么cat.start()?
如果**改成cat.run()**方法,run()方法只是一个普通的方法,没有真正的启动一个子线程(当然这个时候run里打印出的线程名对应的是main而不是 Thread0),它是直接找到run去执行的。
只有start才会去真正的启动一个线程!
这个时候程序会把run方法里的执行完之后再去执行 上面的代码,此时此刻就导致了主线程的阻塞。
这个种情况可以称为串行
线程应用案例二 实现Runnable接口:
JAVA是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承线程类方法来创建线程显然不可能了。
JAVA设计者们提供了另外一个方式创建线程,就是通过实现可运行的接口来创建线程。
package com.cly.thread;//通过实现接口Runnable来开发线程
public class Thread02 {public static void main(String[] args) {Dog dog = new Dog();//dog.start();这里不能调用start//创建了Thread对象,把dog对象(实现了Runnable),放入ThreadThread thread = new Thread(dog);thread.start();}
}class Dog implements Runnable{int count=0;@Overridepublic void run() {//普通方法while (true){System.out.println("小狗汪汪叫...hi"+(++count)+Thread.currentThread().getName());//休眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (count==10){break;}}}
}
为什么Thread thread = new Thread(dog); 可以放入dog
这里使用了设计模式【静态代理模式】来解决的。
代理的意思就是自己没有让具备这个条件的人帮着做,但是真正干活的还是自己!
用代码模拟==>实现Runnable接口开发一个线程的机制 这里演示代码不好懂!!!
线程应用案例三 多个子线程执行:
请编写一个程序,创建两个线程,一个线程每隔1秒输出"hello,world”,输出10次,退出,一个线程每隔1秒输出“hi”,输出5次退出。
package com.cly.thread;public class Thread03 {public static void main(String[] args) {T1 t1 = new T1();T2 t2 = new T2();Thread thread1=new Thread(t1);Thread thread2=new Thread(t2);thread1.start();//启动第一个线程thread2.start();//启动第二个线程}
}class T1 implements Runnable{int count=0;@Overridepublic void run() {while (true) {//每隔1秒输出"hello,world”,输出10次,退出System.out.println("hello,world " + (++count));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (count == 10) {break;}}}
}class T2 implements Runnable{int count=0;@Overridepublic void run() {while (true) {//每隔1秒输出“hi”,输出5次退出。System.out.println("hi " + (++count));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (count == 5) {break;}}}
}
线程如何理解:
3.继承Thread vs实现 Runnable的区别
java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口。实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。
4.初始并发
package com.cly.thread;//狂神版本
//多线程共同操作一个对象
//买火车票例子、 初识并发问题//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class ShellTicketKuangShen implements Runnable {//票数private int ticketNums=10;@Overridepublic void run() {while (true){if (ticketNums<=0) {break;}System.out.println(Thread.currentThread().getName()+"-->拿到了第"+(ticketNums--)+"张票");try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {ShellTicketKuangShen stks =new ShellTicketKuangShen();new Thread(stks,"小明").start();new Thread(stks,"小白").start();new Thread(stks,"小花").start();}
}
解决问题–>引出案例:龟兔赛跑 (Race)
- 首先来个赛道距离,然后要离终点越来越近
- 判断比赛是否结束
- 打印出胜利者
- 龟兔赛跑开始
- 故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
- 终于,乌龟赢得比赛
package com.cly.threadExercise;//模拟龟兔赛跑
public class Race implements Runnable {public static void main(String[] args) {Race race = new Race();new Thread(race,"兔子").start();new Thread(race,"乌龟").start();}//胜利者private static String winner;@Overridepublic void run() {for (int i = 0; i <= 100; i++) {//模拟兔子休息if (Thread.currentThread().getName().equals("兔子") && i%10==0){try {Thread.sleep(5);} 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;}
}
5.实现Callable接口(了解即可)
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(t1);
- 获取结果: boolean r1 = result1.get()
- 关闭服务: ser.shutdownNow();
package com.cly.thread;//下载图片import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;//线程创建方式3 实现Callable接口
public class Thread04 implements Callable<Boolean> {private String url;//网络图片地址private String name;//保存的文件名public Thread04(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 Exception {Thread04 t1=new Thread04("=1343933117,4053295934&fm=26&fmt=auto&gp=0.jpg","奥特曼1.jpg");Thread04 t2=new Thread04("=4093389400,558249165&fm=26&fmt=auto&gp=0.jpg","奥特曼2.jpg");Thread04 t3=new Thread04("=4183078104,522167207&fm=26&fmt=auto&gp=0.jpg","奥特曼3.jpg");// 1. 创建执行服务ExecutorService ser = Executors.newFixedThreadPool(3);
// 2. 提交执行Future<Boolean> result1 = ser.submit(t1);Future<Boolean> result2 = ser.submit(t2);Future<Boolean> result3 = ser.submit(t3);
// 3. 获取结果boolean r1 = result1.get();boolean r2 = result2.get();boolean r3 = result3.get();
// 4. 关闭服务ser.shutdownNow();}}
//下载器
class WebDownloader{//下载方法public void downloader(String url,String name){try {FileUtils.copyURLToFile(new URL(url),new File(name));} catch (IOException e) {e.printStackTrace();}}
}
6.Lambda表达式
λ
希腊字母表中排序第十一位的字母,英语名称为Lambda- 避免匿名内部类定义过多
- 其实质属于函数式编程的概念
( params) ->expression【表达式】
( params) ->statement 【语句】
(params) -> { statements }
new Thread (()->System.out.println(“多线程学习。。。。")) .start();
为什么要使用lambda表达式
- 避免匿名内部类定义过多
- 可以让你的代码看起来很简洁
- 去掉了一堆没有意义的代码,只留下核心的逻辑。
理解Functional Interface(函数式接口))是学习Java8 lambda表达式的关键所在。
函数式接口的定义:
-
任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
public interface Runnable{//函数式接口public abstract void run(){}; }
-
对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
相应练习1:
package com.cly.thread;
/*
* 推导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) {ILike iLike = new Like();iLike.lambda();iLike = new Like2();iLike.lambda();//4.局部内部类class Like3 implements ILike{@Overridepublic void lambda() {System.out.println("i like lambda3");}}iLike = new Like3();iLike.lambda();//5.匿名内部类,没有类的名称必须借助接口或者父类iLike =new ILike() {@Overridepublic 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{@Overridepublic void lambda() {System.out.println("i like lambda");}
}
相应练习2:
//Rannable就可以用lambda表达式 进行简化
package com.cly.thread;public class TestLambda2 {public static void main(String[] args) {ILove iLove = null;iLove = (int a)-> {System.out.println("i love you "+a);};iLove.love(1);//继续简化 1.去掉参数类型iLove = (a)->{System.out.println("i love you "+a);};iLove.love(520);//简化 2.简化括号iLove = a->{System.out.println("i love you "+a);};iLove.love(521);//简化 3.去掉花括号iLove = a->System.out.println("i love you "+a);iLove.love(521314);//总结:
// 1.lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹
// 2.前提是接口为函数式接口
// 3.多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号}
}interface ILove{void love(int a);
}
7.静态代理模式
结婚案例:
package com.cly.thread;//静态代理模式:
/*
* 真实对象和代理对象都要实现一个接口
* 代理对象要代理真实角色
*
* 好处:
* 代理对象可以做很多真实对象做不了的事情
* 真实对象就专注做自己的事情
* */
public class StaticProxy {public static void main(String[] args) {//多线程new Thread(()-> System.out.println("我爱你,很爱很爱")).start();new WeddingCompany(new You()).HappyMarry();// WeddingCompany weddingCompany =new WeddingCompany(new 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();target.HappyMarry();//这就是真实对象after();}private void before() {System.out.println("结婚之前,布置现场");}private void after() {System.out.println("结婚之后,收尾款");}}
8.多线程模拟一个售票系统
[售票系统],编程模拟三个售票窗口售票,分别使用继承 Thread和实现 Runnable方式,并分析有什么问题?SellTicket.java
package com.cly.thread;//使用多线程,模拟三个窗口同时售票
public class ShellTicket {public static void main(String[] args) {/*//测试ShellTicket01 shellTicket01 = new ShellTicket01();ShellTicket01 shellTicket02 = new ShellTicket01();ShellTicket01 shellTicket03 = new ShellTicket01();//这里会出现问题,导致超卖shellTicket01.start();//启动售票线程shellTicket02.start();//启动售票线程shellTicket03.start();//启动售票线程*///第二种使用接口的测试ShellTicket02 shellTicket1 = new ShellTicket02();ShellTicket02 shellTicket2 = new ShellTicket02();ShellTicket02 shellTicket3 = new ShellTicket02();Thread thread1=new Thread(shellTicket1);Thread thread2=new Thread(shellTicket2);Thread thread3=new Thread(shellTicket3);thread1.start();thread2.start();thread3.start();}
}//使用Thread方式
class ShellTicket01 extends Thread{private static int ticketNum=100;//让多个线程共享 num@Overridepublic void run() {while(true){if (ticketNum<=0){System.out.println("售票结束!");break;}//休眠50毫秒try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 "+ Thread.currentThread().getName()+" 售出一张票"+" 剩余票数="+(--ticketNum));}}
}//使用Runnable接口方式
class ShellTicket02 implements Runnable{private static int ticketNum=100;@Overridepublic void run() {while (true){if (ticketNum<=0){System.out.println("售票结束!");break;}//休眠50毫秒try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 "+ Thread.currentThread().getName()+" 售出一张票"+" 剩余票数="+(--ticketNum));}}
}
//继承 Thread和实现 Runnable方式 都会出现超卖的现象,到后面学习了同步(synchronized)就能解决了!!!
9.线程终止
基本说明:
- 当线程完成任务后,会自动退出。
- 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式
package com.cly.threadExit;public class ThreadExit {public static void main(String[] args) throws InterruptedException {T t1 = new T();t1.start();//如果希望main线程去控制t1 线程的终止,必须可以修改loop变量//让main线程休眠10s 在通知t1线程退出Thread.sleep(10*1000);t1.setLoop(false);//改变loop变量终止线程}
}class T extends Thread{private int count=0;private boolean loop=true;public void setLoop(boolean loop) {this.loop = loop;}@Overridepublic void run() {while (loop){try {Thread.sleep(50);//让线程休眠50ms} catch (InterruptedException e) {e.printStackTrace();}System.out.println("T运行中。。。"+ (++count));}}
}
3.线程的常用方法
1.常用方法第一组:
-
setName 设置线程名称,使之与参数name 相同
-
getName 返回该线程的名称
-
start 使该线程开始执行;Java虚拟机底层调用该线程的start0方法
-
run 调用线程对象 run方法;
-
setPriority 更改线程的优先级
-
getPriority 获取线程的优先级
-
sleep 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
-
interrupt A 中断线程
线程中断案例:⭐
package com.cly.threadMethod;public class ThreadMethod01 {public static void main(String[] args) throws InterruptedException {T t = new T();t.setName("CLY");//设置一个线程的名称t.setPriority(Thread.MIN_PRIORITY);//设置一个优先级t.start();//启动子线程//主线程打印5个hi,就中断 子线程的休眠for (int i = 0; i < 5; i++) {Thread.sleep(1000);System.out.println("hi"+i);}System.out.println(t.getName()+" 线程的优先级 ="+t.getPriority());//提前结束休眠t.interrupt();}
}class T extends Thread{@Overridepublic void run() {while (true) {for (int i = 0; i < 100; i++) {//Thread.currentThread().getName() 获取当前线程的名称System.out.println(Thread.currentThread().getName() + "吃包子···" + i);}try {System.out.println(Thread.currentThread().getName() + "休眠中···");Thread.sleep(8000);} catch (InterruptedException e) {//当该线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码System.out.println(Thread.currentThread().getName() + "被interrupt了");}}}
}
2.常用方法第二组:
-
yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。
-
join: 线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务。
案例:main线程创建一个子线程,每隔1s 输出hello,输出20次,主线程每隔1秒,输出hi,输出20次.要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续,
案例
package com.cly.threadMethod;public class ThreadMethod02 {public static void main(String[] args) throws InterruptedException {TT tt = new TT();tt.start();for (int i = 1; i <=20 ; i++) {Thread.sleep(1000);System.out.println("主线程(小弟)吃了"+i+"个包子");if (i==5){System.out.println("小弟吃了5个包子了,剩下的让老大先吃");tt.join();//相当于让老大插队了//Thread.yield();//主线程礼让,但不一定成功!System.out.println("主线程接着吃 小弟接着吃");}}}
}class TT extends Thread{@Overridepublic void run() {for (int i = 1; i <= 20; i++) {try {Thread.sleep(1000);//休眠1秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("子线程(老大)吃了"+i+"个包子");}}
}
线程练习题:
ThreadMethodExercise.java
1.主线程每隔1s,输出hi,一共10次
2.当输出到hi 5时,启动一个子线程(要求实现Runnable),每隔1s输出hello,等该线程输出10次hello后,退出
3.主线程继续输出hi,直到主线程退出.
package com.cly.threadMethod;public class ThreadMethodExercise {public static void main(String[] args) throws InterruptedException {Thread ttt=new Thread(new TTT());//创建子线程for (int i = 1; i <= 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hi~ "+i);if (i==5){ttt.start();//启动子线程,输出hellottt.join();//立即将ttt子线程,插入到main线程,让t3先执行}}}
}class TTT implements Runnable{private int count=0;@Overridepublic void run() {while (true){System.out.println("hello "+(++count));try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (count==10){break;}}}
}
运行结果:
hi~ 1
hi~ 2
hi~ 3
hi~ 4
hi~ 5
hello 1
hello 2
hello 3
hello 4
hello 5
hello 6
hello 7
hello 8
hello 9
hello 10
hi~ 6
hi~ 7
hi~ 8
hi~ 9
hi~ 10
3.用户线程和守护线程
1.用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
2.守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
3.常见的守护线程:垃圾回收机制
如何设置守护线程:
package com.cly.threadMethod;//如何把一个线程设置成守护线程
public class ThreadMethod03 {public static void main(String[] args) throws InterruptedException {MyDaemonThread myDaemonThread = new MyDaemonThread();//如果我们希望当main线程结束后,子线程自动退出//只需要将子线程设为守护线程即可myDaemonThread.setDaemon(true);//先设置在启动就不会报错myDaemonThread.start();for (int i = 1; i <=10 ; i++) {//main线程System.out.println("宝强在辛苦的拍戏。。。");Thread.sleep(1000);}}
}class MyDaemonThread extends Thread{@Overridepublic void run() {while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("马蓉和宋喆快乐聊天,哈哈哈···");}}
}
4.线程的生命周期
线程状态转换图:
案例:
package com.cly.threadMethod;public class ThreadState {public static void main(String[] args) throws InterruptedException {T1 t1 = new T1();System.out.println(t1.getName() +" 状态 "+ t1.getState());t1.start();while (Thread.State.TERMINATED !=t1.getState()){System.out.println(t1.getName() +" 状态 "+ t1.getState());Thread.sleep(1000);}System.out.println(t1.getName() +" 状态 "+ t1.getState());}
}
class T1 extends Thread{@Overridepublic void run() {while (true){for (int i = 0; i < 10; i++) {System.out.println("哈哈哈 "+i);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}break;}}
}
5.线程同步机制(Synchronized)
1.在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
2.也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
同步具体方法-Synchronized
1.同步代码块
synchronized(对象){//得到对象的锁,才能操作同步代码
//需要被同步代码;
}
2.synchronized还可以放在方法声明中,表示整个方法-为同步方法
public synchronized void m (String name){
//需要被同步的代码
}
package com.cly.syn;//使用多线程,模拟三个窗口同时售票
public class ShellTicket {public static void main(String[] args) {//使用接口的测试Thread thread1=new Thread(new ShellTicket02());Thread thread2=new Thread(new ShellTicket02());Thread thread3=new Thread(new ShellTicket02());thread1.start();thread2.start();thread3.start();}
}//使用Runnable接口方式,使用synchronized实现线程同步
class ShellTicket02 implements Runnable{private int ticketNum=100;private boolean loop=true;public synchronized void sell(){//同步方法,在同一时刻,只能有一个线程来执行sell方法if (ticketNum<=0){System.out.println("售票结束!");loop=false;return;}//休眠50毫秒try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 "+ Thread.currentThread().getName()+" 售出一张票"+" 剩余票数="+(--ticketNum));}@Overridepublic void run() {while (loop){sell();}}
}
6.互斥锁⭐⭐⭐
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低
- 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
- 同步方法(静态的)的锁为当前类本身。
使用互斥锁来解决售票问题
案例碰到的问题:
这里演示的代码,把synchronized关键字去掉 加上 都不会多买???
互斥锁的注意事项:
1.同步方法如果没有使用static修饰:默认锁对象为this
2.如果方法使用static修饰,默认锁对象:当前类.class
3.实现的落地步骤:
-
需要先分析上锁的代码;
-
选择同步代码块或同步方法;
-
要求多个线程的锁对象为同一个即可!
7.线程的死锁
多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生.
应用案例:
妈妈:你先完成作业,才让你玩手机
小明:你先让我玩手机,我才完成作业.
package com.cly.syn;//模拟线程死锁
public class DeadLock {public static void main(String[] args) {DeadLockDemo A = new DeadLockDemo(true);A.setName("A线程");DeadLockDemo B = new DeadLockDemo(false);B.setName("B线程");A.start();B.start();}
}//线程
class DeadLockDemo extends Thread{static Object o1=new Object();//保证多个线程共享一个对象,这里使用了staticstatic Object o2=new Object();boolean flag;public DeadLockDemo(boolean flag){this.flag=flag;}@Overridepublic void run() {//下面代码的业务逻辑分析//1.如果flag为T,线程A 就会先得到o1对象锁,然后尝试去获取o2对象锁//2.如果线程A得不到 o2对象锁,就会Blocked//3.如果flag为F,线程B 就会先得到 o2对象锁,然后尝试去获取o1对象锁//4.如果线程B 得不到 o1对象锁,就会Blockedif (flag){synchronized (o1){//对象互斥锁,下面就是同步代码System.out.println(Thread.currentThread().getName()+"进入1");synchronized (o2){//这里获得li对象的监视权System.out.println(Thread.currentThread().getName()+"进入2");}}}else {synchronized (o2){System.out.println(Thread.currentThread().getName()+"进入3");synchronized (o1){//这里获得li对象的监视权System.out.println(Thread.currentThread().getName()+"进入4");}}}}
}
8.释放锁
释放锁的分析:
下面操作会释放锁
- 当前线程的同步方法、同步代码块执行结束案例:上厕所,完事出来
- 当前线程在同步代码块、同步方法中遇到break、return。案例:没有正常的完事,经理叫他修改bug,不得已出来
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束案例:没有正常的完事,发现忘带纸,不得已出来
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去
下面操作不会释放锁
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep().Thread.yield()方法暂停当前线程的执行,不会释放锁
案例:上厕所,太困了,在坑位上眯了一会 - 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。
提示;应尽量避免使用suspend()和resume()来控制线程,方法不再推荐使用
9.线程的章末练习题
习题一:
在main方法中启动两个线程;第1个线程循环随机打印100以内的整数;直到第2个线程从键盘读取了“Q”命令。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-le6MB38t-1627698593849)(C:\Users\86186\AppData\Roaming\Typora\typora-user-images\image-20210606210739214.png)]
package com.cly.threadExercise;import java.util.Locale;
import java.util.Scanner;public class ThreadExercise01 {public static void main(String[] args) {A a = new A();B b = new B(a);a.start();b.start();}
}//创建A线程类 第1个线程循环随机打印100以内的整数
class A extends Thread{private boolean loop=true;@Overridepublic void run() {//输出1-100数字while (loop) {System.out.println((int) (Math.random() * 100 + 1));try {//休眠1秒Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public void setLoop(boolean loop) {this.loop = loop;}
}//直到第2个线程从键盘读取了“Q”命令
class B extends Thread{private A a;private Scanner scanner=new Scanner(System.in);public B(A a) {//构造器中直接传入一个A类的对象this.a = a;}@Overridepublic void run() {while (true) {//接收到用户的输入System.out.println("请输入你的指令(Q)表示退出");char key = scanner.next().toUpperCase().charAt(0);if (key=='Q'){//以通知的方式结束A线程a.setLoop(false);System.out.println("B线程退出...");break;}}}
}
习题二
有2个用户分别从同一个卡上取钱(总额:10000)每次都取1000,当余额不足时,就不能取款了不能出现超取现象=》线程同步问题。
package com.cly.threadExercise;public class ThreadExercise02 {public static void main(String[] args) {T t = new T();Thread thread1 = new Thread(t);thread1.setName("t1");Thread thread2 = new Thread(t);thread2.setName("t2");thread1.start();thread2.start();}
}//编写取款的线程
//1.因为这里涉及到多个线程共享资源,所以我们使用实现Runnable的方式
class T implements Runnable{private int money=10000;@Overridepublic void run() {while (true){//解读//1.这里使用了synchronized实现了线程同步//2.当多个线程执行到这里时,就会去争夺this对象这个锁//3.哪个线程争夺到this对象锁,就执行synchronized代码块,执行完,会释放this对象锁//4.争夺不到的this锁的,就blocked,准备继续争夺//5.this对象锁是非公平锁synchronized (this) {////判断余额是否够if (money < 1000) {System.out.println("余额不足");break;}money -= 1000;System.out.println(Thread.currentThread().getName() + " 取出了1000 当前余额=" + money);}//休眠1秒try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
习题三
案例:下载图片
package com.cly.threadExercise;import org.apache.commons.io.FileUtils;import java.io.File;
import java.io.IOException;
import java.net.URL;//练习Thread,实现多线程同步下载图片
public class ThreadExercise03 extends Thread{private String url;//网络图片地址private String name;//保存的文件名public ThreadExercise03(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) {ThreadExercise03 t1=new ThreadExercise03("=1343933117,4053295934&fm=26&fmt=auto&gp=0.jpg","奥特曼1.jpg");ThreadExercise03 t2=new ThreadExercise03("=4093389400,558249165&fm=26&fmt=auto&gp=0.jpg","奥特曼2.jpg");ThreadExercise03 t3=new ThreadExercise03("=4183078104,522167207&fm=26&fmt=auto&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();}}
}
4.线程的5大状态
线程5大状态
线程5大状态详尽版
1.线程方法
- setPriority(int newPriority) 更改线程的优先级
- getPriority(int newPriority) 获取线程的优先级
- static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- void join() 等待线程终止
- static void yieId() 暂停当前正在执行的线程对象,并执行其他线程
- void interrupt() 中断线程
- boolean isAlive() 测试线程是否处于活动状态
2.停止线程
- 不推荐使用JDK提供的stop()、destroy()方法
- 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量 当flag=false,则终止线程运行
package com.cly.threadMethod;public class TestStop implements Runnable {private boolean flag =true;public static void main(String[] args) {TestStop testStop = new TestStop();new Thread(testStop).start();//启动子线程for (int i = 0; i < 100; i++) {System.out.println("main "+i);if (i==80){testStop.stop();}}}@Overridepublic void run() {int i=0;while (flag){System.out.println("run Thread "+(i++));}}public void stop(){this.flag=false;}
}
3.线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数; 1000毫秒=1秒
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态;
- sleep可以模拟网络延时,倒计时等。
- 每一个对象都有一个锁,sleep不会释放锁;
模拟网络延时
//模拟网络延时:就是为了放大问题的发生性//这个例子就会发现当多个线程操作一个对象的时候就会 出现线程不安全的现象
package com.cly.thread;//狂神版本
//多线程共同操作一个对象
//买火车票例子、 初识并发问题//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class ShellTicketKuangShen implements Runnable {//票数private int ticketNums=10;@Overridepublic void run() {while (true){if (ticketNums<=0) {break;}System.out.println(Thread.currentThread().getName()+"-->拿到了第"+(ticketNums--)+"张票");try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {ShellTicketKuangShen stks =new ShellTicketKuangShen();new Thread(stks,"小明").start();new Thread(stks,"小白").start();new Thread(stks,"小花").start();}
}
模拟倒计时
package com.cly.threadMethod;//模拟10秒倒计时
public class TestSleep{public static void main(String[] args) throws Exception{new TestSleep().tenDown();}public void tenDown() throws Exception{int num=10;while (true){Thread.sleep(1000);System.out.println(num--);if (num<=0){break;}}}
}
4.线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看CPU心情
package com.cly.threadMethod;//测试礼让线程
//礼让不一定成功,看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()+"线程停止执行");}
}
5.Join 线程插队
-
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
-
可以想象成插队
package com.cly.threadMethod;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 Exception {//启动子线程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);}}
}
6.观测线程状态
线程状态。线程可以处于以下状态之一:
-
NEw
尚未启动的线程处于此状态。 -
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
-
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 -
WAITING
正在等待另一个线程执行特定动作的线程处于此状态。 -
TIMED WA工TING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 -
TERMINATED
已退出的线程处于此状态。
package com.cly.threadMethod;//观察测试线程的状态
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);//NEW//观察启动后thread.start();state = thread.getState();System.out.println(state);//Runnablewhile (state != Thread.State.TERMINATED){//只要线程不终止,就一直输出状态Thread.sleep(100);state = thread.getState();//更新线程的状态System.out.println(state);//输出状态}//thread.start();死亡之后的线程是不能再次启动的}
}
7.线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程线程调度器按照优先级决定应该调度哪个线程来执行。
-
线程的优先级用数字表示,范围从1~10.
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5;
-
使用以下方式改变或获取优先级
- getPriority() . setPriority(int xxx)
优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.这都是看CPU的调度
package com.cly.threadMethod;//测试线程的优先级
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);//先设置优先级,再启动t1.start();t2.setPriority(1);t2.start();t3.setPriority(3);t3.start();t4.setPriority(Thread.MAX_PRIORITY);//MAX_PRIORITY=10t4.start();}
}class MyPriority implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());}
}
8.守护线程
- 线程分为用户线程(main())和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志,监控内存,垃圾回收等待…
package com.cly.threadMethod;//测试守护线程
//上帝守护你
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 = 0; i < 36500; i++) {System.out.println("你一生都开心的活着");}System.out.println("goodbye");}
}
5.线程同步
1.线程同步概括
线程同步发生在 多个线程操作同一个资源的时候。
并发:同一个对象被多个线程同时操作
比如:春节时候1万个人去抢3张火车票的时候,如果不进行线程同步,就会出现票数为负的情况,为解决这一问题,所以在涉及到多个线程操作同一个资源的时候会采取线程同步机制,当然仅仅有线程同步显然是不行的这个时候还要加上锁。
线程同步形成条件 : 队列+锁
需要用线程同步解决 线程不安全的问题!
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized ,当一个线程获得对象的排它锁﹐独占资源﹐其他线程必须等待,使用后释放锁即可。
存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁﹐释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.
要保证安全,性能就会降低;要保证性能,安全就会降低!鱼和熊掌不能兼得!
2.三大不安全案例
售票
package com.cly.thread;//使用多线程,模拟三个窗口同时售票
public class ShellTicket {public static void main(String[] args) {ShellTicket02 shellTicket1 = new ShellTicket02();ShellTicket02 shellTicket2 = new ShellTicket02();ShellTicket02 shellTicket3 = new ShellTicket02();new Thread(shellTicket1).start();new Thread(shellTicket2).start();new Thread(shellTicket3).start();}
}//使用Runnable接口方式
class ShellTicket02 implements Runnable{private static int ticketNum=100;private boolean loop = true;@Overridepublic void run() {while (loop){shell();}}//买票类public void shell(){//判断是否有票if (ticketNum<=0){System.out.println("售票结束!");loop=false;}//休眠50毫秒try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}//买票System.out.println("窗口 "+ Thread.currentThread().getName()+" 售出一张票"+" 剩余票数="+(--ticketNum));}
}线程不安全,会出现负数票的情况!> 不安全的 银行取钱```java
package com.cly.thread;//不安全的银行取钱
public class UnsafeBank {public static void main(String[] args) {//账户Account account = new Account(100,"结婚基金");Drawing you = new Drawing(account,50,"你");Drawing girlFriend = new Drawing(account,100,"girlFriend");you.start();girlFriend.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;}@Overridepublic void run() {//判断有没有钱if (account.money-drawingMoney < 0){System.out.println(Thread.currentThread().getName()+"钱不够,取不了");return;}//sleep可以放大问题的发生性try {Thread.sleep(500);} 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);}
}
线程不安全的集合
package com.cly.thread;import java.util.ArrayList;
import java.util.List;//线程不安全的集合
public class UnsafeList {public static void main(String[] args) throws InterruptedException {//往集合中添加一万条线程,如果线程是安全的那list.size()就是一万List<String> list = new ArrayList();for (int i = 0; i < 10000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());}).start();}Thread.sleep(1000);//休眠 放大问题的发生性System.out.println(list.size());}
}
6.同步方法
1.概述
- 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制﹐这套机制就是synchronized关键字,它包括两种用法︰synchronized方法和synchronized块.
public synchronized void method(int args){}
- synchronized方法控制对“对象”的访问﹐每个对象对应一把锁﹐每个synchronized方法都必须获得调用该方法的对象的锁才能执行﹐否则线程会阻塞,方法一旦执行﹐就独占该锁,直到该方法返回才释放锁﹐后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized将会影响效率
2.同步方法的弊端
方法里需要修改的内容才需要锁,锁的太多,浪费资源!
3.同步块
-
同步块:synchronized (Obj ) {}
-
Obj称之为同步监视器
- Obj可以是任何对象﹐但是推荐使用共享资源作为同步监视器。
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身,或者是class[反射中讲解]。
-
同步监视器的执行过程
- 第一个线程访问,锁定同步监视器﹐执行其中代码.
- 第二个线程访问,发现同步监视器被锁定,无法访问.
- 第一个线程访问完毕,解锁同步监视器.
- 第二个线程访问,发现同步监视器没有锁﹐然后锁定并访问
解决售票线程不安全的问题
package com.cly.thread;//使用多线程,模拟三个窗口同时售票
public class ShellTicket {public static void main(String[] args) {ShellTicket02 shellTicket1 = new ShellTicket02();new Thread(shellTicket1,"陈凯").start();new Thread(shellTicket1,"陈良宇").start();new Thread(shellTicket1,"王伟伦").start();}
}//使用Runnable接口方式
class ShellTicket02 implements Runnable{private int ticketNum=10;private boolean loop = true;@Overridepublic void run() {while (loop){shell();}}//买票类//synchronized 同步方法,锁的是thispublic synchronized void shell(){if (ticketNum<=0){System.out.println("售票结束!");loop=false;return;}//休眠50毫秒try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("窗口 "+ Thread.currentThread().getName()+" 售出一张票"+" 剩余票数="+(--ticketNum));}
}
解决银行取款线程不安全问题
package com.cly.thread;//不安全的银行取钱
public class UnsafeBank {public static void main(String[] args) {//账户Account account = new Account(100,"结婚基金");Drawing you = new Drawing(account,50,"你");Drawing girlFriend = new Drawing(account,100,"girlFriend");you.start();girlFriend.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;}@Overridepublic void run() {//切记锁的对象为变化的量,需要增删改的对象synchronized (account){//判断有没有钱if (account.money-drawingMoney < 0){System.out.println(Thread.currentThread().getName()+"钱不够,取不了");return;}//sleep可以放大问题的发生性try {Thread.sleep(500);} 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);}}
}
解决集合线程不安全问题
package com.cly.thread;import java.util.ArrayList;
import java.util.List;//线程不安全的集合
public class UnsafeList {public static void main(String[] args) throws InterruptedException {//往集合中添加一万条线程,如果线程是安全的那list.size()就是一万List<String> list = new ArrayList();for (int i = 0; i < 10000; i++) {new Thread(()->{//lambda表达式synchronized (list){list.add(Thread.currentThread().getName());}}).start();}Thread.sleep(1000);//休眠 放大问题的发生性System.out.println(list.size());}
}
测试JUC线程安全类型的集合
package com.cly.syn;import java.util.concurrent.CopyOnWriteArrayList;//测试juc安全类型的集合
public class TestJUC {public static void main(String[] args) throws InterruptedException {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList();for (int i = 0; i < 10000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());}).start();}Thread.sleep(100);System.out.println(list.size());}
}
4.死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
package com.cly.syn;//死锁:多个线程相互抱着对方需要的资源,然后形成僵持
public class DeadLock02 {public static void main(String[] args) {Makeup g1 = new Makeup(0,"灰姑娘");Makeup g2 = new Makeup(1,"白雪公主");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;//女人public Makeup(int choice,String girlName){this.choice=choice;this.girlName=girlName;}@Overridepublic void run() {//化妆makeup();}//化妆,互相持有对方的锁,就是需要拿到对方的资源private void makeup(){if (choice==0){synchronized (lipstick){//获得口红的锁System.out.println(girlName+"获得口红的锁");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (mirror){//1秒钟后获得镜子System.out.println(girlName+"获得镜子的锁");}}else {synchronized (mirror){//获得镜子的锁System.out.println(girlName+"获得镜子的锁");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}synchronized (lipstick){//1秒钟后获得口红的锁System.out.println(girlName+"获得口红的锁");}}}
}
如何避免产生死锁
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生
5.Lock(锁)
从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
package com.cly.syn;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();}
}class TestLock2 implements Runnable{private int ticket = 10;private boolean loop = true;//定义lock锁private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (loop){try{lock.lock();//加锁Thread.sleep(1000);if (ticket<=0){System.out.println("票买完了");loop = false;return;}System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+(--ticket)+"张票");} catch (InterruptedException e) {e.printStackTrace();} finally {//解锁lock.unlock();}}}
}
思考:为什么加完锁后就只执行第一个线程了?
sleep()没有释放锁,所以一直执行的是第一个线程。sleep总是抱着锁睡觉!
6.synchronized Lock的对比
-
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
-
Lock只有代码块锁,synchronized有代码块锁和方法锁
-
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
-
优先使用顺序:
Lock >同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
7.线程协作——生产者消费者模式
1.线程通信
应用场景:生产者和消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库﹐消费者将仓库中产品取走消费.
如果仓库中没有产品﹐则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止.
如果仓库中放有产品﹐则消费者可以将产品取走消费﹐否则停止消费并等待,直到仓库中再次放入产品为止.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGuOkphY-1627698593852)(C:\Users\86186\AppData\Roaming\Typora\typora-user-images\image-20210623101757200.png)]
2.线程通信-分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件.
- 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
- 对于消费者﹐在消费之后﹐要通知生产者已经结束消费﹐需要生产新的产品以供消费.
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized不能用来实现不同线程之间的消息传递(通信)
Java提供了几个方法解决线程之间的通信问题:
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify() | 唤醒一个处于等待状态的线程 |
notifyAll() | 唤醒同一个对象上所有调用wait()方法的线程,优先级越高的线程优先调度 |
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常lllegalMonitorStateException
解决方式1 ⭐
并发协作模型“生产者/消费者模式”—>管程法
- 生产者:负责生产数据的模块(可能是方法﹐对象﹐线程﹐进程);
- 消费者∶负责处理数据的模块(可能是方法﹐对象﹐线程﹐进程);
- 缓冲区:消费者不能直接使用生产者的数据﹐他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
package com.cly.syn;//测试生产者消费者模型-->利用缓冲区解决:管程法//生产者,消费者,产品,缓冲区
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{SynContainer container;public Productor(SynContainer container){this.container=container;}//生产方法@Overridepublic 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;}//消费@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{//需要一个容器大小Chicken[] chickens=new Chicken[10];//容器计数器int count=0;//生产者放入产品public synchronized void push(Chicken chicken) {//如果容器满了,就等待消费者消费while (count == chickens.length) {//生产等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果没有满,我们就需要丢入产品chickens[count] = chicken;count++;//可以通知消费者消费了this.notifyAll();}//消费者消费产品public synchronized Chicken pop() {//判断能否消费while (count == 0) {//消费者等待try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}//如果可以消费count--;Chicken chicken = chickens[count];//吃完了,通知生产者生产this.notifyAll();return chicken;}
}
解决方式2
并发协作模型“生产者/消费者模式”—>信号灯法
package com.cly.syn;//测试生产者消费者问题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;}@Overridepublic void run() {for (int i = 0; i < 20; 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;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {tv.watch();}}
}//产品-->节目
class TV{//演员表演,观众等待//观众观看,演员等待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;}
}
8.线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理(.……)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
JDK 5.0起提供了线程池相关API: ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
package com.cly.syn;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;//测试线程池
public class TestPool {public static void main(String[] args) {//1.创建服务,创建线程池//2.newFixedThreadPool 参数为线程池大小ExecutorService service = Executors.newFixedThreadPool(10);//执行service.execute(new MyThread());service.execute(new MyThread());service.execute(new MyThread());service.execute(new MyThread());service.execute(new MyThread());//3.关闭连接service.shutdown();}
}class MyThread implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}
}