目录
一.何为并发
我们先从生活场景中找个例子;大家有没有排队买过奶茶,往往会排很长很长的队伍,因为要做奶茶,做奶茶的速度赶不上买的人的速度。那么这个场景去其实就是并发。
那么映射到我们的并发编程,就是一个接口假如同时有100个人访问,那么这个就属于并发。
那么什么是高并发?
高并发就是我系统能承受的并发数,所以,高并发其实是跟我们的系统有关的,比如一个小奶茶店,一个上午只能做500杯奶茶,那么如果有个3,400人排队,其实就是已经属于高并发了,那么500就是你系统能承受的最大的并发数。但是这3,400如果对于大的奶茶店,大奶茶店一个上午能做4,5000杯。那么3,400的量对于大店来说,根本就算不上是高并发。
如何支撑高并发?
这个要从一个接口的生命周期去考虑。除了从软件层面,比如不写死循环,接口字段设计,sql优化,缓存等等。
还有硬件资源:比如: 内存 磁盘 网卡 cpu 宽带等等。
CPU由之前的单核 现在变成了多核!那么CPU能提高并发能力,避免不了并发编程的主角:线程!
二.多线程的意义和使用
1.什么是线程
定义:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
操作系统中并发和并行的区别
- 并发是在一段时间内可以执行多个任务
- 并行是在同一时刻可以执行多个任务
线程存在的意义
- 异步执行
- 在多核CPU中,利用多线程可以实现真正意义上的并行执行
- 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务仓建不同的线程去处理,可以提升程序处理的实时性
- 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快
多线程的特点
- 异步
- 并行
2.线程的生命周期
Java线程从创建到销毁,一共经历6个状态
NEW:初始状态,线程被构建,但是还没有调用start方法之前
RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃CPU使用权,阻塞也分为几种情况
WAITING:等待状态,不带超时时间
TIME_WAITING:超时等待状态,超时以后自动返回
TERMINATED:终止状态,表示当前线程执行完毕
3.线程的创建
1.继承Thread类
Thread类也实现了Runnable接口
public class TreadDemo extends Thread{
@Override
public void run() {
//在run方法里写业务逻辑
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
TreadDemo t1 = new TreadDemo();
//star方法启动线程
t1.start();
2.实现Runnable接口
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t2 = new Thread(new RunnableDemo());
t2.start();
System.out.println("---------");
//简化的创建一个线程
new Thread(()-> {
System.out.println(Thread.currentThread().getName());
}, "aa").start();
}
}
3.实现Callable接口
这种方法创建的线程可以有返回值
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "hello~~";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(1);
Future<String> future = es.submit(new CallableDemo());
System.out.println(future.get());
}
}
4.线程池
后面写
4.线程的启动
new Thread().start()
start()底层是一个native方法 java本身不提供线程 线程是操作系统提供的
run和start的区别
run是线程执行逻辑的方法,start是启动线程的方法。
5.线程的通信
wait一定会释放锁
notify唤醒处于等待队列的线程
notifyAll
实现一个生产者和消费者的案例
Producer
public class Producer implements Runnable{
private Queue<String> bags;
private int size;
public Producer(Queue bags, int size) {
this.bags = bags;
this.size =size;
}
@Override
public void run() {
int i = 0;
while (true) {
i++;
synchronized (bags) {
while (bags.size() == size) {
System.out.println("bags以满");
try {
//wait 一定会释放锁
//要用while判断 而不是if 因为可能下次抢占到锁的还是自己
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//一秒生产一个
Thread.sleep(1000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
bags.add("bags-"+i);
System.out.println("生产者-生产:bags-" + i);
//唤醒阻塞状态的消费者
bags.notifyAll();
}
}
}
}
Consumer
public class Consumer implements Runnable{
private Queue<String> bags;
private int size;
public Consumer(Queue bags, int size) {
this.bags = bags;
this.size =size;
}
@Override
public void run() {
int i = 0;
while (true) {
i++;
synchronized (bags) {
while (bags.isEmpty()) {
System.out.println("bags以空");
//阻塞
try {
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//一秒消费一个
Thread.sleep(1000);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
String bag = bags.remove();
System.out.println("消费者-消费:bags-" + i);
//唤醒阻塞的生产者
bags.notifyAll();
}
}
}
}
Test
public class WaitNotifyDemo {
public static void main(String[] args) {
Queue<String> bags = new LinkedList<>();
int size = 10;
Thread p = new Thread(new Producer(bags,size));
Thread c = new Thread(new Consumer(bags,size));
p.start();
c.start();
}
}
结果:
生产者-生产:bags-1
生产者-生产:bags-2
生产者-生产:bags-3
消费者-消费:bags-1
消费者-消费:bags-2
消费者-消费:bags-3
bags以空
生产者-生产:bags-4
生产者-生产:bags-5
生产者-生产:bags-6
1.从刚刚的案例来看,其实wait/notify本质上其实是一种条件的竞争,至少来说,wait和notify方法一定是互斥存在的,既然要实现互斥,那么synchronized就是一个很好的解决方法
2. wait和notify是用于实现多个线程之间的通信,而通信必然会存在一个通信的载体,比如我们小时候玩的游戏,用两个纸杯,中间用一根线连接,然后可以实现比较远距离的对话。而这根线就是载体,那么在这里也是一样,wait/notify是基于synchronized来实现通信的。也就是两者必须要在同一个频道也就是同一个锁的范围内
6.线程的终止
1.中断
思考:如何正确的终止一个线程?
Thread.stop()? 这种方式显然是不准确的 因为stop方法是一个native方法会强行的终止线程 可能导致线程运行到一半就被终止。
正确中断方式一:我们可以使用共享的变量来控制线程中run方法执行结束.适合中断while循环,中断不了阻塞状态.
正确中断方式二:当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己.可以中断while循环,也可以中断阻塞状态,中断阻塞会抛出异常.
interrupt():通知线程终端,并把共享变量设为true
isInterrupted():获取共享变量的值
interrupted():获取共享变量的值 并把共享变量复位(改为false)
方式一代码演示:
public class StopDemo {
public static boolean stop = false;
static class stopThread implements Runnable {
@Override
public void run() {
while(!stop) {
System.out.println("线程持续执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new stopThread());
t1.start();
Thread.sleep(2000);
//终止线程
stop = true;
}
}
方式二: interrupt()
public class InterruptDemo {
static class interThread implements Runnable{
@Override
public void run() {
//Thread.currentThread().isInterrupted()默认是false
//interrupt (在JVM中)
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程持续执行");
try {
Thread.sleep(5000);
//阻塞状态被中断 Jvm会抛出异常
} catch (InterruptedException e) {
//会被动复位
e.printStackTrace();
//进行相应的处理
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new interThread());
t1.start();
Thread.sleep(2000);
//将属性改为true 友好中断
t1.interrupt();
}
}
2.中断复位
interrupted()
public class interruptedDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
while (true) {
System.out.println("线程执行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//中断标识 true
if (Thread.currentThread().isInterrupted()) {
System.out.println("before"+Thread.currentThread().isInterrupted());
//把标识 true 复位为 false
Thread.interrupted();//中断复位
System.out.println("after"+Thread.currentThread().isInterrupted());
}
}
});
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
7.join()、sleep()
1.join()
如果不传参数,会等到线程执行结束
传参数就会等待参数的时间
public class JoinDemo {
static int x = 0;
static int y = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
x = 1;
y = 1;
},"aa");
Thread t2 = new Thread(()-> {
x = y + 1;
},"bb");
//两个线程的执行顺序是不确定的 所以结果可能是1 也可能是2
t1.start();
t1.join(); //主线程等待 直到t1执行结束(t1线程一定要比t2线程优先执行) --会阻塞主线程
t2.start();
Thread.sleep(1000);
System.out.println(x);
}
}
join方法原理是调用线程的wait方法,唤醒是Jvm负责 调用notify方法
2.sleep()
Thread.sleap()的作用是什么?
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断。
Thread.sleap()工作流程
- 挂起线程并修改其运行状态
- 用sleep()提供的参数来设置一个定时器。
- 当时间结束,定时器会触发,内核收到中断后修改线程的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度
问题思考?
1.假设现在是2019-11-18 12:00:00.000,如果我调用一下Thread.Sleep(1000),在2019-11-1812:00:01.000的时候,这个线程会不会被唤醒?
2.Thread.Sleep(0)的意义?
要解决这两个问题我们就要明白操作系统中的调度算法。
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。
所以在问题1中我们调用了Thread.Sleep(1000),就是告诉操作系统在未来的1秒中我不想参与到cpu的竞争当中,而这1秒的时间过后可能别的线程正在使用cpu,而这个时候操作系统是不会重新分配cpu的,直到这个线程结束或者挂起,我们才可以和别的线程一起竞争。
问题2中调用Thread.Sleep(0),虽然sleep时间为0秒,但是操作系统会接受到命令,所以操作系统会根据线程优先级从新分配资源。
8.ThreaLocal
线程隔离机制 这个知识点面试经常会问
三.并发编程带来的挑战
1.线程安全问题
原子性、有序性、可见性
java中的原子性:指一个操作命令不可再分割
原子性问题举例:
public class Demo1{
public static int count = 0;
public static void incr() {
count++;
}
public static void main(String[] args) throws InterruptedException {
//创建1000个线程执行 incr方法
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
Demo1.incr();
}).start();
}
//睡3秒 确保1000个线程执行结束
Thread.sleep(3000);
System.out.println("结果" + count);
}
}
预期的结果应该是执行1000次所以结果应该是1000,但多次执行发现结果会小于等于1000;这时为什么呢?该如何解决呢?
count++;虽然再我们的java代码中是一行代码,但是在字节码文件中它不是三行代码。
第一个线程去执行getstatic得到的结果是0,没有执行下一步
第二个线程去执行,去执行getstatic得到的结果是0,执行iadd putstatic得到的结果1
第一个线程继续执行执行iadd putstatic得到的结果是1
该如何解决呢?
我们可以通过加锁来解决
public class Demo1{
public static int count = 0;
public static void incr() {
synchronized (Demo1.class) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
//创建1000个线程执行 incr方法
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
Demo1.incr();
}).start();
}
//睡3秒 确保1000个线程执行结束
Thread.sleep(3000);
System.out.println("结果" + count);
}
}
这样每次执行的结果都是1000
缓存与数据库的一致性问题? 并发导致的原子性问题
redis 1 DB2解决不了做到最终一致性就可以 如果要解决 synchronized 不建议
2.锁
Synchronized 同步锁
非公平锁,锁的标志存在对象头
同步锁的本质: 对共享资源加锁
同步锁的用法
修饰方法
public class Demo1{
public static int count = 0;
//修饰方法
public synchronized static void incr() {
count++;
}
}
修饰代码块
public class Demo1{
public static int count = 0;
public static void incr() {
//修饰代码块
synchronized (Demo1.class) {
count++;
}
}
}
同步锁的范围
- 类锁 同一个类一把锁
- 对象锁 同一个实例对象一把锁
同步锁的存储
那么锁是如何存储的呢? 对象头
当我们创建一个对象时,jvm底层会帮我们生成一个对象头(markOop)
同步锁的升级
加锁一定会带来性能开销->怎么优化? 不加锁(在不加锁的情况下解决线程安全问题)
CAS compare and swap 比较并替换
解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
偏向锁
加了锁,但是没有发生竞争,就会使用偏向锁.(默认是关闭的)
偏向锁一次CAS,失败会升级为轻量级锁。
轻量级锁
轻量级锁多次CAS(自旋锁)自适应自旋,通过多次重试来获取锁。
获取锁失败就会锁膨胀,变成重量级锁。
重量级锁
通过阻塞排队获取锁
死锁
什么是死锁:一组相互竞争的线程,相互等待,导致永久阻塞的现象
产生死锁的条件
- 互斥,共享资源×和Y只能被一个线程占用;
- 占有且等待,线程T1已经取得共享资源X,在等待共享资源丫的时候,不释放共享资源X;
- 不可抢占,其他线程不能强行抢占线程T1占有的资源;
- 循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源,就是循环等待。
思考锁的实现
- 锁的互斥特性->共享资源()->标记(0无锁,1代表有锁)
- 没有抢占到锁的线程?->释放CPU资源,[等待->唤醒]
- 等待的线程怎么存储?->数据结构去存储一些列等待中的线程,FIFO(等待队列)
- 公平和非公平(能否插队)
- 重入的特性(识别是否是同一个人?ThreadlD)
3.可见性问题
可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
Java中volatile关键字的作用:在多线程情况下保证可见性,volatile通过内存屏障禁止了指令重排序
加了volatile关键字,就会在我们的汇编语句上加入Lock
可见性是怎么产生的
为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。由于CPU高速缓存的出现。每个CPU有缓存,所以在访问共享资源的时候,肯定会导致问题数据不一致问题会有可见性问题
硬件层面解决
解决一致性问题 加锁
总线锁 性能差
缓存锁 缓存一致性协议 MESI
MESI
MESI可以解决数据一致性问题
- M.(Modify) 被修改的 了只缓存在当前的cpu,跟我们的主内存数据是不一致的
- E.(Exclusive) 独享的 数据只存在当前的CPU,并且没有被修改
- S.(shared) 共享的 数据存在多个CPU,并且与主内存是一致的
- I.(Invalid) 失效的 数据在这个CPU是失效
CPU缓存想修改某个数据的时候,先会通知总线去读。
storebuffer
cpu里面都是串行的,在等待或者通知的时候,不能做任何事情会有性能上的影响!
异步的思想
在等待或者通知的时候我继续去执行下面的事情如果通知完了或者等待失效完了我再回来更新、
cpu storebuffer我需要通知的变量我先放到storebuffer
cpu Invalid queue无效化队列先把要无效化的数据先放到这个queue
带来了好处性能又上来了,但是因为是异步的,所以又会带来数据一致性问题(storebuffer会带来指令重排问题)
内存屏障
内存屏障,可以解决异步带来的指令重排问题,但是性能肯定会降低。
软件层面解决
Java内存模型(JMM)
JMM是一种抽象的内存模型, 定义了共享内存中多个线程的读写规范
JMM是为了解决不同操作系统的差异化,提供一个统一的线程安全一致性模型,提供了在Java层面解决可见性和有序性问题的解决方案。
volatile可以根据不同的操作系统生成不同的内存屏障指令,从而达到线程安全的效果。
硬件层面提供了内存屏障来解决指令重排序问题
所以软件层面,JMM提供了四种内存屏障(对硬件层面内存屏障的封装)
Happens-Before模型
Happens-Before就是如果一个操作happens-before另一个操作,那么第一个操作的执行结果应该对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
如果重排序之后的执行结果,与按happens-before规则来执行的结果一致,那么这种重排序就不违法(JMM支持这种重排序)
程序顺序规则(as-if-serial语义)
一个线程中每个操作,happens-before于该线程中任意后续操作。
int a = 0;
int b = 0 ;
void test(){
int a=1;
int b=1;
int c=a*b ;
}
a happens-before b ; b happens-before c
传递性规则
a happens-before b ;且 b happens- before c; 则a happens-before c
Volatile变量规则
volatile修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作.
监视器锁规则
对一个锁的解锁,happens-before于随后对这个锁加锁。
int x=10;
synchronized(this){
//后续线程读取到的×的值一定12
if(x<12){
x=12;
}
x=12;
start规则
如果主线程执行操作t1.start()(启动线程t1),那么主线程的T1.start()前的操作happens-before于线程t1中的任意操作。
public class startDemo{
int x=0;
Thread t1=new Thread(()->{
//读取x的值一定是20
if(x==20){
}
});
x = 20;
//前面的操作一定 hanpens-before t1.start();
t1.start();
}
Join规则
Join方法可以保证结果对后续可见性,在前面有案例
final关键字提供了内存屏障的规则,所以也可以保证可见性
文章评论