Java多线程的同步优化的6种方案

Java多线程的同步优化的6种方案

2020-06-24 22:18:19发布 浏览数:742
概述:Java多线程的同步优化的6种方案

概述

处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。

加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。

在Java内存模型中,分为主内存和线程工作内存,线程使用共享数据时,先从主内存中拷贝数据到工作内存,使用完成之后再写入主内存中。

在Java中,有多线程并发时,我们可以使用多线程同步的方式来解决内存一致性的问题。通常我们可以在程序中添加同步锁来保障数据的安全访问,但是也经常会带来一些同步性能问题,那么本章将针对常见的同步问题给出了一些优化方案。

读写锁

在多线程操作下,如果我们的某些数据经常被读取操作,但非常少的时机被写入操作。这时,如果我们使用synchronized等同步方式,性能会非常低。

这种场景下,我们应该使用读写锁来进行优化。

读写锁的特点:


读写锁维护一对锁,读锁和写锁。 可以共享读,但只能一个写。 读读不互斥,读写互斥,写写互斥。

某些特定的场景,使用读写锁会极大的提高多线程并发操作的效率。因为,读写锁中,读锁不是排它锁,所以可以并发执行,可以非常显著的提高读取效率;只有在写锁时,是排它锁,这时需要等待写锁的释放。

ReetrantReadWriteLock

ReadWriteLock接口

Java并发包中ReadWriteLock是一个接口,抽象了读写锁方法:

public interface ReadWriteLock {
    
    Lock readLock();

    
    Lock writeLock();
}
123456789101112131415

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。

ReetrantReadWriteLock类

Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。

1. ReetrantReadWriteLock获取锁顺序有两种模式:


非公平模式(默认):非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。 公平模式:当以公平模式初始化时,线程将会以队列的顺序获取锁。

2. 可重入

ReetrantReadWriteLock锁是可重入的,当然一个线程获取多少次锁,就必须释放多少次锁。


读线程获取读锁之后能够再次获取读锁。 写线程获取写锁之后能再次获取写锁,也可以获取读锁。

3. 锁降级

在读写锁中,锁降级:从写锁变成读锁;锁升级:从读锁变成写锁。


ReentrantReadWriteLock是不支持锁升级的,也就是当一个线程持有了读锁,当该线程再次使用写锁时,是不可以的。如果一个线程持有了读锁,则在获取写锁之前,一定要先释放读锁。 ReentrantReadWriteLock支持锁降级的,也就是如果当前线程是写锁的持有者,并保持获得写锁的状态,同时又获取到读锁,然后释放写锁的过程。按照获取写锁、获取读锁、再释放写锁的顺序,即写锁能够降级为读锁。

读写锁状态的设计

读写锁的状态是用一个int值来表示的。state(int32位)字段分成高16位与低16位,其中高16位表示读锁个数,低16位表示写锁个数。

例如,当前一个线程获取到了写锁,并且重入了两次,因此低16位是3,并且该线程又获取了读锁,并且重入了一次,所以高16位是2,当写锁被获取时如果读锁不为0那么读锁一定是获取写锁的这个线程。

写时复制

写时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者同时要求相同资源,他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。

在Java中,Copy on Write这种机制通常用在集合上,在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改。修改完成之后,将指向原来容器的引用指向新的容器(副本容器)。

写时复制的特点


由于不会修改原始容器,只修改副本容器。因此,可以对原始容器进行并发地读。其次,实现了读操作与写操作的分离,读操作发生在原始容器上,写操作发生在副本容器上。 数据一致性问题:因为修改操作发生在副本上,读操作的线程可能不会立即读取到新修改的数据内容,但最终修改操作会完成并更新容器,因此这是最终一致性。 CopyOnWrite容器适用于读多写少的场景。写操作时,需要复制一个容器,会造成很大的内存开销。 不适合于数据的强一致性场合。若要求数据修改之后立即能被读到,则不能用写时复制技术。因为它是最终一致性。

Java写时复制容器类

JDK中提供了CopyOnWriteArrayList类和CopyOnWriteArraySet类,实现了写时复制。

减小锁的粒度

如果我们在一个大的数据操作类里面,大量使用了锁,并且还是同一个锁,这时,我们的多线程同步效率就会变得非常低。

我们可以将数据按照不同的类型及应用场景进行分割,然后用不同的锁进行同步,这样,不同的场景下就不会产生排它锁的冲突问题,可以大大提高同步的效率。

该方案简单来说就是将一个大锁,分割成多个小锁,这样就能显著的提高多线程并发执行的效率。

减小锁的占有时间

如果在一个较大的方法中,我们直接给该方法加了一个锁,但是我们需要同步的地方只是该方法中的一行操作代码,这样就是很糟糕的同步使用方式了。

我们可以将锁细化到使用它的代码行上,而不是整个函数都加锁,这样锁的持有时间就会变少,从而提高了多线程同步的性能。

该方案是将同步块的代码范围减小,从而降低锁的持有时间,达到优化多线程同步性能的目的。

锁粗化

虽然说,减少锁的占有时间可以提高性能,但是有时候,这种方式并不适用。

例如,一个循环中,我们在循环体中,使用了锁,这样反而会降低性能,这时我们应该在循环开始之前加锁,结束之后释放,也就是将锁粗化。

这是为什么呢?

这是因为,频繁的对锁进行请求、释放、状态修改等操作,会造成大量系统资源的消耗,从而降低性能。

ThreadLocal

同步效率低,是因为多线程同步等待造成的,那么我们可以换一个思路,如果让每个线程都持有一份数据,那这样就不会存在竞争的问题了,也就不需要同步锁了。这样就会很大程度上提高多线程并发的性能。

关于ThreadLocal相关实现原理及使用可以参考之前的文章《ThreadLocal线程本地对象原理分析》。

总结

Java中可以使用锁来解决多线程的同步问题,保障了数据的一致性,但也会代理很多问题,本章总结了多线程同步的几种优化方案:

  1. 某些特定的场景(大多是读多、写少的场景),使用读写锁会极大的提高多线程并发操作的效率。因为,读写锁中,读锁不是排它锁,所以可以并发执行,可以非常显著的提高读取效率;只有在写锁时,是排它锁,这时需要等待写锁的释放。 Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。 写时复制机制可以显著提高并发效率,在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改。修改完成之后,将指向原来容器的引用指向新的容器(副本容器)。CopyOnWrite容器适用于读多写少的场景。写操作时,需要复制一个容器,会造成很大的内存开销。 通过减小锁的粒度,来提高同步效率。 减小锁的占有时间是指,通过将同步块的代码范围减小,从而降低锁的持有时间,达到优化多线程同步性能的目的。 有时,大量的锁和锁状态修改会造成系统资源的消耗,我们可以通过锁粗化来优化性能。 我们可以换一个思路,使用ThreadLocal来提高多线程并发的性能。


请先
登录
后评论
0 条评论
暂时没有评论
最新文章
更多