讲到线程安全队列之前先说一下线程安全与线程不安全
- 线程安全:多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的
- 线程不安全:不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
举例:购票系统有1000张票。A线程和B线程同时抢票,有时候会抢到同一张票。这是就是线程不安全。两个线程可能同时抢一个数据。这时候就要用到锁了
线程安全队列:ConcurrentQueue<T>的使用
NuGet 安装包 > System.Collections.Concurrent
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
ConcurrentQueue<SellTicket> cqsellTickets = new ConcurrentQueue<SellTicket>();
SellTicket sellTicket=new SellTicket();
List<SellTicket> sellTickets = new List<SellTicket>();
sellTickets.Add(new SellTicket
{
name = "张三",
count="011A"
});
{
name = "李四",
count="011B"
});
cqsellTickets.Enqueue(sellTickets[0]);//单条入队
Console.WriteLine(cqsellTickets.Count);
//输出为:1
cqsellTickets.TryDequeue(out sellTicket);//单条出队
Console.WriteLine(cqsellTickets.Count);
//输出为:0
//这条数据出队了后cqsellTickets里面是没有这个数据了
Console.WriteLine("购票人姓名:"+ sellTicket.name+ "座位号:"+ sellTicket.count);
//输出为: 购票人姓名:张三座位号:011A
foreach (var item in sellTickets)
{
cqsellTickets.Enqueue(item);//多条入队
}
Console.WriteLine(cqsellTickets.Count);
//输出为:2
foreach (var item in cqsellTickets)
{
cqsellTickets.TryDequeue(out sellTicket);//多条出队
Console.WriteLine("购票人姓名:"+ sellTicket.name+ "座位号:"+ sellTicket.count);
////输出为: 购票人姓名:张三座位号:011A
////输出为: 购票人姓名:李四座位号:011B
}
Console.WriteLine(cqsellTickets.Count);
//输出为:0
class SellTicket
{
public string name { get; set; }
public string count { get; set; }
}
ConcurrentQueue进队出队已经讲解完了。然后我们结合多线程看看
多线程购票
static List<SellTicket> sellTickets = new List<SellTicket>();
for (int i = 1; i < 1000; i++)
{
SellTicket sellTicket = new SellTicket();
sellTicket.count = i + "";
sellTicket.name = "张" + i;
sellTickets.Add(sellTicket);
}
//创建两个线程
ThreadStart threadStart1 = new ThreadStart(StartThedad1);
Thread thread1 = new Thread(threadStart1);
ThreadStart threadStart2 = new ThreadStart(StartThedad2);
Thread thread2 = new Thread(threadStart2);
thread1.Start();//启动线程1
thread2.Start();//启动线程2
static void StartThedad1()
{
while (true)
{
if (sellTickets.Count > 0)
{
SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
sellTickets.Remove(sellTicket);//获取到删除数据
Console.WriteLine("thread1 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
static void StartThedad2()
{
while (true)
{
if (sellTickets.Count > 0)
{
SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
sellTickets.Remove(sellTicket);//获取到删除数据
Console.WriteLine("thread2 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
运行如下:
1604825564(1).jpg
数据重复了,而且数据很杂乱
我们加个锁(Lock)来看看效果
private static object dataLock = new object();
static List<SellTicket> sellTickets = new List<SellTicket>();
static void StartThedad1()
{
lock(dataLock)
{
while (true)
{
if (sellTickets.Count > 0)
{
SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
sellTickets.Remove(sellTicket);//获取到删除数据
Console.WriteLine("thread1 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
}
static void StartThedad2()
{
lock(dataLock)
{
while (true)
{
if (sellTickets.Count > 0)
{
SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
sellTickets.Remove(sellTicket);//获取到删除数据
Console.WriteLine("thread2 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
}
运行结果:
image.png
这样就对了。加上锁后效果很好
不加锁。用ConcurrentQueue安全队列来试试,看看效果如何
static ConcurrentQueue<SellTicket> cqsellTickets = new ConcurrentQueue<SellTicket>();
static void Main(string[] args)
{
for (int i = 1; i < 1000; i++)
{
SellTicket sellTicket = new SellTicket();
sellTicket.count = i + "";
sellTicket.name = "张" + i;
cqsellTickets.Enqueue(sellTicket);//入队
}
//创建两个线程
ThreadStart threadStart1 = new ThreadStart(StartThedad1);
Thread thread1 = new Thread(threadStart1);
ThreadStart threadStart2 = new ThreadStart(StartThedad2);
Thread thread2 = new Thread(threadStart2);
thread1.Start();//启动线程1
thread2.Start();//启动线程2
}
static void StartThedad1()
{
while (true)
{
if (cqsellTickets.Count > 0)
{
SellTicket sellTicket = new SellTicket();
cqsellTickets.TryDequeue(out sellTicket);//出队,赋值到sellTicket
Console.WriteLine("thread1 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
static void StartThedad2()
{
while (true)
{
if (cqsellTickets.Count > 0)
{
SellTicket sellTicket = new SellTicket();
cqsellTickets.TryDequeue(out sellTicket);//出队,赋值到sellTicket
Console.WriteLine("thread2 购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
}
}
}
运行看看效果如何
image.png
不管有多乱,数据是不会重复的。有的人是不是想说,那我用锁不是更好吗,为什么要用这个线程安全队列。
首先我们知道锁主要有两种,悲观锁和乐观锁。对于悲观锁,它永远会假定最糟糕的情况,就像我们上面说到的互斥机制,每次我们都假定会有其他的线程和我们竞争资源,因此必须要先拿到锁,之后才放心的进行我们的操作,这就使得争夺锁成为了我们每次操作的第一步。乐观锁则不同,乐观锁假定在很多情况下,资源都不需要竞争,因此可以直接进行读写,但是如果碰巧出现了多线程同时操控数据的情况,那么就多试几次,直到成功(也可以设置重试的次数)。
悲观锁:同一时间只有一个线程具有读写的权限。锁的机制在并发量不大情况下,十分的清晰有效。在并发量较大的时候,会因为对锁的竞争而越发不高效。同时,锁本身也需要维护一定的资源,也需要消耗性能。
乐观锁:每次读写都不考虑锁的存在,那么他是如何知道自己这次操作和其他线程是冲突的呢?这就是Lock-free队列的关键——原子操作。原子操作可以保证一次操作在执行的过程中不会被其他线程打断,因此在多线程程序中也不需要同步操作
我们也要像乐观锁一样,编译一次不过,不行两次,两次不行三次。直到编译通过。