购票模拟--多线程学习(一)

购票场景:多人在多个窗口购买N张票

多个窗口同时售票

public void run() {
    while(total>0){
		sale();
	}
}
public synchronized void sale() {
	if(total>0) {		
		System.out.println(Thread.currentThread().getName()+"卖了第"+count+"张票");
		total--;//票总数
        count++;//第X张票
	}
}

购票模拟--多线程学习(一)_第1张图片

通过synchronized 关键字修饰方法(也可以修饰代码块)达到同步效果,实现一票一卖。synchronizedtonb同步方法内还需要判断一次票数是total>0,因为在卖最后一张票时,其他线程会堵塞在同步方法外,待解锁后继续“卖票”,这样会出现超卖现象。

购票模拟--多线程学习(一)_第2张图片

由图1可以发现窗口26连续卖多张票,这是因为cpu按时间片给线程分配资源,一个线程在分配的时间片内连续运行直到阻塞、死亡或者时间片用完处于就绪状态。

客户排队买票

场景:一轮放票一个客户只能买一张(一次)。

一人一张,买到票的人就不能参与购票,从线程的角度就是出于阻塞或者死亡。

public synchronized void sale() {
	if(total>0) {		
		System.out.println("...");
		total--;
		count++;//一轮已卖票数
	}if(count==100) {
		count=0;
		notifyAll();//唤醒所有线程进行下一轮抢票
	}else {
		try {
			wait();//买到票的线程等待下一轮抢票
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 

wait()通知线程进入睡眠等待唤醒,这是线程会释放占有的“锁标识”,其他线程就可以进入synchronized的方法了。最后一个线程唤醒所有线程(notifyAll())进行新一轮的购票。

无锁购票

以上都是利用synchronized关键字获取锁来控制线程购票,获取锁与释放锁难免会占用系统资源(本场景无法体现)。因此考虑无锁方法实现购票。经过一番学习,了解到了CAS(compare and swap)

一个CAS方法包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。只有当V的值等于E时,才会将V的值修改为N。如果V的值不等于E,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。
具体知识可以参考:https://blog.csdn.net/liubenlong007/article/details/53761730

 模拟场景:10000张票分100轮,每轮有100人抢100张票,限一人一张

思路分析:每个线程有个独立变量来标识自己购买票的数量(1到100),同变量已卖票数(或者库存)。当每个线程持票数等于放票轮次序号(序号由0到99)时就可以进行抢票,即进入新的一轮抢票。

由于我的100个线程是使用同一TicketSales实例 ,因此ThreadLocal 类作为每个线程的独立变量。同一个ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。(也可以给每个线程独立的AtomicInteger类作为标记)。

具体使用CAS方法。代码如下:

public final void getNext() {   
         for (;;) {
            int current = count.intValue()/100;//放票轮次序号,第一次为0
            int next = current+1;
            if (num.get().compareAndSet(current, next)) {
            	AtomicInteger x = new AtomicInteger(next);
                num.set(x); 
                break;
            }else {
            	/*try {
            		Thread.sleep(0);
            	}catch(InterruptedException e){
            		e.printStackTrace();
            	}*/
            	Thread.yield();//Thread.sleep(0)--Thread.sleep(100)效果更稳定
            }
         }
	}

这里使用了Thread .yield()或者Thread.sleep(0)让线程使用完时间片之前就让步。如果线程在CAS方法里无限循环的话,是很占用系统资源的。买完一次票的线程必定进入无限循环直到下一次抢票,因此当线程无法跳出循环时就及时放弃剩下的时间片,通过Thread .yield()或者Thread.sleep(0)从运行状态转到就绪状态重新竞争CPU(也就是刚让出CPU的进程也还会参与CPU竞争)

在运行过程中还会出现卡克现象,由于系统分配时间片是随机的,总有倒霉蛋抢不到CPU。因此如果增加一点点sleep的时间,可以看到程序很顺畅的运行完(肉眼上)

具体测试情况可以can参考 下面博客:https://www.cnblogs.com/stg609/p/3857242.html

当然这里使用无锁(CAS)方法并不比synchronized提高性能多少,原因在于CAS更适合使用在多读少写的情形,大量写操作必然会因为自旋而占用大量资源。这里仅使用CAS方法作为一个学习例子。

如何有更好的方法请留言交流。

完整代码如下。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
class TicketSales implements Runnable{
	private AtomicInteger count = new AtomicInteger(0);
	public ThreadLocal  num = new ThreadLocal ();//每个线程下一张票
	private BlockingQueue bq;//存放票
	private CountDownLatch start;
    private CountDownLatch end;
	TicketSales(BlockingQueue bq,CountDownLatch start,CountDownLatch end ){
		this.bq = bq;
		this.start = start;
		this.end = end;		
	}
	public void run() {
		num.set(new AtomicInteger(1));
		try {
			start.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		while(count.intValue()<10000){
			try {
				if(count.intValue()<10000) {	
					System.out.println(Thread.currentThread().getName()+"买了"+bq.take().getTicket());
					count.incrementAndGet();					
					getNext();					
				}
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
		end.countDown();
	}
	public final void getNext() {   
         for (;;) {
            int current = count.intValue()/100;
            int next = current+1;
            if (num.get().compareAndSet(current, next)) {
            	AtomicInteger x = new AtomicInteger(next);
                num.set(x); 
                break;
            }else {
            	/*try {
            		Thread.sleep(0);
            	}catch(InterruptedException e){
            		e.printStackTrace();
            	}*/
            	Thread.yield();//Thread.sleep(0)-Thread.sleep(100)效果更稳定
            }
         }
	 }
}
public class ThreadLocalDemo {

	public static void main(String[] args)throws Exception {
		// TODO 自动生成的方法存根
		CountDownLatch start = new CountDownLatch(1);
		BlockingQueue bq = new LinkedBlockingQueue<>();
		TicketSales ts= new TicketSales(bq,start);		
		for(int i=1;i<101;i++) {			
			new Thread(ts,"Person"+i).start();
		}
		start.countDown();//全体人员准备就绪
		try {
			for(int i =1;i<10001;i++) {
				 bq.put(new Ticket("第"+i+"张票"));
			 } 
            end.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部票卖完了");
	}
}

 

你可能感兴趣的:(购票模拟--多线程学习(一))