当前位置: 首页 > 后端技术 > Java

Java多线程、同步容器与并发容器

时间:2023-04-02 02:06:59 Java

1、为什么这种方法不能实现线程安全?分析一段代码:packagecom.guor.util;importjava.util.ArrayList;importjava.util.Collections;importjava.util.List;publicclassListHelper{publicListlist=Collections.synchronizedList(newArrayList());publicsynchronizedbooleanputIfAbsent(Ex){booleanabsent=!list.contains(x);如果(不存在){list.add(x);}returnabsent;}}毕竟putIfAbsent声明了一个synchronized类型的变量,对吧?问题是同步是在错误的锁上完成的。不管列表使用哪种锁来保护它的状态,可以肯定的是这个锁不是ListHelper上的锁。ListHelper只是带来了同步的错觉。虽然所有的链表操作都声明为synchronized,但是使用了不同的锁,也就是说putIfAbsent相对于List的其他操作不是原子的,所以不能保证putIfAbsent在执行的时候Anotherthread不会修改链表。为了使该方法正确执行,List在实现客户端锁定或外部锁定时必须使用相同的锁。客户端锁定意味着对于使用对象X的客户端代码,使用X本身用于保护器状态的锁来保护客户端代码。要使用客户端锁定,您必须知道X使用的是哪个锁定对象。Vector和同步wrapper的文档中指出,它们通过使用Vector或wrapper的内置锁来支持客户端锁定。下面的代码可以实现线程安全的列表操作。publicbooleanputIfAbent(Ex){synchronized(list){booleanabsent=!list.contains(x);如果(不存在){list.add(x);}returnabsent;}}2.组合应该是存在的当给一个类添加原子操作时,有一个更好的方法:组合。在下面的代码中,ImprovedList通过将对List对象的操作委托给底层的List实例来实现对List的操作,同时还增加了一个原子的putIfAbsent方法。(与Collections.synchronizedList等容器包装器一样,ImprovedList假定一个链表对象被传递给构造函数后,客户端代码将不再直接使用这个对象,而只能通过ImprovedList来访问它。)packagecom.guor。util;导入java.util.List;publicclassImprovedListimplementsList{privatefinalListlist;publicImprovedList(Listlist){this.list=list;}publicsynchronizedbooleanputIfAbsent(Tx){布尔值不存在=!list.contains(x);如果(不存在){list.add(x);}returnabsent;}publicsynchronizedvoidclear(){list.clear();}...}ImprovedList通过其自身的内置锁添加了额外的锁定层。它不关心底层列表是否是线程安全的。即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一致的加锁机制来实现线程安全。虽然额外的同步层可能会导致轻微的性能损失,但ImprovedList比模拟另一个对象的锁定策略更可靠。事实上,我们使用Java监视器模式来包装一个现有的List并确保线程安全,只要我们在类中拥有对底层List的唯一外部引用。3.同步容器类同步容器类包括Vector和Hashtable。实现线程安全的方法是封装它们的状态,同步每个公共方法,这样一次只有一个线程可以访问容器的状态。同步容器类是线程安全的,但在某些情况下,可能需要额外的客户端锁定来保护一致的操作。常见的对容器的复合操作包括:迭代、跳转、条件操作,如“如果不存在则添加”。在同步容器类中,这些操作在没有客户端锁定的情况下仍然是线程安全的,但是当其他线程并发修改容器时,它们可能会出现意外行为。4.隐藏迭代器虽然加锁可以防止迭代器抛出ConcurrentModificationException,但是你必须记住,所有迭代共享容器的地方都需要加锁。实际情况更复杂,因为在某些情况下,迭代器会被隐藏起来。比如log.info("setcontentis:"+set),编译器将字符串的拼接操作转化为调用StringBuilder.append(Object),这个方法会调用容器的toString方法。标准容器的toString方法将迭代容器,并对每个元素调用toString以生成容器内容的格式化表示。一个容器的hashCode、equals等方法也会间接进行迭代操作,发生在一个容器作为另一个容器的元素和键值时。五、并发容器jdk1.5提供了多种并发容器来提高同步容器的性能。同步容器序列化所有对容器状态的访问以实现它们的线程安全。这种做法是以严重降低并发性为代价的,当多个线程竞争容器的锁时,吞吐量会严重降低。另一方面,并??发容器是为多个线程的并发访问而设计的。在jdk1.5中加入了ConcurrentHashMap来代替同步的、基于hash的Map和CopyOnWriteArrayList来代替以遍历操作为主的同步List。在新的ConcurrentMap接口中增加了对一些常见复合操作的支持,例如“如果不存在则添加”、替换和有条件地删除。用并发容器代替同步容器可以大大提高可扩展性并降低风险。jdk1.5增加了两种新的容器类型:Queue和BlockingQueue。队列用于临时存储一组等待处理的元素。它提供了几种实现,包括传统的先进先出队列ConcurrentLinkedQueue和非并发优先级队列PriorityQueue。队列上的操作不会阻塞。如果队列为空,则获取元素的操作将返回空值。虽然List可以用来模拟Queue的行为,但实际上Queue是通过LinkedList实现的,但是还需要一个Queue类,因为它可以去除List的随机访问需求,从而实现更高效的并发。BlockingQueue扩展了Queue并添加了可阻塞的插入和获取等操作。如果队列为空,则获取元素的操作将被阻塞,直到队列中出现可用的元素。如果队列已满,插入元素将阻塞,直到队列中有可用空间为止。同步容器在每个操作期间持有一个锁。6、ConcurrentHashMap和HashMap一样,ConcurrentHashMap也是一个基于哈希的Map,但是它使用了完全不同的锁策略来提供更高的并发性和可扩展性。ConcurrentHashMap并没有在同一个锁上同步每个方法,使得一次只能有一个线程访问容器,而是使用更精细的锁机制来实现更大程度的共享。段锁。在这种机制下,任意数量的读线程可以并发访问Map,执行读操作的线程和执行写操作的线程可以并发访问Map,一定数量的写线程可以并发修改Map。ConcurrentHashMap的结果是在并发访问环境下会获得更高的吞吐量,而在单线程环境下只会有非常小的性能损失。ConcurrentHashMap返回的迭代器是弱一致的,而不是“及时失败”。弱一致性迭代器可以容忍并发修改。创建迭代器时,会遍历已有的元素,修改操作可以在迭代器构造完成后反映到容器中。