多线程操作集合时如何保证集合的线程安全性

奋斗先看示例代码1

using System;
using System.Collections.Generic;
using System.Threading;
 
namespace CollSecExp
{
    class Program
    {        
        static void Main(string[] args)
        {
            List<int> list = new List<int>();
            for (int i = 0; i < 10; i++)
            {
                list.Add(i);
            }

            Thread t1 = new Thread(() =>
            {
                 foreach (var item in list)
                 {
                     Console.WriteLine("t1.item:{0}", item);
                     Thread.Sleep(1000);
                 }
            });
            t1.Start();

            Thread t2 = new Thread(() =>
            {
                Thread.Sleep(1000);
                list.RemoveAt(1);
                list.RemoveAt(3);
                foreach (var item in list)
                {
                     Console.WriteLine("t2.item:{0}", item);
                 }
            });
            t2.Start();
        }
    }
}

运行示例代码1,会抛出InvalidOperationException异常,提示“集合已修改;可能无法执行枚举操作。”

这是因为,线程2移除index=13的元素导致集合被修改,很显然,此时线程1遍历集合肯定会出错,因为它这时遍历得到的结果与实际情况已经不符,就算取出也没有了价值。

 

针对这种情况,我们可以对集合加锁来保证线程对集合操作的同步。

奋斗修改示例代码1,得到示例代码2

using System;
using System.Collections.Generic;
using System.Threading;
 
namespace CollSecExp
{
    class Program
    {        
        static void Main(string[] args)
        {
            object sycObj = new object();
            List<int> list = new List<int>();
            for (int i = 0; i < 10; i++)
            {
                list.Add(i);
            }
 
            Thread t1 = new Thread(() =>
            {
                lock (sycObj)
                {
                    foreach (var item in list)
                    {
                        Console.WriteLine("t1.item:{0}", item);
                        Thread.Sleep(1000);
                    }
                }
            });
            t1.Start();

            Thread t2 = new Thread(() =>
            {
                Thread.Sleep(1000);
                lock (sycObj)
                {
                    list.RemoveAt(1);
                    list.RemoveAt(3);
                    foreach (var item in list)
                    {
                        Console.WriteLine("t2.item:{0}", item);
                    }
               }
            });
            t2.Start();
        }
    }
}

示例代码2与示例代码1的区别仅在加锁操作上,变化的地方已经用红色字体标出。

再次运行代码,未报错,得到以下运行结果:

 

从结果我们可以看出,加锁以后,线程1执行的结果是正常的,正确的输出了list10个元素。同样,线程2执行结果也是正常的,移除index=13的元素后,遍历出剩下的8个元素。不过这里要补充说明一下,移除index=1的元素时,为什么是源集合中的index=14的元素被移除?那是因为移除index=1的元素后,index=4的元素在新集合中的index就变成3了,所以会有这样的结果。

 

另外,在编写测试代码时,还遇到了下面这样一个问题,拿出来分享一下。

大哭将线程2的代码改写成下面的形式。

Thread t2 = new Thread(() =>
{
     Thread.Sleep(1000);
     lock (sycObj)
     {
          foreach(var item in list)
          {
               if (item % 2 == 0)
               {
                    list.Remove(item);
               }
          }
          foreach (var item in list)
          {
               Console.WriteLine("t2.item:{0}", item);
          }
     }
});
t2.Start();

运行程序,你会发现,线程1能正常运行,但是在执行线程2时,同样会InvalidOperationException异常,提示信息仍为“集合已修改;可能无法执行枚举操作。”

分析代码,发现是在遍历集合并作移除操作的代码部分(已用青色字体标出),对于这点其实不在多线程程序中也是会出错的。很明显这种做法就不对,你既想遍历集合又想动态删除某些符合条件的元素,这是不可能的,哪有这么好的事。

 

其实,除了加锁,还可以使用System.Collections.Concurrent命名空间中提供的ConcurrentBag<T>来代替List<T>,ConcurrentBag<T>是线程安全的集合类。

奋斗代码片段3:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Concurrent;   //注1 

namespace CollSecExp
{
    class Program
    {        
        static void Main(string[] args)
        {
            ConcurrentBag<int> list = new ConcurrentBag<int>();
            for (int i = 0; i < 10; i++)
            {
                list.Add(i); //注2
            }
 
            Thread t1 = new Thread(() =>
            {
                foreach (var item in list)
                {
                    Console.WriteLine("t1.item:{0}", item);
                    Thread.Sleep(1000);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() =>
            {
                Thread.Sleep(1000);
                int a;
                list.TryTake(out a); //注3
                foreach (var item in list)
                {
                    Console.WriteLine("t2.item:{0}", item);
                }
            });
            t2.Start();
        }
    }
}

示例代码3与示例代码1的区别在于使用ConcurrentBag<int>来代替List<int>保存集合数据。这样一来就算不加锁,也能保证线程操作的同步。

有几点稍微补充说明一下:

[注1]需要引用命名空间System.Collections.Concurrent才可以使用ConcurrentBag<T>类。

[注2]ConcurrentBag<T>的Add方法与List<T>的Add方法相同。

[注3]ConcurrentBag<T>的移除方法为TryTakeList<T>的移除方法不相同,这点需要注意。

 运行程序,得到结果:

 

会发现线程1与线程2能正常运行,只是此时线程12会并行执行,且遍历集合时会从项值大的到小的顺序进行,关于这点,以后再研究。

 

就到这里了。再见

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java高并发线程安全集合是指在多线程环境下能够保证数据一致性和线程安全的数据结构。Java提供了许多高并发线程安全集合,包括ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList、CopyOnWriteArraySet等。 ConcurrentHashMap是一个线程安全的哈希表,它允许多个线程同读取并修改其中的元素。它使用分段锁的方式来实现并发访问,不同的线程可以同访问不同的分段,从而提高了并发性能。 ConcurrentSkipListMap是一个基于跳表的并发有序映射,它可以提供较好的并发性能,且支持按照键的顺序进行遍历。它的实现是通过通过多层链表实现的,每一层链表中的节点按照键的顺序排列。 ConcurrentSkipListSet是一个基于ConcurrentSkipListMap的并发有序集合,它实现了Set接口,并且保证元素的有序性和线安全性。 CopyOnWriteArrayList是一个线程安全的ArrayList,它通过每次修改创建一个新的副本来实现线程安全。虽然在插入和删除操作需要复制整个数组,但读取操作非常高效,适用于读操作远多于写操作的场景。 CopyOnWriteArraySet是一个线程安全的Set,它是基于CopyOnWriteArrayList实现的。它通过复制整个数组来实现线程安全,保证了元素的唯一性和线程安全。 这些高并发线程安全集合在多线程环境中保证了数据的一致性和线安全性,能够提高并发性能和效率,适用于高度并发和需要频繁读写的场景。但需要注意的是,并发集合在某些操作上可能会损失一些性能,因此在选择使用需根据具体需求进行权衡和选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值