实现线程安全的目的是?
实际上是保证程序运行的原子性,有序性,可见性。
原子性、一致性:对数据的所有操作要么都成功,要么都失败。
有序性:程序的执行严格保持逻辑顺序。
可见性:对数据的更新对外同步可知。
实现多线程数据安全的几种方式:
- synchronized关键字
- volatile
- Lock
- CoutDownLatch
- CyclicBarrier
- Semaphore
线程安全的一些数据结构
ConcurrentHashmap、HashTable一些理解
JVM内存模型确保有序性的happens-before原则
synchronized关键字
JVM使用synchronized来保证线程执行安全,使用synchronized修饰的执行范围具有线程互斥的效果。
synchronized修饰代码块,方法持有的锁是当前对象。
synchronized修饰的静态方法持有的锁对象是当前类class对象。
在JVM中,synchronized采用对象中的对象头的Monitor,Monitor对象中有个Owner成员描述的是持有锁的线程。
如果是synchronized修饰的是代码块,则实际上是使用Monitorenter指令和monitorexit指令关开来进行互斥作用。实际上这两个指令是将monitor的线程计数器进行递增和递减。
如果synchronized修饰的是方法,则方法在运行时会在flags键对应的值中带有ACC_SYNCHRONIZED修饰符。表示在程序运行时有检测到这个ACC_SYNCHRONIED时会需要获取到monitor锁对象才有继续执行的权利。
synchronized和Reetanlock的异同?
共同点:对锁的持有具有可重入特点。
不同点:
实现方式:synchronized是JVM层面的实现。Lock是JAVA API层面实现。synchronized 是内置锁,由 JVM 实现获取锁和释放锁的原理,还分为偏向锁、轻量级锁、重量级锁。Lock 根据实现不同,有不同的原理,例如 ReentrantLock 内部是通过 AQS 来获取和释放锁的。
加锁方式:synchronized加解锁是自动的,程序运行异常会自动释放锁。Lock需要手动调用lock和unlock操作。但因此Lock的灵活性要比synchronized强,synchronized内的运行时间较长会导致其他线程堵塞而影响程序运行。
性能:在等同业务场景下,因Lock方式更加灵活,经过优化往往性能要比synchronized的方式性能要高。但同时也增加了程序的复杂性。以前Java 5时synchronized是重量级锁,在性能明显弱势,但是Java 6以后JVM对synchronized关键字进行优化后性能提升,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差很多。锁级别:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.
优化后的synchronized Java 6锁的优化
线程无锁状态:略
偏向锁:当synchronized在单线程环境下,没有其他线程和它竞争锁的时候,就会启动非常轻量的偏向锁机制。程序采用CAS的方式获取monitor锁对象。
轻量级锁:当出现其他线程来竞争锁的时候,偏向锁机制就会实现,采用轻量级锁机制。这时后面的线程会通过CAS的方式竞争锁。如果再来第三个线程同样采用CAS的竞争锁,但是发现竞争不到锁的话就会自旋,假如在有限的CAS后还是无法竞争到锁,那么轻量级机制就会失效,采用重量级锁的方式,将没有获取到锁的线程全部挂起。
重量级锁:没有获取到锁的线程全部挂起,等待有锁的线程释放锁,再进行竞争锁。
总结:整体来看synchronized的优化机制是使用CPU的资源消耗来提升效率,以空间换时间的策略。偏向锁、轻量级锁消耗CPU资源,不挂起线程,响应时间快。重量级锁,追求吞吐快,不消耗CPU资源。
volatile关键字
这个关键字主要是保证了数据的可见性。采用了CPU的的CAS机制。来确保CPU高速缓存区对数据的修改能够迅速刷会主内存,同时使其他线程栈中的该份数据的拷贝失效,当下一次读取数据时重新从主内存中获取。从而保证了数据的可见性。
另外,volatile关键之禁止了指令重排。
关于ABA的问题。
问题描述:当线程A对将数据age从1改成2,然后又将数据从2改成1。这时候数据实质上数据以及改变了。但对于线程B来说,没有感知。这就是ABA问题。
解决方法:将数据的改变新增vission字段做自增操作,让线程B能够感知数据的变化。使用
Atomic包,保证数据的原子性。
Lock机制
通常实现达到线程互斥的效果可以使用Java ReetrankLock,进行获取锁和释放锁。
ReetrankLock的机制不仅仅实现了多线程下的读读的锁,也实现了写的锁。在一些情况下, 读和写是不同方向的操作,读操作不依赖写操作,或者写操作不依赖读操作。这时候可以使用ReentrantReadWriteLock。其中另外提供了readLock和writeLock来分别获取读锁和写锁来进行操作。
CoutDownLatch和CyclicBarrier同异
两个有点类似,但又一点不同。他们都是用来保证线程调度级别上的有序性。CoutDownLatch是等所有线程执行完毕后执行某一条线程。CyclicBarrier是等待某条线程执行完后放行所有正在等待的线程。有互补的操作效果,有那意思。
Semaphore是线程交通管制类
执行n条线程,多的线程要等待有坑后才能进入执行。有点类似地铁的交通管制,只有N条道可以通行,多出来的人要排队。通过semaphore.acquire(); // 获取一个许可,可以进入一个人。semaphore.release(); // 释放一个坑位,表示又可以进一个人了。
ConcurrentHashmap、HashTable一些理解
ConcurrentHashmap在JDK 8主要是采用了Hashmap相近的实现原理加上CAS来实现线程安全的存储结构。
HashTable是HashMap的一个添加synchronized对成员方法修饰来实现线程同步的效果。
JVM happens-before八大原则
happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性(有序性)
(1)程序的顺序性规则
一条线程中,编译器会进行指令重排优化,所以导致不一定是顺序编译,但会确保运行结果是顺序推演的结果,做到逻辑有序性。
(2)volatile 变量规则
对于被volatile修饰的变量,所有写操作happens-before所有读操作,同时volatile编译会禁止指令重排
(3)happens-before传递性规则
对于A操作happens-before于B,B操作happens-before于C,那么A一定happens-before于C,确保有序性。所以说具有传递性。
(4)对象终结原则
对象的构造方法一定happens-before 它的finalize()方法
(5)线程锁规则
对于一个线程的加锁操作,一定happens-before解锁操作
(6)线程start(),线程join()规则,中断interect规则
线程A启动线程B,线程A前面操作happens-before于B的start,不难理解
线程join规则,线程执行所有操作happens-before线程的join返回。