并发与不可变性

2023-10-13 17:48
文章标签 并发 可变性

本文主要是介绍并发与不可变性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对于今天的应用程序来说,并发是一个重要的、也愈发受到关注的方面。随着交易量的增加、业务日趋复杂,对大量并发线程的需求也越来越急迫。另外,由依赖注入管理的对象在应用程序中的其角色也极为关键。 Singleton就是典型的这种需求。
对于一个每分钟需要处理几百个请求的大型Web应用来说,如果Singleton设计得很糟糕,它会成为严重的瓶颈,以及系统的并发性能的短板,甚至在一些特定的条件下,会导致系统失去可伸缩性。
糟糕的并发行为可能比你想象的要普遍。并且,由于它们产生的影响只有在性能测试期间才会暴露出来,这使得识别和解决这些问题变得更加困难。因此,研究Singleton与并发的关系就变得很重要了。

“可变性”是这个问题的一个关键要素。“不可变性”的理念背后也有很多隐晦的陷阱。所以,我们首先讨论一下所谓的“不可变”到底是指什么。为了直接切入问题,我们将以一系列谜题的方式来探讨。
不可变性谜题#1下面的Book类是不可变的么?

public class Book {private String title;public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}
}

答案#1这个问题的答案很简单:不是。只要调用setTitle()就可以任意修改title域的值。所以这个类不是不可变的。如果将title声明为final的,就可以让Book类成为不可变的,如下所示:

public class ImmutableBook {private final String title;public ImmutableBook(String title) {this.title = title;}public String getTitle() {return title;}
}

一旦在构造函数里面设置了title的值以后,就不能再改变它了。
不可变性谜题#2下面的类是不可变的么?

public class AddressBook {private final String[] names;public AddressBook(String[] names) {this.names = names;}public String[] getNames() {return names;}
}

答案#2
域names的值是final类型,只会在构造函数中设置一次。所以AddressBook应该是不可变的,对不对?错!事实上,容易混淆的地方在于names是一个数组,将它声明为final只会使它的引用成为不可变的。 下面的代码完全是合法的,但它却潜在地破坏了数据。而这正是多线程程序所担心的问题:

public class AddressBookMutator {private final AddressBook book;@Injectpublic AddressBookMutator(AddressBook book) {this.book = book;}public void mutate() {String[] names = book.getNames();for (int i = 0; i < names.length; i++)names[i] = "Censored!"for (int i = 0; i < names.length; i++)System.out.println(book.getNames()[i]);
}
} 

虽然names域是不可改变的,但是方法mutate()会破坏性地改写数组。如果运行了这段程序,AddressBook中的每个名字都变成了“Censored(已篡改)”。解决这个问题的真正方法是避免使用数组,或者在真正理解它们以后非常谨慎地使用。更好的办法是使用容器类库(比如java.util),这样能够用一个不可修改的封装类来保护数组的内容。参看谜题3,它示范了用java.util.List取代数组。
不可变性谜题#3下面的BetterAddressBook类是不可变的么?

public class BetterAddressBook {private final List names;   public BetterAddressBook (List names){this.names = Collections.unmodifiableList(names);    }   public List getNames() {return names; }
}

答案#3
谢天谢地,没错,BetterAddressBook是不可变的。Collections类库中的封装类可以确保一旦设置了names的值,就不能对它再有任何更新。下面的代码虽然可以编译,却会在运行时导致异常:

BetterAddressBook book = new BetterAddressBook(Arrays.asList("Landau", "Weinberg", "Hawking"));
book.getNames().add(0, "Montana");

不可变性谜题#4
下面是谜题3的变体,仍然使用我们前面的见到的BetterAddressBook类。是否存在某种构造方法,使得我仍然可以在构造以后修改它?前提是不允许修改BetterAddressBook的代码。
答案非常简单,只是有点儿混乱:

List physicists = new ArrayList();
physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));
BetterAddressBook book = new BetterAddressBook(physicists);
physicists.add("Einstein");

现在遍历BetterAddressBook的names列表:

for (String name : book.getNames())
System.out.println(name);

