常见问题
线程协作
1) wait/notify
wait : Object中,让出锁,阻塞等待。
notify/notiryAll:Object中,唤醒wait的进程,具体唤醒哪一个,要看优先级。
public class NotifyTest {
public static void main(String[] args) {
byte[] LOCK = new byte[0];
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1");
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(() -> {
synchronized (LOCK) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
LOCK.notifyAll();
}
});
t1.setPriority(1);
t2.setPriority(3);
t3.setPriority(2);
t1.start();
t2.start();
t3.start();
}
}
结果:
或者
结论:wait不释放锁,notifyAll具体唤醒哪一个,参考优先级,但是不是绝对按照优先级来执行。
2)sleep
sleep:Thread中。使线程沉睡一段时间,在沉睡期间,不释放锁。
public class SleepTest{
public static void main(String[] args) {
final byte[] lock = new byte[0];
new Thread(()->{
synchronized (lock){
System.out.println("start");
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}).start();
new Thread(()->{
synchronized (lock){
System.out.println("need lock");
}
}).start();
}
}
结果:
等待1S后
3)yield
yield:Thread中。不释放锁,运行中转为就绪,让出cpu给大家去竞争。当然有可能自己又抢了回来。
public class YieldTest{
public static void main(String[] args) throws InterruptedException {
final byte[] lock = new byte[0];
//让出执行权,但是锁不释放
Thread t1 = new Thread(()->{
synchronized (lock){
System.out.println("start");
Thread.yield();
System.out.println("end");
}
});
//可以抢t1,但是拿不到锁,白费
Thread t2 = new Thread(()->{
synchronized (lock){
System.out.println("need lock");
}
});
//不需要锁,可以抢t1的执行权,但是能不能抢得到,不一定
//所以多执行几次,会看到不同的结果……
Thread t3 = new Thread(()->{
System.out.println("t3 started");
});
t1.start();
t2.start();
t3.start();
}
}
或者
分析:
t3会插队抢到执行权,但是t2不会,因为t2和t1共用一把锁而yield不会释放
t3不见得每次都能抢到。可能t1让出又抢了回去
4)join
join:父线程等待子线程执行完成后再执行,将异步转为同步。注意调的是子线程,阻断的是父线
程。
public class JoinTest implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am sub");
}
public static void main(String[] args) throws InterruptedException {
Thread sub = new Thread(new JoinTest());
sub.start();
sub.join();
System.out.println("I am main");
}
}
结果:
死锁
public class DeadLock {
byte[] lock1 = new byte[0];
byte[] lock2 = new byte[0];
void f1() throws InterruptedException {
synchronized (lock1){
Thread.sleep(1000);
synchronized (lock2){
System.out.println("f1");
}
}
}
void f2() throws InterruptedException {
synchronized (lock2){
Thread.sleep(1000);
synchronized (lock1){
System.out.println("f2");
}
}
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(()->{
try {
deadLock.f1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
deadLock.f2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
死锁产生的必要条件:
- 互斥使用,即资源只能独享,一个占了其他都必须等。
- 不可抢占,资源一旦被占,就只能等待占有者主动释放,其他线程抢不走。
- 贪婪占有,占着一把锁不释放,同时又需要申请另一把。
- 循环等待,即存在等待环路,A → B → C → A。
总结来说:两个线程已经持有了锁,还想拿对方手里的锁,就会导致死锁的产生。
那我们怎么来排查死锁呢?可以用JDK自带的工具:
- jps + jstack pid
通过jps找到线程号,再执行jstack pid,找到 Found one Java-level deadlock:xxx - jconsole
执行jconsole,打开窗口,找到 线程 → 检测死锁 - jvisualvm
执行jvisualvm,打开窗口,双击线程pid,打开线程,会提示死锁,dump查看线程信息
电商实际应用
超时订单
在电商应用中,经常能够遇到订单超时的场景。当订单超时后,我们需要将订单状态修改,恢复库存等等这些操作。实际应用中,超时订单一般有以下几种设计方案。
设计方案
1)定死扫表:定时任务轮询扫描订单表,比对时间,超时的订单挑出来。
优点:实现简单
缺点:数据量大的时候,不适用。
2)延迟消费:定义一个队列用于延迟操作,在用户下单的时候,将订单放到这个队列中。
- DelayQueue:实现简单,不需要借助外部中间件。可借助db事务,down机丢失,同时注意内存占用
- 消息队列,设置延迟并监听死信队列,注意消息堆积产生的监控报警
- redis过期回调,redis对内存空间的占用
这里的案例我们采用延迟队列来实现。
订单超时类
public class OrderDto implements Delayed {
private int id; // 订单ID
private long invalid; // 超时时间
public OrderDto(int id, long deta){
this.id = id;
this.invalid = deta * 1000 + System.currentTimeMillis();
}
//倒计时,降到0时队列会吐出该任务
@Override
public long getDelay(TimeUnit unit) {
return invalid - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
OrderDto o1 = (OrderDto) o;
return this.invalid - o1.invalid <= 0 ? -1 : 1;
}
public int getId() {
return id;
}
public long getInvalid() {
return invalid;
}
}
定义监控类,启动守护线程,如果有超时任务,提交进线程池
@Component
public class OrderMonitor {
@Autowired
OrdersMapper mapper ;
//延时队列
final DelayQueue<OrderDto> queue = new DelayQueue<OrderDto>();
//任务池
ExecutorService service = Executors.newFixedThreadPool(3);
//投放延迟订单
public void put(OrderDto dto){
this.queue.put(dto);
System.out.println("put task:"+dto.getId());
}
//在构造函数中启动守护线程
public OrderMonitor(){
this.execute();
System.out.println("order monitor started");
}
//守护线程
public void execute(){
new Thread(()->{
while (true){
try {
OrderDto dto = queue.take();
System.out.println("take task:"+dto.getId());
service.execute(new Task(dto));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
//任务类
class Task implements Runnable{
OrderDto dto;
Task(OrderDto dto){
this.dto = dto;
}
@Override
public void run() {
Orders orders = new Orders();
orders.setId(dto.getId());
orders.setUpdatetime(new Date());
orders.setStatus(-1);
System.out.println("cancel order:"+orders.getId());
mapper.updateByPrimaryKeySelective(orders);
}
}
}
下单:
@RestController
@RequestMapping("/order")
@Api(value = "多线程取消超时订单demo")
public class OrderController {
@Autowired
OrdersMapper mapper;
@Autowired
OrderMonitor monitor;
@PostMapping("/add")
@Transactional
@ApiOperation("下订单")
public int add(@RequestParam String name){
Orders order = new Orders();
order.setName(name);
order.setCreatetime(new Date());
order.setUpdatetime(new Date());
//超时时间,取10-20之间的随机数(秒)
order.setInvalid(new Random().nextInt(10)+10);
order.setStatus(0);
mapper.insert(order);
//事务性验证
// int i = 1/0;
monitor.put(new OrderDto(order));
return order.getId();
}
}
加减库存
设计方案
1)rabbitmq异步排队:使用rabbitmq先排队,请求到来时之间入队,界面显示排队中,消费端逐个消费,同时扣减库存,界面轮询查询结果。可能会出现排队半天抢完的情况。
2)库存预热:使用缓存或内存变量,活动开始前从db中提取库存值初始化,请求到来时直接扣减,及时提醒。可能出现一种感觉,活动刚开始就抢没了……
实现
初始化库存:
//热加载数据
@GetMapping("/load")
@ApiOperation(value = "热加载库存")
//bean初始化后,立刻热加载
@PostConstruct
public Map load(){
products.clear();
List<Product> list = productMapper.selectByExample(null);
list.forEach(p -> {
products.put(p.getId(),new AtomicInteger(p.getNum()));
});
return products;
}
开始抢购:
//抢购
@GetMapping("/go")
@ApiOperation(value = "抢购")
public void go(int productId){
for (int i = 0; i < 10; i++) {
new Thread(()->{
int count = 0;
long userId = Thread.currentThread().getId();
while (products.get(productId).getAndDecrement() > 0){
count++;
//扔消息队列,异步处理
template.convertAndSend("promotion.order",productId+","+userId);
}
System.out.println(Thread.currentThread().getName()+"抢到:"+count);
}).start();
}
}
价格排序
设计方案
1)直接数据库sort,这种最典型
2)redis缓存zset获取,在商品列表缓存,web网站排序场景中常见
3)内存排序,有时候,需要复杂的运算和比较逻辑,sql sort操作表达不出来时,必须进入内存运算
我们这里使用方案3测试下。
实现
ForkJoinTask:
public class SortTask extends RecursiveTask<List<Product>> {
private List<Product> list;
public SortTask(List<Product> list){
this.list = list;
}
@Override
//分拆与合并
protected List<Product> compute() {
if (list.size() > 2){
//如果拆分的长度大于2,继续拆
int middle = list.size() / 2 ;
//拆成两个
List<Product> left = list.subList(0,middle);
List<Product> right = list.subList(middle+1,list.size());
//子任务fork出来
SortTask task1 = new SortTask(left);
task1.fork();
SortTask task2 = new SortTask(right);
task2.fork();
//join并返回
return mergeList(task1.join(),task2.join());
}else if (list.size() == 2 && list.get(0).getPrice() > list.get(1).getPrice()){
//如果长度达到2个了,但是顺序不对,交换一下
//其他如果2个且有序,或者1个元素的情况,不需要管他
Product p = list.get(0);
list.set(0,list.get(1));
list.set(1,p);
}
//交换后的返回,这个list已经是每个拆分任务里的有序值了
return list;
}
//归并排序的合并操作,目的是将两个有序的子list合并成一个整体有序的集合
//遍历两个子list,依次取值,两边比较,从小到大放入新list
//注意,left和right是两个有序的list,已经从小到大排好序了
private List<Product> mergeList(List<Product> left,List<Product> right){
if (left == null || right == null) return null;
//合并后的list
List<Product> total = new ArrayList<>(left.size()+right.size());
//list1的下标
int index1 = 0;
//list2的下标
int index2 = 0;
//逐个放入total,所以需要遍历两个size的次数之和
for (int i = 0; i < left.size()+right.size(); i++) {
//如果list1的下标达到最大,说明list1已经都全部放入total
if (index1 == left.size()){
//那就从list2挨个取值,不需要比较直接放入total
total.add(i,right.get(index2++));
continue;
}else if (index2 == right.size()){
//如果list2已经全部放入,那规律一样,取list1
total.add(i,left.get(index1++));
continue;
}
//到这里说明,1和2中还都有元素,那就需要比较,把小的放入total
//list1当前商品的价格
Float p1 = left.get(index1).getPrice();
//list2当前商品的价格
Float p2 = right.get(index2).getPrice();
Product min = null;
//取里面价格小的,取完后,将它的下标增加
if (p1 <= p2){
min = left.get(index1++);
}else{
min = right.get(index2++);
}
//放入total
total.add(min);
}
//这样处理后,total就变为两个子list的所有元素,并且从小到大排好序
System.out.println(total);
System.out.println("------------------");
return total;
}
}
开始排序:
public class SortController {
@Autowired
ProductMapper mapper;
@GetMapping("/list")
List<Product> sort() throws ExecutionException, InterruptedException {
//查商品列表
List<Product> list = mapper.selectByExample(null);
//线程池
ForkJoinPool pool = new ForkJoinPool(2);
//开始运算,拆分与合并
Future<List<Product>> future = pool.submit(new SortTask(list));
return future.get();
}
}