锁是用于控制多个线程对共享资源的访问的机制,防止出现程序对共享资源的竞态关系
线程安全
在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况
线程的竞态条件
竞态条件(race condition)
竞态条件(race condition)指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。竞争条件会产生超出预期的情况,一般情况下我们都希望程序执行的结果是符合预期的,因此竞争条件是一种需要被避免的情形。
竞争条件分为两类:
- Mutex(互斥):两个或多个进程彼此之间没有内在的制约关系,但是由于要抢占使用某个临界资源(不能被多个进程同时使用的资源,如打印机,变量)而产生制约关系。
- Synchronization(同步):两个或多个进程彼此之间存在内在的制约关系(前一个进程执行完,其他的进程才能执行),如严格轮转法。
要阻止出现竞态条件的关键就是不能让多个进程/线程同时访问那块共享变量。访问共享变量的那段代码就是临界区(critical section)。所有的解决方法都是围绕这个临界区来设计的。
线程安全的三大特性
1、原子性(Atomicity):原子操作是不可分割的操作,要么全部执行成功,要么全部不执行。在多线程环境下,如果多个线程同时执行原子操作,不会出现数据不一致的情况。例如,使用synchronized关键字或者Lock接口来保证关键代码块的原子性。
2、可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。在多线程环境下,需要保证共享变量的修改对其他线程是可见的,通常可以使用volatile关键字来实现可见性。
3、有序性(Ordering):有序性是指程序的执行顺序按照代码的先后顺序来执行,不会因为编译器的优化或者CPU的乱序执行而导致结果的不确定。在多线程环境下,需要保证共享变量的读写操作是有序的,通常可以通过synchronized关键字或volatile关键字来实现有序性。
Java的内存模型
了解更多:https://www.51cto.com/article/658158.html
我们都知道JVM中每个线程都有自己的栈空间,共享变量会存放在主内存中。在并发修改变量的过程中线程可能会发生挂起,导致写入到主内存的值发生覆盖。导致破坏了原子性
as-if-serial语义和happen-before原则
1、as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序
2、JVM定义的Happens-Before原则是一组偏序关系:对于两个操作A和B(共享数据),这两个操作可以在不同的线程中执行。如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的
3、as-if-serial 和 happens-before 的区别
- as-if-serial是针对单线程程序的执行结果的一致性,允许虚拟机对单线程程序进行指令重排序,只要不改变执行结果。
- happens-before是针对多线程程序的执行顺序的一致性,描述了多线程之间操作的可见性和顺序关系。
- as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度
volatile关键字
volitale是Java虚拟机提供的一种轻量级的同步机制
1、保证可见性
可见性主要存于JMM的内存模型当中,指当一个线程改变其内部的工作内存当中的变量后,其他内存是否可以感知到,因为不同的工作线程无法访问到对方的工作内存,线程间的通信必须依靠主内存进行同步
2、不保证原子性
当在多线程改变变量时,需要将变量同步到工作线程中;当线程A对变量修改时,还没同步到主内存中线程挂起,线程B也对变量进行修改,这时线程A进行执行,就会覆盖线程B的值,由于可见性,这是线程B也会变成线程A修改的值,导致一致性问题
3、禁止指令重排
在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
想要线程安全必须保证原子性,可见性,有序性。而volatile只能保证可见性和有序性
内存屏障
- Memory barrier 能够让CPU或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。
- Memory barrier是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
- 有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。
Memory Barrier可以被分为以下几种类型:
1、LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
2、StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
3、LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
4、StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile语义中的内存屏障
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
volatile的内存屏障策略非常严格保守,保证了线程可见性。
final语义中的内存屏障
新建对象过程中,构造体中对final域的初始化写入(StoreStore屏障)和这个对象赋值给其他引用变量,这两个操作不能重排序;
初次读包含final域的对象引用和读取这个final域(LoadLoad屏障),这两个操作不能重排序;
Intel 64/IA-32架构下写操作之间不会发生重排序StoreStore会被省略,这种架构下也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。
锁的类型
从线程是否需要对资源加锁可以分为
悲观锁(系统)
和乐观锁(CAS)
从资源已被锁定,线程是否阻塞可以分为自旋锁(CAS)
从多个线程并发访问资源,也就是 Synchronized 可以分为无锁
、偏向锁
、轻量级锁
和重量级锁
从锁的公平性进行区分,可以分为公平锁
和非公平锁
从根据锁是否重复获取可以分为可重入锁
和不可重入锁
从那个多个线程能否获取同一把锁分为共享锁(读锁)
和排他锁(写锁)
类锁和对象锁
类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的
类锁是指对静态方法或静态变量加锁时所产生的锁。类锁是类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的;当一个线程获取了一个类锁,其他线程就无法同时获取该类的锁,直到该线程释放了锁。
对象锁是指对非静态方法或非静态变量加锁时所产生的锁。每个对象都有自己的对象锁,当一个线程获取了某个对象的锁,其他线程就无法同时获取该对象的锁,直到该线程释放了锁
自旋锁
优点:自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁
缺点:
1、自旋锁一直占用 CPU ,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致 CPU 效率降低。
2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
CAS自旋
CAS(Compare and Swap)是一种基于原子操作的并发控制机制,用于实现多线程之间的同步。CAS操作包含三个操作数,分别为内存位置V、期望值A和新值B。当且仅当V的值等于A时,CAS将V的值设为B,否则不做任何操作。
CAS 是一条 CPU 的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe类提供的 CAS 方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
AtomicInteger
:用于原子性地操作整数型变量。AtomicLong
:用于原子性地操作长整型变量。AtomicBoolean
:用于原子性地操作布尔型变量。AtomicReference
:用于原子性地操作引用类型变量。AtomicIntegerArray
:用于原子性地操作整数型数组。AtomicLongArray
:用于原子性地操作长整型数组。AtomicReferenceArray
:用于原子性地操作引用类型数组。
ABA问题
在多线程编程中,常常会遇到ABA问题。简单来说,ABA问题就是指线程A读取了共享变量V的值,然后线程B将V的值改为了其他值,再次改回了原来的值,然后线程A又进行了写操作。这种情况下,线程A会认为V的值没有发生变化,但实际上V的值已经发生了变化。
为了解决ABA问题,Java中提供了一个带有时间戳的原子类AtomicStampedReference。AtomicStampedReference类可以通过增加时间戳来解决ABA问题,时间戳的作用是记录每一次变量的修改操作,使得在比较并交换时不仅需要比较变量的值,还需要比较变量的时间戳是否相同。这样就可以避免ABA问题的发生。
具体来说,AtomicStampedReference类中的compareAndSet方法不仅会比较当前值和期望值是否相等,还会比较当前的时间戳是否相等。只有当前值和时间戳都相等时,才会执行CAS操作,否则不会执行。
Unsafe类
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用
如何调用Unsafe类
1、从getUnsafe
方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a
把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe
方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
2、通过反射获取单例对象theUnsafe。
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
标签:变量,并发,屏障,线程,内存,操作,排序
From: https://www.cnblogs.com/luojw/p/18141005