直接上案例
模拟多线程抢票
public class Site implements Runnable{
private int count = 10; //记录剩余票数
private int num = 0; //记录买到第几张票
@SneakyThrows
public void run() {
while (true) {
//没有余票时,跳出循环
if (count <= 0) {
break;
}
//第一步:修改数据
num++;
count--;
TimeUnit.SECONDS.sleep( 1);// 模拟网络延时
//第二步:显示信息
System.out.println(Thread.currentThread().getName() + "抢到第" + num + "张票,剩余" + count + "张票!");
}
}
//快速测试
public static void main(String[] args) {
Site site = new Site();
Thread person1 = new Thread(site, "学明");
Thread person2 = new Thread(site, "二明");
Thread person3 = new Thread(site, "黄牛");
System.out.println("********开始抢票********");
person1.start();
person2.start();
person3.start();
}
}
咱们可以看到 “学明” 明显抢不过 “黄牛” ,肯定是回不去家的!!!
不是从第1张票开始
存在多人抢到一张票的情况
有些票号没有被抢到
多个线程操作同一共享资源时,将引发数据不安全问题,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
解决
使用线程同步机制的使用
同步代码块。
同步方法。
锁机制。
1.同步代码块
synchronized表示同步,可以修饰代码块,也可以修饰方法。如果synchronized修饰代码块,那么这个代码块就是同步代码块。
语法:
synchronized (锁对象) {
...
}
说明:
锁对象就是一个普通的Java对象,锁对象可以是任何类型的,可以是Student,Object, ArrayList
锁对象仅仅起到一个标记的作用,除此之外,就没有其他含义了。
同步代码块的作用:
只有持有锁的线程才能够进入到同步代码块。
线程同步可以解决线程安全问题,会牺牲效率
public class Site implements Runnable {
private int count = 10; //记录剩余票数
private int num = 0; //记录买到第几张票
//创建对象,当做锁对象,该对象可以是任何类型的,该对象仅仅起到一个标记作用。
//锁对象一定要是唯一的。 多个线程使用的锁必须是同一个锁对象,否则也不能保证线程安全。
Object lock = new Object();
//在run方法中编写线程要执行的任务,线程要执行的任务是买票任务。
@SneakyThrows
public void run() {
//定义死循环模拟售票窗口一直卖票的过程
while (true) {
//同步代码块
//当线程执行到同步代码块时会看一下同步代码块上面有没有锁。
//如果同步代码块上面有锁,那么线程会获取到锁,然后进入到同步代码块中。
//如果同步代码块上面没有锁,那么线程会在同步代码块位置一直等着获取锁
synchronized (lock) {
// 没有余票时,跳出循环
if (count <= 0) {
break;
}
// 第一步:修改数据
num++;
count--;
TimeUnit.SECONDS.sleep( 1);// 模拟网络延时
// 第二步:显示信息
System.out.println(Thread.currentThread().getName() + "抢到第"
+ num + "张票,剩余" + count + "张票!");
}
}
//当线程离开同步代码块,线程会释放掉自己的锁。这样线程就又可以去竞争这个锁了,哪个线程能抢到,哪个线程去执行。
}
public static void main(String[] args) {
Site site = new Site();
Thread person1 = new Thread(site, "学明");
Thread person2 = new Thread(site, "二明");
Thread person3 = new Thread(site, "黄牛");
System.out.println("********开始抢票********");
person1.start();
person2.start();
person3.start();
}
}
学明运气不佳,还是抢不过黄牛,荒废了单身18年的手速,还是回不去家!!!
2.同步方法
如果synchronized修饰方法,那么该方法就是同步方法,同步方法同样可以解决线程安全问题。
同步方法格式
修饰符 synchronized 返回值类型 方法名(参数列表) {
方法体;
return 返回值;
}
优缺点
同步代码块:
优点:灵活,可以对任意代码进行同步。
缺点:语法不如同步方法简洁
同步方法:
优点:语法简洁。
缺点:不如同步代码块灵活。 是直接将整个的方法体都加了同步。
public class Site implements Runnable {
private int count = 10; //记录剩余票数
private int num = 0; //记录买到第几张票
public void run() {
while (true) {
if (!sale()) {
break;
}
}
}
//定义同步方法,用来卖票
//同步方法,相当于将整个的方法体都加了同步代码块。
// 同步方法:卖票
@SneakyThrows
public synchronized boolean sale() {
if (count <= 0) {
return false;
}
// 第一步:修改数据
num++;
count--;
TimeUnit.SECONDS.sleep( 1);// 模拟网络延时
// 第二步:显示信息
System.out.println(Thread.currentThread().getName() + "抢到第" + num
+ "张票,剩余" + count + "张票!");
if (Thread.currentThread().getName().equals("黄牛")) {
return false;
}
return true;
}
public static void main(String[] args) {
Site site = new Site();
Thread person1 = new Thread(site, "学明");
Thread person2 = new Thread(site, "二明");
Thread person3 = new Thread(site, "黄牛");
System.out.println("********开始抢票********");
person1.start();
person2.start();
person3.start();
}
}
学明离家18年,已经泪流满面,为了让学明快点回家,十年都不用再买票了;
3.锁机制
ReentrantLock :它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
ReentrantReadWriteLock :它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
LockSupport :它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
Lock是一个接口,如果要用,需要使用实现类,最常用的实现类是ReentrantLock
void lock():获取锁
void unlock():释放锁。
/*
使用锁实现多线程
*/
public class Site implements Runnable {
private int count = 10; //记录剩余票数
private int num = 0; //记录买到第几张票
Lock lock = new ReentrantLock();
@SneakyThrows
public void run() {
//定义死循环模拟售票窗口一直卖票的过程
while (true) {
lock.lock();//获取锁
// 没有余票时,跳出循环
if (count <= 0) {
lock.unlock();
break;
}// 第一步:修改数据
num++;
count--;
TimeUnit.SECONDS.sleep( 1);// 模拟网络延时
// 第二步:显示信息
System.out.println(Thread.currentThread().getName() + "抢到第"
+ num + "张票,剩余" + count + "张票!");
lock.unlock();//释放锁
}
}
public static void main(String[] args) {
Site site = new Site();
Thread person1 = new Thread(site, "学明");
Thread person2 = new Thread(site, "二明");
Thread person3 = new Thread(site, "黄牛");
System.out.println("********开始抢票********");
person1.start();
person2.start();
person3.start();
}
}
CAS
除了加锁以外,还有无锁的方式,就比如CAS
CAS(Compare and Swap)操作是一种并发编程中常用的原子操作,用于实现多线程环境下的无锁同步。CAS操作可以实现对共享变量的原子性读取和更新。
库存举例
@Component
public class Inventory {
private AtomicInteger count;
public Inventory() {
this.count = new AtomicInteger(10); // 初始化库存数量,实际开发中,可以从数据库或缓存获取数来源
}
public int getCount() {
return count.get();
}
public boolean updateCount(int newValue) {
// 获取余额的最新值
int prev = count.get();
if (prev <= 0) {
return false;
}
// 要修改的余额
int next = prev - newValue;
if (next < 0) {
return false;
}
return count.compareAndSet(prev, next);
}
}
@RestController
public class testController {
@Resource
private Inventory inventory;
private static Logger logger = Logger.getLogger(testController.class.getName());
@RequestMapping(value = "testCAS", method = {RequestMethod.POST})
public void simulateInventoryUpdate(@RequestParam Integer newValue) {
logger.info("传入数量:" + newValue);
// 获取库存数量
int currentCount = inventory.getCount();
logger.info("当前库存数量:" + currentCount);
// 模拟更新库存
boolean success = inventory.updateCount(newValue);
if (success) {
logger.info("库存更新成功");
} else {
logger.error("库存更新失败");
}
// 更新后的库存数量
int updatedCount = inventory.getCount();
logger.info("更新后的库存数量:" + updatedCount);
}
}
成功
失败