互联网的特性
还是说到了互联网三高,高可用,高性能,高并发。 这一次我们重点关注一下高并发。 刚刚做了一个充值项目,涉及到高并发的场景, 尤其是支付过程,很容易造成线程安全的问题,我认为高并发第一解决的是线程安全;第二追求服务性能。 配合MQ可以实现秒杀功能
多线程的理解
如果我们只使用单线程的话,那每一次都要等到消费者完成了任务消费才能够轮到下一个消费者进行消费,对于CPU的性能是一种极大的浪费,可能大部分时间都只是用来读事务,而不是用来计算,中央处理器现在运算速度大约为每秒50亿次,也就是一个线程可能几毫秒就处理完成了,所以我们要关注使用多线程
多线程的四种实现方式
- 继承Thread类,重写里边的Run方法
- 实现Runnable接口,需要实现run方法,任务完成后交给Thread类
- 实现Callable接口,相比上一个可以得到一个返回值。
- 创建一个线程池,包括7个参数
线程池的7个参数【重要】
- 核心线程数量
- 最大线程数量
- 临时线程存活时间
- 临时线程存活时间单位(秒、分钟、小时)
- 队列容器的大小
- 创建线程的工厂
- 拒绝策略
-
ThreadPoolExecutor.AbortPolicy: //丢弃任务并抛出RejectedExecutionException异常。是默认的策略。 ThreadPoolExecutor.DiscardPolicy: //丢弃任务,但是不抛出异常 这是不推荐的做法。 ThreadPoolExecutor.DiscardOldestPolicy: //抛弃队列中等待最久的任务 然后把当前任务加入队列中。 ThreadPoolExecutor.CallerRunsPolicy: //调用任务的run()方法绕过线程池直接执行
-
怎样用SpringBoot来集成线程池
如何解决线程安全问题??
这里要谈到的是高并发场景,比如线程A和线程B同时提交订单,A提交了子订单,B也提交了子订单同时完成了支付,但是只剩下一件货,B没有收到短信同时A也完成了支付,这就造成了多线程的安全问题。
解决安全问题之前,我们要熟悉几种线程的状态
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
安全问题出现的条件
-
是多线程环境
-
有共享数据
-
有多条语句操作共享数据
比如说实现3个线程同时抢票,如果说不把线程锁住,很容易出现3个线程同时对一张票进行操作
所以,我们要控制线程的状态,可以上一把锁,这样就能很好避免线程的安全问题。
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
//t1进来后,就会把这段代码给锁起来
if (tickets > 0) {
try {
Thread.sleep(100);
//t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; //tickets = 99;
}
}
//t1出来了,这段代码的锁就被释放了
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
另一种方法是同步方法,它和同步代码块是效果是一样的,但是两者还是有一点小小的区别:同步方法就是把synchronized关键字加到方法上。 锁对象是this.
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
第三种常用的方法(RenntrantLock方法)
Lock锁【应用】
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
- ReentrantLock构造方法
| 方法名 | 说明 |
| --------------- | -------------------- |
| ReentrantLock() | 创建一个ReentrantLock的实例 |
- 加锁解锁方法
| 方法名 | 说明 |
| ------------- | ---- |
| void lock() | 获得锁 |
| void unlock() | 释放锁 |
- 代码演示
```java
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//synchronized (obj){//多个线程必须使用同一把锁.
try {
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// }
}
}
}
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
wait()和sleep()有什么区别呢?
- sleep可以控制线程的休眠时间,比如sleep(1000),wait需要唤醒线程,需要使用notify()
- sleep是线程的方法,而wait是Object,JVM自带的方法,每个对象都可以使用。
并发工具类
我们在数据存储的过程中也涉及到多线程,比如我们最常用的集合HashMap, HashMap并不能保证线程的安全问题,随时可能指向null; 另一个HashTable,HashTable在执行put()方法和get()方法是会将整张表用悲观锁锁起来,效率低下,一次只能执行一个操作,效率太低,有没有什么高效的方法吗???