恩,看来,我们必须要重新审视谜题3中的答案了。只有满足了names列表没有泄露到BetterAddressBook类以外的前提条件,BetterAddressBook才是不可变。更好的方法是,我们能够重写一个完全安全的版本:在构造的时候复制一份列表:

@Immutable
public class BestAddressBook {private final List names;public BestAddressBook(List names) {this.names = Collections.unmodifiableList(new ArrayList(names));    }   public List getNames() {return names;}
}

现在,你可以随意泄露甚至修改原来的列表了:

List physicists = new ArrayList();
physicists.addAll(Arrays.asList("Landau", "Weinberg", "Hawking"));
BetterAddressBook book = new BetterAddressBook(physicists);
physicists.clear();physicists.add("Darwin");
physicists.add("Wallace");physicists.add("Dawkins");
for (String name : book.getNames())System.out.println(name);

同时BestAddressBook不会受到任何影响:

Landau
Weinberg
Hawking

尽管你不必每次都使用这么小心的方法,但是如果你无法确保参数是否可能泄露到其他的对象中去,那么建议你使用这种方法。
不可变性谜题#5下面的Library类是不可变的么?(调用了谜题1中的Book)

public class Library {private final List books;   public Library(List books) {        this.books = Collections.unmodifiableList(new ArrayList(books));    }   public List getBooks() {return books;}}

答案#5Library依赖于一个Book列表,不过它非常小心地先复制一份列表,然后把它封装在一个不可变的包装类中。当然它唯一的域也是final的。每件事看起来都无懈可击了?事实上,Library其实是可变的!尽管Book的容器是不变的,但是Book对象自身却不是。回忆一下谜题1中的场景,Book的title可以被修改:

Book book = new Book();
book.setTitle("Dependency Injection")
Library library = new Library(Arrays.asList(book));
library.getBooks().get(0).setTitle("The Tempest"); //mutates Library

不可变性和对象图的黄金规则是每一个被依赖的对象也必须是不可变的。在BestAddressBook中,我们很幸运,因为Java中的String已经是不可变的了。在声明一个“不可变”的对象以前,仔细地检查它依赖的每一个对象也都是安全不可变的。在谜题4中见到的@Immutable标注可以帮你传达这一意图,并将它记录到文档中。
原文地址:http://www.infoq.com/cn/articles/dhanji-prasanna-concurrency

这篇关于并发与不可变性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/204877

相关文章

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

PostgreSQL中的多版本并发控制(MVCC)深入解析

引言 PostgreSQL作为一款强大的开源关系数据库管理系统,以其高性能、高可靠性和丰富的功能特性而广受欢迎。在并发控制方面,PostgreSQL采用了多版本并发控制(MVCC)机制,该机制为数据库提供了高效的数据访问和更新能力,同时保证了数据的一致性和隔离性。本文将深入解析PostgreSQL中的MVCC功能,探讨其工作原理、使用场景,并通过具体SQL示例来展示其在实际应用中的表现。 一、

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

Go并发模型:流水线模型

Go作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,Go并发模型有多种模式,通过流水线模型系列文章,你会更好的使用Go的并发特性,提高的程序性能。 这篇文章主要介绍流水线模型的流水线概念,后面文章介绍流水线模型的FAN-IN和FAN-OUT,最后介绍下如何合理的关闭流水线的协程。 Golang的并发核心思路 Golang并发核心思路是关注数据流动。数据流动的过程交给cha

Java 入门指南:Java 并发编程 —— 并发容器 ConcurrentLinkedDeque

文章目录 ConcurrentLinkedDeque特点构造方法常用方法使用示例注意事项 ConcurrentLinkedDeque ConcurrentLinkedDeque 是 Java 并发工具包(java.util.concurrent 包)中的一个线程安全的双端队列(Deque)实现,实现了 Deque 接口。它使用了链表结构,并且针对高并发环境进行了优化,非常适合

并发线程基础(一)

一、多个线程多个锁(lock) 概念:每个线程拿到自己的锁之后,获取线程锁之后,执行synchronized方法内的内容。      (一)synchronized 重入          概念:我比较喜欢通过自己的语言去记住和简化概念,重入:也叫“递归锁”,指同一个线程的外层方法获得锁之后,该方法里面调用修饰锁的方法,仍然可以获取锁,相互不受影响的。           语言话:同个类中