目录
什么是线程安全
多线程编程中的三个核心概念
JMM内存模型
JMM内存模型怎么实现原子性、可见性
怎么保证线程安全
什么是线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
多线程编程中的三个核心概念
原子性、可见性、有序性
原子性:跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。(转账问题) 可见性:当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。【缓存一致性问题:CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。】 有序性:程序执行的顺序按照代码的先后顺序执行。【为了提升cpu的缓存效率,产生了指令重排序】【内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制(是一个CPU指令,保证特定操作执行的顺序性;保证某些变量的内存可见性(利用该特性实现volatile内存可见性:可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。))】
禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。
所以技术前辈们想到了在物理机器上定义出一套内存模型JMM, 规范内存的读写操作。内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
JMM内存模型
jmm主要解决了三个问题:原子性、可见性、有序性
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。
为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现
lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。 unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
JMM对8种内存交互操作制定的规则吧: 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。 不允许线程将没有assign的数据从工作内存同步到主内存。 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
JMM内存模型怎么实现原子性、可见性
1.保证原子性:可以使用 lock 和 unlock 操作来满足要求, 尽管虚拟机并未把 lock 和 unlock 操作直接开放给用户使用,但却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式的使用这两个操作,而这两个字节码指令反映到 Java 代码中就是同步代码块 —— synchronized 关键字。 2.保证可见性:volatile.JMM 对 volatile 定义了特殊规则:假设 T 表示一个线程, V 和 W分别表示两个 volatile 型变量。 ①、线程T对变量V的 use 动作和 load 、read 动作是相关联的,必须连续且一起出现。这条规则保证了T线程对其他线程修改的变量的可见性。 ②、线程T对变量V的 assign 动作和 store 、 write 动作是相关联的,必须连续且一起出现。这条规则确保了其他线程对T线程修改的变量的可见性。 ③、那么如果A先于B,那么P先于Q。这条规则保证了volatile修饰的变量不会被指令重排序优化。 3.保证有序性:volatile通过内存屏障实现(内存屏障:是一个CPU指令,保证特定操作执行的顺序性;保证某些变量的内存可见性)
StoreLoad Barriers(写读屏障)是一个“全能型”的屏障,它同时具有其他3个屏障的效果。
怎么保证线程安全
1.synchronized关键字,一个表现为原生语法层面的互斥锁,它是一种悲观锁。使用synchronized可以拿来修饰类,静态方法,普通方法和代码块。比如:Hashtable类就是使用synchronized来修饰方法的 2.使用Lock接口下的实现类。常用的实现类就是ReentrantLock 类,它其实也是一种悲观锁。一种表现为 API 层面的互斥锁。通过lock() 和 unlock() 方法配合使用。因此也可以说是一种手动锁,使用比较灵活。但是使用这个锁时一定要注意要释放锁,不然就会造成死锁。一般配合try/finally 语句块来完成。 3.使用线程本地存储ThreadLocal。当多个线程操作同一个变量且互不干扰的场景下,可以使用ThreadLocal来解决。它会在每个线程中对该变量创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。通过set(T value)方法给线程的局部变量设置值;get()获取线程局部变量中的值。当给线程绑定一个 Object 内容后,只要线程不变,就可以随时取出;改变线程,就无法取出内容。 4.使用乐观锁机制。在表设计的时候,我们通常就需要往表里加一个version字段。每次查询时,查出带有version的数据记录,更新数据时,判断数据库里对应id的记录的version是否和查出的version相同。若相同,则更新数据并把版本号+1;若不同,则说明,该数据发生了并发,被别的线程使用了,进行递归操作,再次执行递归方法,直到成功更新数据为止。