多线程
线程、进程、多线程
普通方法调用和多线程
Process与Thread
- 说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位
- 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的的单位。
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局
本章核心概念
- 线程就是独立的执行路径
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如cpu调度时间,并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建
三种创建方式
继承Thread类
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
具体可以查看帮助文档
/*
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
总结:注意,线程开启不一定立即执行,由CPU调度执行
*/
public class Demo03 extends Thread{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("副进程");
}
}
public static void main(String[] args) {
//创建一个线程对象
Demo03 thread2 = new Demo03();
//调用start()方法开启线程
thread2.start();
for (int i = 0; i < 2000; i++) {
System.out.println("主进程");
}
}
}
/*
...
副进程
副进程
主进程
副进程
副进程
副进程
副进程
副进程
...
*/
案例:多线程同步下载图片
//继承Thread,实现多线程同步下载图片
public class Demo04 extends Thread{
private String url; //网络图片地址
private String name; //保存的文件名
public Demo04(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
}
public static void main(String[] args) {
Demo04 t1 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "1.png");
Demo04 t2 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "2.png");
Demo04 t3 = new Demo04("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "3.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));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO异常,downloader方法出现问题");
}
}
}
/*
下载了文件名为:2.png
下载了文件名为:1.png
下载了文件名为:3.png
*/
实现Runnable
- 定义MyRunnable类实现Runnable接口
- 实现run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
推荐使用Runnable对象,因为Java单继承的局限性,实际上Tread类中实现的就是Runnable接口,本质上是一样的,区别在于接口和类
public class Demo05 implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
System.out.println("副进程");
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
Demo05 thread2 = new Demo05();
//创建线程对象,调用start()方法开启线程
new Thread(thread2).start();
for (int i = 0; i < 2000; i++) {
System.out.println("主进程");
}
}
}
小结
- 继承Thread类
- 子类继承Thread类具备多线程能力
- 启动线程:子类对像.start()
- 不建议使用:避免OOP单继承局限性
- 实现Runnable接口
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start()
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
买票案例
//多个线程同时操作同一给对象
//买火车票的例子
//发现问题:多个线程操作同一个资源的况下,线程不安全,数据紊乱
public class Demo06 implements Runnable{
//票数
private int ticketNums = 10;
@Override
public void run() {
while (true) {
if (ticketNums <= 0) {
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- +"张票");
}
}
public static void main(String[] args) {
Demo06 t = new Demo06();
new Thread(t,"张三").start();
new Thread(t,"李四").start();
new Thread(t,"王五").start();
}
}
/*
李四拿到了第10张票
张三拿到了第8张票
王五拿到了第9张票
李四拿到了第6张票
王五拿到了第5张票
张三拿到了第7张票
李四拿到了第2张票
王五拿到了第4张票
张三拿到了第3张票
王五拿到了第1张票
张三拿到了第0张票
李四拿到了第-1张票
*/
龟兔赛跑
public class Demo07 implements Runnable{
private static String winner;
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
//睡觉和走一步用时
int time = 0;
if (Thread.currentThread().getName().equals("兔子") && i % 4 == 0) {
time = 4000;
} else if (Thread.currentThread().getName().equals("乌龟")){
time = 500;
}
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!isWin(i)) {
System.out.println(Thread.currentThread().getName() + "跑了" + i + "步了");
} else {
break;
}
}
}
public static boolean isWin(int steps){
if (winner != null) {
return true;
} else {
if (steps >= 10) {
winner = Thread.currentThread().getName();
System.out.println("Winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Demo07 thread = new Demo07();
new Thread(thread, "兔子").start();
new Thread(thread, "乌龟").start();
}
}
/*
兔子跑了1步了
兔子跑了2步了
兔子跑了3步了
乌龟跑了1步了
乌龟跑了2步了
乌龟跑了3步了
乌龟跑了4步了
乌龟跑了5步了
乌龟跑了6步了
乌龟跑了7步了
兔子跑了4步了
兔子跑了5步了
兔子跑了6步了
兔子跑了7步了
乌龟跑了8步了
乌龟跑了9步了
Winner is 乌龟
*/
实现Callable接口(了解即可)
- 实现Callable接口,需要返回值类型
- 重写cal方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors .newFixedThreadPool(1);
- 提交执行:Future
result1 = ser.submit(t1); - 获取结果:boolean r1=result1.get()
- 关闭服务:ser.shutdownNow();
//线程创建方式三:实现callable接口
/*
callable的好处
1.可以定义返回催
2.可以抛出异常。
*/
public class Demo08 implements Callable<Boolean> {//返回值类型自己选择
private String url; //网络图片地址
private String name; //保存的文件名
public Demo08(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() {
WebDownloader2 webDownloader = new WebDownloader2();
webDownloader.downloader(url, name);
System.out.println("下载了文件名为:" + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Demo08 t1 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "1.png");
Demo08 t2 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "2.png");
Demo08 t3 = new Demo08("https://images.cnblogs.com/cnblogs_com/guangzan/1894231/o_230626114104_spring.png", "3.png");
//创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(3);//线程池
//提交执行:
Future<Boolean> result1 = ser.submit(t1);
Future<Boolean> result2 = ser.submit(t2);
Future<Boolean> result3 = ser.submit(t3);
//获取结果:
boolean r1 = result1.get();
boolean r2 = result2.get();
boolean r3 = result3.get();
System.out.println(r1);
System.out.println(r2);
System.out.println(r3);
//关闭服务:
ser.shutdownNow();
}
}
//下载器
class WebDownloader2{
//下载方法
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方法出现问题");
}
}
}
/*
下载了文件名为:2.png
下载了文件名为:3.png
下载了文件名为:1.png
true
true
true
*/
静态代理模式
//静态代理模式总结:
//真实对象和代理对象都要实现同一个接口
//代理对象要代理真实角色
//好处:
//代理对象可以做很多真实对象做不了的事
//真实对象专注做自己的事
public class Demo09 {
public static void main(String[] args) {
//可以用匿名类实现,线程,属于实现Runnable接口方式
// new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println("我爱你");
// }
// }).start();
new Thread(() -> System.out.println("我爱你")).start();
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 before() {
System.out.println("布置婚礼现场");
}
private void after() {
System.out.println("婚后交尾款");
}
}
/*
我爱你
布置婚礼现场
我要结婚了,好开心
婚后交尾款
*/
Lambda表达式
为什么要使用lambda表达式
-
避免匿名内部类定义过多
-
可以让你的代码看起来很简洁
-
去掉了一堆没有意义的代码,只留下核心的逻辑。
-
其实质属于函数式编程的概念
理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在
函数式接口的定义:
- 任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。
- 对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。
Runnable接口就是一个函数式接口
总结:
- lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹
- 前提是接口为函数式接口
- 多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号,
public class Demo10 {
//3.静态内部类
static class lambdaCl2 implements lambdaIf{
@Override
public void like(int extent) {
System.out.println("I like lambda2 with " + extent);
}
}
public static void main(String[] args) {
//用传统类实现
new lambdaCl1().like(1);
//用静态内部类实现
new lambdaCl2().like(2);
//4.局部内部类
class lambdaCl3 implements lambdaIf{
@Override
public void like(int extent) {
System.out.println("I like lambda3 with " + extent);
}
}
new lambdaCl3().like(3);
//5.用匿名内部类实现
new lambdaIf() {
@Override
public void like(int extent) {
System.out.println("I like lambda4 with " + extent);
}
}.like(4);
//6.lambda表达式
lambdaIf like = null;
like = (e) -> {System.out.println("I like lambda5 with " + e);};
like.like(5);
//7.进一步简化
like = e -> System.out.println("I like lambda6 with " + e);
like.like(6);
/*
总结:
lambda表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹。
前提是接口为函数式接口
多个参数也可以去掉参数类型,要去掉就都去掉,必须加上括号,
*/
}
}
//1.定义一个函数式接口
interface lambdaIf{
void like(int extent);
}
//2.实现类
class lambdaCl1 implements lambdaIf{
@Override
public void like(int extent) {
System.out.println("I like lambda1 with " + extent);
}
}
/*
I like lambda1 with 1
I like lambda2 with 2
I like lambda3 with 3
I like lambda4 with 4
I like lambda5 with 5
I like lambda6 with 6
*/
线程状态
线程方法
停止线程
- 不推荐使用JDK提供的 stop()、
destroy()方法。【已废弃】 - 推荐线程自己停止下来
- 建议使用一个标志位进行终止变量
当flag=false,则终止线程运行。
//测试stop
//1.建议线程正常停止--->利用次数,不建议死循环
//2.建议使用标志位--->设置一个标志位
//3.不要使用stop或者destroy等过时或者JDK不建议使用的方法
public class Demo11 implements Runnable{
//1.设置一个标识位
private boolean flag = true;
@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) {
Demo11 t = new Demo11();
new Thread(t).start();
for (int i = 0; i <= 5; i++) {
System.out.println("main" + i);
if (i == 4) {
//调用stop方法切换标识位,让线程停止
t.stop();
System.out.println("线程停止了");
}
}
}
}
/*
main0
run.....Thread0
main1
run.....Thread1
main2
run.....Thread2
main3
run.....Thread3
main4
run.....Thread4
线程停止了
main5
*/
线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数;
- sleep存在异常InterruptedException;
- sleep时间达到后线程进入就绪状态,sleep可以模拟网络延时,倒计时等。
- 每一个对象都有一个锁,sleep不会释放锁
应用案例:
- 模拟网络延时:放大问题的发生性
- 倒计时
- 打印当前时间
//倒计时模拟,打印时间
public class Demo12 {
public static void main(String[] args) {
// timeDown(1000);
getTime(10);
}
//模拟倒计时
public static void timeDown(int time) {
while (0 != time--) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(time);
}
}
//打印当前系统时间
public static void getTime(int num) {
Date startTime = new Date(System.currentTimeMillis());//获取系统当前时间
while (0 != num--) {
try {
Thread.sleep(1000);
startTime = new Date(System.currentTimeMillis());
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
线程礼让
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让cpu重新调度,礼让不一定成功!看CPU心情
public class Demo13 {
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() + "线程完成执行");
}
}
线程强制执行_join
- Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以想象成插队
public class Demo14 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("join..." + i);
}
}
public static void main(String[] args) throws InterruptedException {
Demo14 th = new Demo14();
Thread thread = new Thread(th);
thread.start();
for (int i = 0; i < 15; i++) {
if (i == 10) {
thread.join();
}
System.out.println("main..." + i);
}
}
}
/*
main...0
main...1
main...2
main...3
main...4
join...0
main...5
main...6
main...7
main...8
main...9
join...1
join...2
join...3
join...4
join...5
join...6
join...7
join...8
join...9
main...10
main...11
main...12
main...13
main...14
*/
线程状态观测
-
线程状态观测
线程状态。线程可以处于以下状态之一:
- NEW
尚未启动的线程处于此状态。 - RUNNABLE
在Java虚拟机中执行的线程处于此状态。 - BLOCKED
被阻塞等待监视器锁定的线程处于此状态。 - WAITING
正在等待另一个线程执行特定动作的线程处于此状态, - TIMED WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 - TERMINATED
已退出的线程处于此状态。
- NEW
一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态
//观察测试线程的状态
public class Demo15 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("over");
});
//观察状况
Thread.State state = thread.getState();
System.out.println(state);//NEW
//观察启动后
thread.start();
state = thread.getState();
System.out.println(state);//Run
while (state != Thread.State.TERMINATED) {//只要线程不终止,就一直输出状态
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
state = thread.getState();
System.out.println(state);
}
//终止后就不能再次启动了,即线程中断或者结束,一旦进入死亡状态,就不能再次启动,否则报错
// thread.start();
}
}
/*
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
over
TERMINATED
*/
线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
-
线程的优先级用数字表示,范围从1~10。
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY =10;
- Thread.NORM_PRIORITY = 5;
-
使用以下方式改变或获取优先级
- getPriority(),setPriority(int xxx)
-
优先级的设定建议在start()调度前,先设置优先级在启动
-
优先级低只是意味着获得调度的概率低。并不是优先级低就不会被调用了。这都是看CPU的调度
//测试线程的优先级
public class Demo16 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
MyPriorty myPriorty = new MyPriorty();
Thread thread5 = new Thread(myPriorty, "thread5");
Thread thread1 = new Thread(myPriorty, "thread1");
Thread thread3 = new Thread(myPriorty, "thread3");
Thread thread7 = new Thread(myPriorty, "thread7");
Thread thread10 = new Thread(myPriorty, "thread10");
thread5.start();
thread1.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread3.setPriority(3);
thread3.start();
thread7.setPriority(7);
thread7.start();
thread10.setPriority(Thread.MAX_PRIORITY);
thread10.start();
}
}
class MyPriorty implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
}
}
/*
main-->5
thread5-->5
thread10-->10
thread7-->7
thread3-->3
thread1-->1
*/
守护(daemon)线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕,如main()
- 虚拟机不用等待守护线程执行完毕,如gc()
- 如,后台记录操作日志,监控内存,垃圾回收等待,
public class Demo17 {
public static void main(String[] args) {
Lucy lucy = new Lucy();
Me me = new Me();
Thread thread1 = new Thread(lucy);
thread1.setDaemon(true);//默认是false表示是用户线程,正常的线程都是用户线程..
Thread thread2 = new Thread(me);
thread1.start();//守护线程,虚拟机不用等待其执行完毕。
thread2.start();//虚拟机要确保用户线程执行完毕
}
}
//好运和自信
class Lucy implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("和好运自信相伴");
}
}
}
//我
class Me implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++) {
System.out.println("一生都很开心的活着");
}
System.out.println("=====goodbye! world!=====");
}
}
线程同步
多个线程操作同一个资源
并发:同一个对象被多个线程在同一个时间间隔内操作
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象这时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
形成条件:队列+锁
- 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待使用后释放锁即可。存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题
三大不安全案例
-
不安全买票,具体可查看上面线程创建中的小结案例
-
不安全的取钱
//不安全的取钱 //两个人去银行取钱,账户 public class UnsafeBank { public static void main(String[] args) { Account account = new Account(15, "888"); Drawing you = new Drawing(account, 10, "我"); Drawing girlFriend = new Drawing(account, 10, "girlFriend"); you.start(); girlFriend.start(); } } class Account{ int money;//余额 String name;//卡名 public Account(int money, String name) { this.money = money; this.name = name; } } //模拟取款 //《这里选择继承Threa的类是因为只用到了一种线程,》当然可以实例化多个线程 class Drawing extends Thread{ Account account;//账号 //取了多少钱 int drawingMoney; //手里还有多少钱 int nowMoney; public Drawing(Account account, int drawingMoney, String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; } //取钱 @Override public void run() { if (account.money - drawingMoney < 0) { System.out.println(Thread.currentThread().getName() + "余额不足,操作失败"); return; } try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } //更新卡内余额 account.money -= drawingMoney; //你手里的钱 nowMoney += drawingMoney; System.out.println(account.name + "余额为:" + account.money); System.out.println(this.getName() + "手里的钱:" + nowMoney); } } /* 888余额为:-5 888余额为:-5 girlFriend手里的钱:10 我手里的钱:10 */
-
线程不安全的集合
//线程不安全的集合 public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(() -> { list.add(Thread.currentThread().getName()); }).start(); } System.out.println(list.size()); } } /*线程直接可能冲突了,导致没有有效的添加到集合 9720 */
同步方法
-
由于我们可以通过 private 关键字来保让数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法synchronized方法和synchronized块
同步方法:public synchronized void method(int args){}
-
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
-
同步方法弊端
- 若将一个大的方法申明为synchronized 将会影响效率
- 方法里面需要修改的内容才需要锁,锁的太多,浪费资源
同步块
- 同步块:synchronized(Obj) {}
- Obj称之为 同步监视器
- Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是 class[反射中讲解 ]
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
三大不安全案例的解决办法
-
买票
public class Demo06 implements Runnable{ //票数 private int ticketNums = 10; private boolean flag = true; @Override public void run() { while (true) { buy(); if (!flag) break; } } //synchronized同步方法,锁的是this,就是锁的这个Demo6对象 public synchronized void buy() { if (ticketNums <= 0) { flag = false; return; } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- +"张票"); } public static void main(String[] args) { Demo06 t = new Demo06(); new Thread(t,"张三").start(); new Thread(t,"李四").start(); new Thread(t,"王五").start(); } }
-
取钱
public class UnsafeBank { public static void main(String[] args) { Account account = new Account(15, "888"); Drawing you = new Drawing(account, 10, "我"); Drawing girlFriend = new Drawing(account, 10, "girlFriend"); you.start(); girlFriend.start(); } } class Account{ int money;//余额 String name;//卡名 public Account(int money, String name) { this.money = money; this.name = name; } } //模拟取款 //《这里选择继承Threa的类是因为只用到了一种线程,》当然可以实例化多个线程 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; } //取钱 /* 用synchronized修饰run方法,本身锁的就是Drawing类, 然而在main方法中临界资源是账号,而不是银行Drawing,new了两个不同的Drawing, 现实生活中也可以看出,即便在两个银行网点,同时取一个账号的钱,也不会出错 */ @Override public void run() { //这里锁的更具体,限制共享资源,用同步块 synchronized (account) { if (account.money - drawingMoney < 0) { System.out.println(Thread.currentThread().getName() + "余额不足,操作失败"); return; } try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } //更新卡内余额 account.money -= drawingMoney; //你手里的钱 nowMoney += drawingMoney; System.out.println(account.name + "余额为:" + account.money); System.out.println(this.getName() + "手里的钱:" + nowMoney); } } }
-
集合
public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { new Thread(() -> { synchronized (list) { list.add(Thread.currentThread().getName()); } }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(list.size()); } }
小结:
- 锁的范围要具体,要精确否则失效,锁的对象一定是增删改查的资源
- 在第三个案例中,如果没有休眠时间,即使应用同步后也会出现错误,可能是具体创建需要一些时间,也可以看出休眠的是主线程,这不是同步角度的原因
CopyOnWriteArrayList
//测试JUC安全类型的集合
public class Demo21 {
public static void main(String[] args) {
//JUC提供了一个本身针对这种不安全情形的集合
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(list.size());
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
public class Demo22 {
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; //使用化妆品的人
Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//化妆,相互持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException{
//产生死锁,死锁四个条件:请求保持,不可剥夺,循环等待,互斥访问
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
}
}
} else {
synchronized (mirror) {
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(1000);
synchronized (lipstick) {
System.out.println(this.girlName + "获得口红的锁");
}
}
}
//没有死锁
// if (choice == 0) {
// synchronized (lipstick) {
// System.out.println(this.girlName + "获得口红的锁");
// Thread.sleep(1000);
// }
// synchronized (mirror) {
// System.out.println(this.girlName + "获得镜子的锁");
// }
// } else {
// synchronized (mirror) {
// System.out.println(this.girlName + "获得镜子的锁");
// Thread.sleep(1000);
// }
// synchronized (lipstick) {
// System.out.println(this.girlName + "获得口红的锁");
// }
// }
}
}
Lock锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized相同的并发性和内存语在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:
- Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
生产者消费者问题
问题分析
作为一个408人,这个问题在熟悉不过了
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
- 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized 不能用来实现不同线程之间的消息传递(通信)
线程通信
-
Java提供了几个方法解决线程之间的通信问题
-
个人认为这里就是PV操作和wait(),singal()的升级版本
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException
解决方式
- 并发协作模型“生产者/消费者模式”--->管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
- 生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据
- 并发协作模型“生产者/消费者模式”--->信号灯法
管程法
//测试:生产者消费者模型-->利用缓冲区解决:管程法
//生产者, 消费者, 产品, 缓冲区
public class Democracy25 {
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;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
container.push((new Chicken(i)));
System.out.println("生产了" + i + "只鸡");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//消费者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
System.out.println("消费了" + container.pop().id + "只鸡");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//产品
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) throws InterruptedException {
//如果容器满了,那就等待消费者消费
if (count >= chickens.length) {
this.wait();
}
//如果没有满, 我们就需要丢入产品
chickens[count] = chicken;
count++;
//通知消费者消费了
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop() throws InterruptedException {
//判断能否消费
if (count <= 0) {
//等待生产者生产,消费者等待
this.wait();
}
//可以消费了
count--;
Chicken chicken = chickens[count];
//通知生产者可以生产了
this.notifyAll();
return chicken;
}
}
信号灯
public class Demo26 {
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 < 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;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//产品-->节目
class TV{
//演员表演,观众等待
//观众观看,演员等待
String voice;//节目
boolean flag = true;//信号灯,true没界面,false有界面
//表演
public synchronized void play(String voice) {
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
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) {
throw new RuntimeException(e);
}
}
System.out.println("观看了:" + voice);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
使用线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理(..)
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- JDK 5.0起提供了线程池相关API:ExecutorService 和Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable - void shutdown():关闭连接池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class Demo27 {
public static void main(String[] args) {
//1.创建服务,创建线程池
//newFixedThreadPool参数为:线程池大小,j静态方法,不用new
ExecutorService service = Executors.newFixedThreadPool(10);
//执行
service.execute(new Mythread());
service.execute(new Mythread());
service.execute(new Mythread());
service.execute(new Mythread());
//2.关闭链接
service.shutdown();
}
}
class Mythread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
/*
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
*/