目录
1.线程是什么?
2.线程安全(重点)
1.概念:
1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期
那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:
1.多个线程修改了同一个变量
2.线程在CPU调度上是抢占式执行的
3.没有保证线程的原子性
4.内存可见性
JAVA内存模型JVM是什么?
为什么要用JVM呢?
5.代码的有序性
我们来总结一下造成线程不安全的原因主要有哪些?
3.解决线程不安全问题(Synchronized关键字-监视器锁)
核心代码展示:对count自增的方法上锁
结果展示:
4.Synchronized关键字的注意事项
在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域
5.Synchronized的使用
1.线程是什么?
Thread是Java中的类,PCB是系统中真正的线程
调用start()方法才会真正的去系统中申请一个线程
线程的状态有哪些?
2.线程安全(重点)
1.概念:
如果多线程环境下代码运行的结果与单线程环境下相同,则说明这个线程是安全的
1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期
可以看见结果并不是我们所想的100000
那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:
1.多个线程修改了同一个变量
2.线程在CPU调度上是抢占式执行的
3.没有保证线程的原子性
原子性就是一段代码,要么全部执行成功,要么全部执行失败
我们知道,代码最终都会编译成CPU可以执行的指令,那么count++这一条指令其实对应着三步操作:
1.从内存把数据读到CPU
2.进行数据更新
3.把数据写回CPU
那么为什么不保证原子性就会出错呢?首先这和线程的抢占式执行密切相关
我们来举个例子:
分别有两个线程对count这个变量进行自增操作t1和t2,count初始值为0
假设t2先从内存把数据读到CPU,此时CPU被t1抢占了过去,t1开始从内存把数据读到CPU并对count进行了自增操作:count++,此时t2又把CPU抢占了回来,也对count进行了自增操作,count++,此时又回到t1,将数据写回CPU,count = 1,然后t2也进行了数据的写回,count= 1;现在我们可以发现问题了,由于没有保证原子性,导致多个线程之间进行了重复的自增操作,覆盖掉别的线程修改后的值,所以最终的结果不对
图解如下:
4.内存可见性
在多线程环境下,某一个线程修改了共享的变量,其他线程不能及时地接收到最新的值
JAVA内存模型JVM是什么?
1.主内存:指的是硬件的内存条,进程在启动的时候会申请一些资源,包括内存资源,用来保存所有的变量
2.工作内存:指的是线程独有的内存空间,它们之间不能相互访问,起到了线程之间内存隔离的作用
3.JVM规定,一个线程在修改某个变量的时候,必须把这个变量从主内存加载到自己的工作内存,修改完成后在返回主内存
为什么要用JVM呢?
Java是一个跨平台语言,而JVM把不同的计算设备和操作系统对内存的管理做了一个统一的封装
5.代码的有序性
是指在编译过程中,JVM调用本地接口,CPU执行指令的过程中,指令的有序性
这种通过打乱顺序,把不相关的代码或指令重新排列,从而提高程序效率的方式,在程序中叫做指令重排序
在单线程中重排序之后的结果百分百正确,但在多线程环境下就未必了
我们来总结一下造成线程不安全的原因主要有哪些?
3.解决线程不安全问题(Synchronized关键字-监视器锁)
对当前执行的代码加锁
当某一个线程要执行这个方法时,就先获取锁,获取到锁之后再去执行代码,其他线程也需要执行这个方法时,也需要获取锁,但是已经有线程持有锁时,他就需要等待,等到上一个线程释放这把锁(注意分辨锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系)之后才有可能竞争到CPU的调度
因为线程时抢占式执行,CPU对调度是随机的,并不是先阻塞等待的线程就一定会先拿到锁
我们发现上了锁之后的代码变成了单线程,这也是不可避免的
所以在获取数据时,我们用多线程来提高效率
在修改数据时,用Synchronized上锁来保证安全
核心代码展示:对count自增的方法上锁
public synchronized void increase() {
count++;
}
结果展示:
可以看见上锁之后结果正确了,说明Synchronized解决了原子性的问题,并且通过把并行操作变成了串行操作,也保证了内存的可见性
但是Synchronized并不能保证有序性
4.Synchronized关键字的注意事项
1.从并行到串行:首先要保证正确,然后才考虑效率
2.加锁与CPU调度:锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系
3.加锁的范围:比如加在for循环外就与串行是一模一样的,但加在外面两个for循环之间就是并发执行的,这样写要比两个for循环分别执行要快很多,这个要跟及实际情况考虑,加锁的范围越大称锁的粒度越大,反之越小
4.给代码块加锁:Synchronized可以用来修饰代码块,此时需要传入一个参数,用来表明锁的对象
5.锁对象:竞争的锁都必须是针对同一个对象的,可以自定义一个单独的对象表示锁(可以是JAVA中的任何对象),也可以使用this,多线程并不关心锁的对象是哪个,只关心他们是否竞争同一把锁,也就是是否是同一个对象,只有同一个对象才会产生锁的竞争
在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域
- mark word 与 类型指针(_class) 这两部分统一称为对象头 主要描述了当前是哪个线程获取到的资源,记录的是线程对象信息,当线程释放所资源的时候就会把线程对象信息清除掉,其他的线程就可以继续获得锁资源
- 实例数据 保存的就是类中的属性
- 对其填充 每一个类对象占用的字节数必须是八字节的整数倍
5.Synchronized的使用