深入理解JVM(③)再谈线程安全 (3)

尽管在JDK5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利。从长远看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息。

非同步阻塞

互斥同步面临的主要问题时进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步(Blocking Synchronized)。从解决问题的角度来看,互斥同步是一种悲观的并发策略,无论共享的数据是否真的会出现竞争,都会进行加锁。
随着硬件指令集的发展,出现了另一种选择,基于冲突检测的乐观并发策略,通俗地说就是不管风险,先进行操作,发生了冲突,在进行补偿,最常用的补偿就是不断重试,直到出现没有竞争的数据为止。使用这种乐观并发策略不再需要线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronized)。

在进行操作和冲突检测时这个步骤要保证原子性,硬件可以只通过一条处理器指令就能完成,这类指令常用的有:

测试并设置(Test and Set);

获取并增加(Fetch and Increment);

交换(Swap);

比较并交换(Compare adn Swap,简称CAS)

加载链接/条件存储(Load-Linked/Store-Conditional,简称LL/SC)

Java类库从JDK5之后才开始使用CAS操作,并且该操作有sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。但是Unsafe的限制了不提供给用户调用,因此在JDK9之前只有Java类库可以使用CAS,譬如J.U.C包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现。直到JDK9,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。

下面来看一个例子

在这里插入图片描述


这是之前的一个例子在验证volatile变量不一定完全具备原子性的时候的代码。20个线程自增10000次的操作最终的结果一直不会得到200000。如果按之前的理解就会把race++操作或increase()方法用同步块包起来。

但是如果改成下面的代码,效率将会提高许多。

public class AtomicTest { public static AtomicInteger race = new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); } private static final int THREADS_COUNT = 20; public static void main(String[] args) throws Exception{ Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0;i<THREADS_COUNT;i++){ threads[i] = new Thread(() -> { for(int i1 = 0; i1 <10000; i1++){ increase(); } }); threads[i].start(); } while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(race); } }

运行效果:

200000

使用哦AtomicInteger代替int后,得到了正确结果,主要归功于incrementAndGet()方法的原子性,incrementAndGet()使用的就是CAS,在此方法内部有一个无限循环中,不断尝试讲一个比当前值大一的新值赋值给自己。如果失败了,那说明在执行CAS操作的时候,旧值已经发生改变,于是再次循环进行下一次操作,直到设置成功为止。

无同步方案

要保证线程安全,也不一定非要用同步,线程安全与同步没有必然关系,如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证正确性,因此有一些代码天生就是线程安全的,主要有这两类:
可重入代码:是指可以在代码执行的任何时刻中断它,然后去执行另外一段代码,而控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
可重入代码有一些共同特征:
不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数传入,不调用非可重入的方法等
简单来说就是一个原则:如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
线程本地存储(Thread Local Storage)如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能,就可以把共享数据的可见范围限制在同一个线程内,这样无须同步也能保证线程之间不出现数据争用的问题
如大部分使用消费队列的架构模式,都会将产品的消费过程限制在一个线程中消费完,最经典一个实例就是Web交互模式中的“一个请求对应一个服务器线程”的处理方式,这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygddx.html