目录
题外话
正题
1.线程调度是随机的
2.修改共享数据
知识点
线程同步机制
线程异步机制
举例说明
synchronized()
知识点
举例说明
举例代码详解
死锁
举个例子:
代码
小结
题外话
这两天忽冷忽热的感冒了,昨天状态特别不好断更了一天,今天继续加油!
我会把所有知识点放在每个小标题最前面,省的大家东找一个西找一个,大家可以先略微看看知识点,把文章看一遍回过头再看知识点,就有种豁然开朗的感觉
然后我比较喜欢用费曼学习法,大家有兴趣可以查查
这里简单说下费曼学习法,大家应该也或多或少知道这个,大家在跟着视频学习的时候,可以附和视频中的老师,或者试着对话,再或者遇到知识点,可以用给别人讲课的方式,把知识点说出来,这样确实很温故知新
就是在宿舍自言自语,自我对话会被别人当成傻子,怕社死的还是慎重考虑
我觉得做笔记也很关键,我从开始学javase,就开始把所有java知识点都放在了一个word文档里
复习起来也很方便,不需要再去翻找视频什么的费时费力,(如下图)
正题
我们先说说引发线程安全的因素
1.线程调度是随机的
大家都知道,JVM中线程是抢占式调度,所有线程去抢占cup的资源,线程抢的资源越多,执行该线程的速度也会越快
我们要做的是在写代码的时候,使多线程让任意执行顺序下都可以顺利执行
2.修改共享数据
知识点
一.先提出两个概念
我们把线程排队执行(线程一个一个执行),叫做;线程同步机制
而抢占式执行(多个线程一起执行),叫做;线程异步机制
先说说线程同步机制和线程异步机制的优缺点
线程同步机制
优点:可以保证数据安全问题
缺点:因为排队执行,导致效率变低,而且利用synchronized也会产生一些问题(如死锁等等)
线程异步机制
优点:多线程抢占式执行,效率很高
缺点:因为线程各自独立运行,互不影响,修改共享数据会产生数据错乱问题
二.再说说在java中什么数据会产生修改共享数据的问题
1.一般情况下:局部变量不存在线程安全问题。(尤其是基本数据类型不存在线程安全问题,因为它们在栈中,栈不共享数据,如果是引用数据类型,就另说了)
2.实例变量可能存在线程安全问题,实例变量在堆中,堆是多线程共享的。
3.静态变量也可能存在线程安全问题,静态变量在堆中,堆是多线程共享的
举例说明
先让大家更清晰理解修改共享数据会产生什么问题
比如一个银行账户被张三和李四两个人使用,两个人都把这张卡绑定支付,两个人都想取钱出来花,(如下图)
首先,银行会各自读取银行卡余额,然后再各自取钱,按照JVM中线程抢占式调用资源,
我们不知道张三取钱的时候,李四会不会才进行到获取银行卡余额这种情况,如果发生了这种情况,就会出现银行卡余额数据异常(当然,银行肯定非常安全,这种问题早就被解决了)
上述问题就是修改共享数据
所以面对这种问题,我们并不需要抢占式执行,而是需要排队执行,给张三取完钱,再去执行李四取钱的操作.
我们就需要synchronized()
synchronized()
知识点
1.线程同步的本质是:线程排队执行就是同步机制。
2. 语法格式:
synchronized(必须是需要排队的这几个线程共享的对象){
需要同步的代码
}
“必须是需要排队的这几个线程其享的对象”这个必须选对,
这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
3.可以给一块代码加锁,也可以对实例方法,静态方法加锁
加锁并不是一个很好的选择
加锁优缺点
优点:防止修改共享数据造成数据错乱
缺点:1.加锁的位置需要很慎重考虑
因为有些线程需要修改共享数据,而有些线程不需要修改共享数据,如果把这种数据全部加锁,就会产生效率问题
2.加锁占用资源比较大
3.当我们写的代码越来越多,如果滥用锁的话会产生死锁等等一系列问题,导致效率很低
举例说明
synchronized()就是锁,大家可以理解为上厕所
一套屋子只有一个厕所,有人进去了,其他人只能等他出来才能进去
通过一段代码,让大家更清晰理解上锁会怎么样
这里代码没有上锁,让大家看看修改共享数据会造成什么后果
举例代码详解
以下是实现两个线程各自执行一万次count累加
public class ThreadDemo14 { static int count=0; public void add() { count++; } public static void main(String[] args) throws InterruptedException { ThreadDemo14 t=new ThreadDemo14(); Thread t1=new Thread(()->{ for (int i = 0; i < 10000; i++) { t.add(); } }); Thread t2=new Thread(()->{ for (int i = 0; i < 10000; i++) { t.add(); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(count); } } 正常来说我们理想状态打印出的count应该是20000,但是修改共享数据会发生数据异常(如下图) 这次大家看好我加锁后的代码
public class ThreadDemo14 { static int count=0; //直接给add方法加锁 public synchronized void add() { count++; } public static void main(String[] args) throws InterruptedException { ThreadDemo14 t=new ThreadDemo14(); Thread t1=new Thread(()->{ for (int i = 0; i < 10000; i++) { t.add(); } }); Thread t2=new Thread(()->{ for (int i = 0; i < 10000; i++) { t.add(); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(count); } }
而这次运行结果和咱们理想结果一样
我是在add方法上直接加锁了,当然也可以在方法里加锁,synronized(this){代码块}这种形式即可
死锁
我们再说一下死锁的问题,其实死锁就是互相矛盾
举个例子:
小明在厕所1,厕所1只能洗手,一个厕所只能进入一个人
小红在厕所2,厕所2只能蹲坑,一个厕所只能进入一个人
小明洗完手想去蹲坑,而小红蹲完坑想去洗手,但是一个厕所只能进入一个人,这样他们永远不能走出自己所在的厕所,如下图
让大家看看代码,或许就明白了!
代码
public class ThreadDemo15 { public static void main(String[] args) { //创建两个Oberject对象o1,o2 Object o1=new Object(); Object o2=new Object(); //建立两个线程t1,t2 Thread t1=new MyThread01(o1,o2); Thread t2=new MyThread01(o1,o2); //t1命名t1,t2命名t2 t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } } //创建类继承Thread class MyThread01 extends Thread { //创建私密变量o1,o2 private Object o1; private Object o2; //创建构造方法 public MyThread01(Object o1,Object o2) { this.o1=o1; this.o2=o2; } //重写run方法 @Override public void run() { //如果线程名字等于t1 if ("t1".equals(Thread.currentThread().getName())) { //进入o1锁 synchronized (o1) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("解开了"); //进入o2锁 synchronized (o2) { } } } //如果线程名字和t2一样 else if ("t2".equals(Thread.currentThread().getName())) { //进入o2锁 synchronized (o2) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("解开了"); //进入o1锁 synchronized (o1) { } } } } }
当我们运行的时候就会发现,t1和t2都进入了锁中,但是永远不会结束进程,永远不会报错,就这样一直锁着,如下图
所以说还是需要好好掌握一下synchronized()的使用方式
小结
写完这篇有点晚了,但今天收获还是很大的,喜欢的朋友多多支持一下吧!