一:老规矩我们想一想、为什么会有多线程这个东西?他会影响什么?
一个项目在多线程环境下(其实就是多个请求),当n个线程同时的操作(增删改)一份共享资源时,因为线程调度的不确定性,可能引起资源状态前后的不一致,这种问题就是线程安全问题
二:实际开发工程中如何判断是否有线程安全的问题?
要能够明确 竞态资源是否存在,是什么资源?
例如:三个客户端同时调用同一个controller的同一个方法,对tomcat来说这三个客户就是三个线程,里面的controller在默认情况下就是单列的,这三个线程会调用同一个方法method()。抛开数据不谈这个调方法不是线程安全问题,但是假设方法里面有一个i++的操作就是有线程安全的问题这个 i 就是三个线程的竞态资源,在数据库如果仅仅只是查询就没有线程安全问题,但是有查询有修改啥的操作就有线程安全问题,只不过是数据库把线程安全问题给解决了。
三: 线程安全的引发原因
引起线程不安全的原因在于:
1、可见性
2、原子性
3、有序性任何一个业务,如果没有保证以上
注意:3个特性中的任意一个特性,就有线程安全的问题
1)可见性
一个线程对一份资源的修改,对其他线程必须立即可见
当前有一个主存,假设现在有三个线程,这三个线程不是直接操作主存的而是把主存的数据拿到每个线程自己的工作内存去修改,在返回给主存并且通知其他线程的工作内存(这个操作会自动完成)CPU相当于在刷存储时间是比较快的,但是有很少的概率会出现一个线程修改数据其他线程是不知道的。导致其他线程还在改原先的数据。
2)原子性(实际开发过程中大概率都是因为原子性的问题)
一个操作是一个整体不可分割的
比如:i = 10;//原子性 当10赋值给 i 的时候,别的线程是一定不能抢CPU的。
i++; //非原子性 x=i+1; | i=x;
j = 10.0;//在32位的系统上非原子,(double类型8个字节占64位)在64位的系统上是原子性
注意:两个原子性的操作放在一起,就不是原子性的了(两个原子性中间可能会被打断)
三:有序性
指令重排:cpu为了优化考虑,可能会打乱代码的执行顺序。
指令重排的保证:cpu可以保证代码在单线程情况下,指令重排后,不影响原程序的执行结果。
其实就是代码逻辑没有问题,一旦发生指令重排代码顺序换了,导致代码的逻辑错误
四:如何解决线程安全的问题
1)使用volatile关键字
volatile关键字可以保证变量的可见性,以及局部有序性(如果只写了一个变量其他变量还是会重排的)。
volatile不能保证原子性。
使用volatile关键字修饰的变量,一旦被某个线程修改,其他线程是立刻可见的。
2)使用JUC(java.util.concurrent)包下的AtomicXxxx类来保证原子性
AtomicXxxx类里面的方法都是原子操作
3)使用AtomicXxxx类提供的cas方法
CAS(compare and swap)本质上就是乐观锁操作
flag.compareAndSet(0, 1)
* 判断flag的值是否为0,如果为0就修改成1,并且返回true,如果不为0就不修改,并且返回false
* 疑问:为什么这个判断我们不自己写?
* 因为,这个过程是一个原子不可分割的操作(因为cas判断和比较是原子操作自己写判断和比较则不是原子性操作)
4)使用synchronized关键字加锁
synchronized关键字,可以保证可见性、原子性、有序性
可见性:解锁时,会强制的将所有变量刷新到主存中
原子性:加锁后,其他线程无法获得锁,也就不能打断线程中的程序执行
有序性:原子性保证了,有序性就保证了
锁什么东西?- 重要1、尽可能选择细粒度更小的对象上锁
2、synchronized修饰普通方法时,默认锁this对象
3、synchronized修饰静态方法时,默认锁当前类的class对象
5)使用Lock对象加锁
JDK1.4提供的新的加锁方式,JDK1.5之后,对synchronized关键字做了一个优化,性能和Lock就差不多了。
所以现在还是有很多程序员喜欢使用synchronized关键词
他分为重入锁、读写锁:
重入锁:
只要保证lock对象是同一个对象就行了,不像synchronized关键词还要去判断锁的啥能不能锁的住。
读写锁:
读锁兼容 读锁
读锁不兼容 写锁
写锁不兼容 写锁
6)使用数据库的锁保证数据的一致性(表锁、行锁(共享锁、排他锁))
7)使用Redis的Lua脚本保证数据一致
.....
五: 集合中的线程安全
1)ArrayList、HashMap等都是线程不安全的集合,那么,多线程中使用这些线程不安全的集合会有什么问题?
这些基础集合,添加元素时,因为是多线程,同时没有任何锁机制,所以很大的概率发生一些元素覆盖或者丢失的情况。多线程同时读写也会造成一定的问题,比如读到写线程的中间状态,造成业务判定问题等等
2)如何解决多线程操作中,集合不安全的问题?
在多线程环境下,可以使用一个线程安全的集合,比如Vector、Hashtable。但是Vector和Hashtable的锁的细粒度太大了,对于高并发的读写性能损耗太高,实际开发过程中,并不推荐使用。建议采用JDK1.4之后推出的JUC包中提供的一些线程安全的集合,比如ConcurrentHashMap等,这些集合在保证线程安全的同时,也尽可能的提高了并发能力。
思考:1、是不是实际开发过程中就一定不能用ArrayList这种线程不安全的集合?- 不是,具体问题具体分析
2、使用了线程安全的集合,是否就意味着不会发生线程安全问题呢?- 所谓的线程安全集合,只是指里面的每个单独的方法是线程安全的,但是将这些方法组合起来形成的业务,并不能保证线程安全
3)CopyOnWriteArrayList - 线程安全版的ArrayList
CopyOnWriteArrayList 采用重入锁 + 写入时复制的手段,保证集合的线程安全。添加的时候加锁,写入时无需加锁(写入时复制的方式),以此来提高集合的读取的效率。CopyOnWriteArrayList特别适合读多写少的场景,并发读取是没有任何锁机制,但是写入的成本会将对较高,每次写入都需要拷贝一新的数组。并且这种方式,可能使得读取的数据有一定概率是旧数据,所以如果程序允许这种短时间内的不一致性(最终一致性),CopyOnWriteArrayList是非常合适的,但是如果程序必须要求数据的绝对一致性,这时应该采用
4)ConcurrentHashMap - 线程安全版的HashMap
底层实现:
JDK1.7之前,采用分段锁的方式保证线程安全Jdk1.8之后,采用CAS + synchronized来保证线程安全(比JDK1.7锁的细粒度更小)
他的添加是怎么实现的呢?
六:线程间通讯的实际运用场景
线程a通过方法发送消息到,这个方法没有响应的返回值,Golang返回的响应也是通过websock发送,tomcat接收消息是列外一个线程b,这两个线程之间就要用到线程之间的通讯,对tcp来讲是没有响应的,那为什么http有响应,因为http做了一层封装,我给你发完消息我什么都不能干,一定要等到回的消息才能做其他的。