简单描述一下线程安全问题:在程序并发执行的过程中,对于临界区的一些共享数据,可能同时会有多个线程对其进行修改,造成数据覆盖、脏读等一系列问题
如何实现线程安全?
首先想到的就是实现线程同步,让并发线程同步执行,保证共享的数据在同一时刻只能被一个线程使用。
同步方案
一、互斥实现同步(也可以理解为阻塞同步)
互斥同步是一种最常见也是最主要的并发正确性保障手段。互斥是实现同步的一种手段,临界区、互斥量(Mutex)、信号量(Semaphore)都是常见的互斥实现方式。互斥是方法,同步是目的。
在Java中使用了synchronized 和 Lock 来实现互斥同步,也就是通过加锁的方式来实现。
Java提供了Lock 的实现, 像ReentrantLock,它就像synchronized的超集,相比于synchronized增加了一些高级功能,主要由以下三项:等待可中断、可实现公平锁、可以绑定多个条件变量
(synchronized 和 ReentrantLock具体实现这里不做过多阐述)
二、非阻塞同步
互斥同步面临的主要问题是进行线程阻塞和唤醒造成的线程上下文切换所带来的性能开销。因为采用加锁的方式来实现同步,当所资源被其他线程占有,当前线程获取不到锁的时候就会进入阻塞,释放CPU资源,当锁资源被释放时再抢占CPU资源,这样会导致线程上下文的切换(线程由用户态到内核态,内核态再到用户态),线程上下文切换是很消耗性能的。
从解决问题的方式来看,互斥同步属于一种悲观的并发策略。
非阻塞同步 是一种基于冲突检测的乐观并发策略,通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享数据的确被争用,产生了冲突,那么再进行其他的补偿措施,做常用的补偿措施就是不断地重试,直到共享数据不再存在竞争时。这种乐观的并发并发策略的实现不再需要把线程阻塞挂起,也称为"无锁"并发。
这种"无锁并发"主要一种实现就是 CAS指令。
在Java的Unsafe类中,提供了一系列CAS方法,compareAndSwapInt()、compareAndSwapLong()等。CAS指有三个参数:变量的内存地址(V)、旧的预期值(A)、准备设置的新值(B),CAS指令执行时,当V和A相等时,才会用B更新V中的值,否则就不执行更新,上述的过程是一个原子操作。
CAS并不完美,它存在一个逻辑漏洞:ABA问题 ,如果一个变量在初次读取的时候是A,并且在准备赋值的时候也是A,就能保证在此期间没有其他线程对其进行修改吗,也有可能时 A -> B - > A这种情况。为了解决这个问题,Java 的J.U.C包中提供了带有标记的原子引用类AtomicStampedReference,它可以通过控制变量的版本信息来保证CAS的正确性。
注:CAS是如何保证比较新旧值和更新值这两个操作的原子性的呢?
CAS指令是基于硬件实现的原子指令,在操作系统层面,CAS还是会加锁的,通过加锁的方式锁定总线,避免其他CPU访问共享变量。( 还是加了"锁" /(ㄒoㄒ)/~~)
无同步方案
要保证线程安全,也并非一定要进行同步,同步与线程安全两者并没有必然的联系。同步只是保障存在共享数据的争用时的线程安全,如果能保证一个方法本来就不实际共享数据,那么自然就不需要任何的同步措施。