Java线程安全需要这些的内容

140 阅读6分钟

实现线程安全的目的是?

实际上是保证程序运行的原子性,有序性,可见性。

原子性、一致性:对数据的所有操作要么都成功,要么都失败。

有序性:程序的执行严格保持逻辑顺序。

可见性:对数据的更新对外同步可知。

实现多线程数据安全的几种方式:

  • 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返回。