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

概述

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

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

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

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

读写锁

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

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

读写锁的特点:

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

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

ReetrantReadWriteLock

ReadWriteLock接口

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

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

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

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

到此这篇关于Java多线程的同步优化的6种方案的文章就介绍到这了,更多相关Java多线程同步优化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java多线程-同步块实例讲解

    java多线程-同步块 Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java 同步块用来避免竞争.本文介绍以下内容: Java 同步关键字(synchronzied) 实例方法同步 静态方法同步 实例方法中同步块 静态方法中同步块 Java 同步示例 Java 同步关键字(synchronized) Java 中的同步块用 synchronized 标记.同步块在 Java 中是同步在某个对象上.所有同步在一个对象上的同步块在同时只能被一个线程进入并执

  • java 多线程的同步几种方法

    java 多线程的同步几种方法 一.引言 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊.闲话不多说,进入正题. 二.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常.举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个

  • Java 多线程同步 锁机制与synchronized深入解析

    打个比方:一个object就像一个大房子,大门永远打开.房子里有很多房间(也就是方法).这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法).房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间.另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人.所有的东西就这么多了,下面我们看看这些东西之间如何作用的. 在此我们先来明确一下我们的前提条件.该对象至少有一个synchronized方法,否则这个key还有啥意义.当然也就不会有我们的这个主题了. 一

  • java多线程的同步方法实例代码

    java多线程的同步方法实例代码 先看一个段有关银行存钱的代码: class Bank { private int sum; public void add(int num){ sum = sum + num; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("total num is : " + sum); } } class Cu

  • Java多线程编程之CountDownLatch同步工具使用实例

    好像倒计时计数器,调用CountDownLatch对象的countDown方法就将计数器减1,当到达0时,所有等待者就开始执行. java.util.concurrent.CountDownLatch 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待.用给定的计数初始化CountDownLatch.由于调用了countDown()方法,所以在当前计数到达零之前,await方法会一直受阻塞.之后,会释放所有等待的线程,await的所有后续调用都将立即返回.这种现

  • 浅谈Java多线程编程中Boolean常量的同步问题

    在JAVA中通过synchronized语句可以实现多线程并发.使用同步代码块,JVM保证同一时间只有一个线程可以拥有某一对象的锁.锁机制实现了多个线程安全地对临界资源进行访问.   同步代码写法如下:   代码1: Object obj = new Object(); ... synchronized(obj) { //TODO: 访问临界资源 } JAVA的多线程总是充满陷阱,如果我们用Boolean作为被同步的对象,可能会出现以下两种情况:   一. 以为对一个对象加锁,实际同步的是不同对

  • 基于Java回顾之多线程同步的使用详解

    首先阐述什么是同步,不同步有什么问题,然后讨论可以采取哪些措施控制同步,接下来我们会仿照回顾网络通信时那样,构建一个服务器端的"线程池",JDK为我们提供了一个很大的concurrent工具包,最后我们会对里面的内容进行探索. 为什么要线程同步? 说到线程同步,大部分情况下, 我们是在针对"单对象多线程"的情况进行讨论,一般会将其分成两部分,一部分是关于"共享变量",一部分关于"执行步骤". 共享变量 当我们在线程对象(Run

  • java多线程编程之使用Synchronized块同步变量

    下面的代码演示了如何同步特定的类方法: 复制代码 代码如下: package mythread; public class SyncThread extends Thread{ private static String sync = ""; private String methodType = ""; private static void method(String s) {  synchronized (sync)  {sync = s;System.out

  • java多线程编程之使用Synchronized块同步方法

    synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可以通过synchronized块来同步一个对象变量.也可以使用synchronized块来同步类中的静态方法和非静态方法.synchronized块的语法如下: 复制代码 代码如下: public void method(){    - -    synchronized(表达式)    {        -

  • Java多线程编程中synchronized线程同步的教程

    0.关于线程同步 (1)为什么需要同步多线程? 线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源.我们采用Java中的同步代码块和同步方法达到这样的目的.比如这样的解决多线程无固定序执行的问题: public class TwoThreadTest { public static void main(String[] args) { Thread th1= new MyThread1(); Thread th2= new MyThread2(); th1.st

  • 浅谈Java多线程实现及同步互斥通讯

    Java多线程深入理解本文主要从三个方面了解和掌握多线程: 1. 多线程的实现方式,通过继承Thread类和通过实现Runnable接口的方式以及异同点. 2. 多线程的同步与互斥中synchronized的使用方法. 3. 多线程的通讯中的notify(),notifyAll(),及wait(),的使用方法,以及简单的生成者和消费者的代码实现. 下面来具体的讲解Java中的多线程: 一:多线程的实现方式 通过继承Threa类来实现多线程主要分为以下三步: 第一步:继承 Thread,实现Thr

随机推荐