购票场景:多人在多个窗口购买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张票
}
}
通过synchronized 关键字修饰方法(也可以修饰代码块)达到同步效果,实现一票一卖。synchronizedtonb同步方法内还需要判断一次票数是total>0,因为在卖最后一张票时,其他线程会堵塞在同步方法外,待解锁后继续“卖票”,这样会出现超卖现象。
由图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
具体使用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("全部票卖完了");
}
}