在Java中使用线程安全集合时,什么';处理并发问题的最佳方法是什么?

在Java中使用线程安全集合时,什么';处理并发问题的最佳方法是什么?,java,multithreading,concurrency,Java,Multithreading,Concurrency,这可能是我的一个非常幼稚的想法,但我总是假设下面的代码示例在Java中使用线程安全集合时会一直工作,并且不会因NullPointerException而崩溃。不幸的是,线程t2似乎能够从两个线程声明下面的containsKey()和get()方法调用之间的列表中删除该项。注释部分显示了一种处理此问题而不获取NullPointerException的方法,因为它只是测试get()的结果是否为null 我的问题是,在Java中使用线程安全集合处理这个问题的正确方法是什么?当然,我可以使用互斥锁或同步

这可能是我的一个非常幼稚的想法,但我总是假设下面的代码示例在Java中使用线程安全集合时会一直工作,并且不会因NullPointerException而崩溃。不幸的是,线程t2似乎能够从两个线程声明下面的containsKey()和get()方法调用之间的列表中删除该项。注释部分显示了一种处理此问题而不获取NullPointerException的方法,因为它只是测试get()的结果是否为null

我的问题是,在Java中使用线程安全集合处理这个问题的正确方法是什么?当然,我可以使用互斥锁或同步块,但这种类型的集合是否会破坏线程安全集合的许多优点和易用性?如果我必须使用互斥锁或同步块,难道我不能改用非线程安全的集合吗?此外,我经常听说(在学术界)检查代码的空值是一种糟糕的编程实践。我只是疯了吗?这个问题有一个简单的答案吗?先谢谢你

package test;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] args) {
        final Map<Integer, Integer> test = new ConcurrentHashMap<>();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    test.put(0, 0);
                    Thread.yield();
                }
            }           
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    test.remove(0);
                    Thread.yield();
                }
            }           
        });

        t1.start();
        t2.start();

        while(true) {
            if (test.containsKey(0)) {
                Integer value = test.get(0);
                System.out.println(value);
            }
            Thread.yield();
        }   

        // OR
//      while(true) {
//          Integer value = test.get(0);
//          if (value != null) {
//              System.out.println(value);
//          }
//          Thread.yield();
//      }           
    }
}
封装测试;
导入java.util.Map;
导入java.util.concurrent.ConcurrentHashMap;
公开课考试{
公共静态void main(字符串[]args){
最终映射测试=新的ConcurrentHashMap();
线程t1=新线程(新的可运行线程(){
@凌驾
公开募捐{
while(true){
测试。放置(0,0);
螺纹屈服强度();
}
}           
});
线程t2=新线程(新可运行(){
@凌驾
公开募捐{
while(true){
测试。移除(0);
螺纹屈服强度();
}
}           
});
t1.start();
t2.start();
while(true){
if(试验容器(0)){
整数值=test.get(0);
系统输出打印项次(值);
}
螺纹屈服强度();
}   
//或
//while(true){
//整数值=test.get(0);
//if(值!=null){
//系统输出打印项次(值);
//          }
//螺纹屈服强度();
//      }           
}
}
这个

if (test.containsKey(0)) {
     Integer value = test.get(0);
     System.out.println(value);
}
它仍然不是原子的。检查
containsKey
后,可以添加/删除线程

您需要在该代码段周围的共享资源上进行同步。或者在
get
之后检查
null


ConcurrentHashMap
中的所有操作都是线程安全的,但它们不会超出方法边界。

您误用了线程安全集合

线程安全集合无法阻止其他代码在
containsKey()
get()
之间运行

相反,它们为您提供了额外的线程安全方法,这些方法将自动检查并获取元素,而不允许其他线程进行干扰

这意味着您不应通过基本集合接口(
Map
List
)使用并发集合。
相反,将字段声明为
ConcurrentMap

在您的情况下,只需调用
get()
,如果找不到密钥,它将自动返回null。
除了在此处检查
null
之外,别无选择。(与更优雅的函数语言不同,后者使用Maybe monad)

使用线程安全集合在Java中处理此问题的正确方法是什么

只执行一个操作,因此它是原子的。它也更快

    Integer value = test.get(0);
    if (value != null) {               
         System.out.println(value);
    }
我经常听说(在学术界)检查代码的空值是一种糟糕的编程实践。我只是疯了吗

可能吧。我认为检查
null
,如果一个值可以是
null
是最佳实践

此外,我经常听说(在学术界)检查代码的空值是一种糟糕的编程实践

对于泛型映射,当您编写
Integer i=Map.get(0)时
然后如果
i
null
,则不能断定
0
不在映射中-它可能在那里,但映射到
null

但是,对于,可以保证不存在空值:

与Hashtable类似,但与HashMap不同,此类不允许将null用作键或值。

因此,使用:

Integer i = map.get(0);
if (i != null) ...

很好。

另请参阅我的博客帖子,他没有使用
同步*
(通常都是无用的);他正在使用新的、设计合理的、并行的集合。我不知道。(不是我)OP知道这一点,他正在寻找一个推荐的解决方案。@PeterLawrey:是的;推荐的解决方案就是
get()
,正如我在底部所说的。为什么不能从基本接口引用并发哈希映射?put是put,get是get,不考虑。@Consty:要使用Java 8获取新的原子方法(如
putIfAbsent()
),将
putIfAbsent
方法放入常规
Map
界面。与其他类似
getOrDefault(key,defaultValue)
的工具一起,它允许为不存在的键获取
null
以外的内容。期待它的另一个原因。