目录
序
对多线程与高并发的知识梳理,内容比较简略,适合于快速的回想,内容参考马士兵老师的多线程与高并发,课程笔记https://download.csdn.net/download/qq_43337254/18355403
线程
进程与线程区别
什么是线程安全、什么是同步
什么叫阻塞队列与同步队列、等待队列
协程/纤程
run和start
-
直接在主线程调用run方法,相当于是,在执行main方法的时候先去执行run,run执行完后才去执行main里面的方法
-
在主线程调用start方法的时候回产生一个分支,这个分支会和主程序一块运行(交替执行)
创建线程的方法
面试问题:启动线程的三种方式,如下有两种,第三种可以回答通过线程池Executors.newCatchedThread。
- 继承一个Thread,重写run方法。
- 实现一个Runnable接口,重写里面的run方法。
- 要启动这样一个线程还是得new一个Thead,然后传入这个实现的Runnable接口的实例,再调用start方法
- Callable接口
sleep、yield、join
一般sleep用于线程的休眠,就是说使当前线程睡眠,然后去运行别的线程
yield相当于谦让的退出(返回就绪状态),当前线程调用之后,就相当于说是,暂时离开对这个CPU的使用,也就是跑到等待队列里面去了,然后别的线程就有机会去使用这个CPU,CPU就会从等待队列中取出一个线程去使用(也可能取出刚才退出使用的那个线程)
join就是加入,将一个线程加入到另一个线程里面,加进去之后呢,就得等加入的那个线程执行完之后才去执行原线程
Thread State
通过线程的getState()可以获得的当前线程状态
- new:创建一个线程
- Runnable:线程被线程调度器去执行(交给操作系统去执行)
- Ready(就绪):扔到CPU的等待队列里面,线程被挂起、或者是调用yield
- Running:线程真正被cpu调度的时候
- Teminated(终止):正常情况,线程执行完任务之后就进入这个状态,进入这个状态之后就不能在调用这个线程的start方法了。
- TimeWaiting:带参数的这些方法,都会让线程从Runnable进入这个状态waite、sleep、join、parkNanos、parkUntil
- Waiting:无期限的等待,waite、join、park
- Blocked:阻塞就是synchronized。
锁
多个线程去访问一个资源的时候必须要上锁。
Synchronized关键字
锁方法和锁this是一样的都是锁对象
锁静态方法就是锁类
可重入的概念
一个同步方法调另一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候任然会得到该对象的锁
为什么Synchronized要支持重入呢?
现在构建一个场景:父类Parent里面有一个Synchronized修饰的普通方法fun(),子类Child里面也有一个Synchronized修饰的同步方法fun(),现在如果在子类的fun里面调用super.fun()如果不支持重入那么父类和子类就会发生死锁。
异常发生
在程序之中如果发生异常,锁会被释放,然后这个会导致一个问题,就是,如果还有其他的线程正在等待这把锁,然后,这个锁因为异常的发生被释放掉了,这个时候,就会产生一个乱入的现象。
底层实现
这个主要是可以谈一个锁升级的过程可以参考我就是厕所所长!(一)
这里个人觉得应该是要说一下对象头里面的东西,Markword
然后就是monitorenter和monitorexit
volatile
首先来一个直观的例子
public class T {
volatile boolean running = true;//这里可以对比一下,加和不加volatile
void m() {
System.out.println("m start");
while(running) {
//略
}
System.out.println("m end");
}
public static void main(String[] args) {
T t = new T();
new Thread(new Runnable( run() { m() })).start();
try {
TimeUnit.SECONDS.sleep(1);
}catch{
e.prinStakTrace();
}
t.running = false;
}
}
运行结果:
- 不加volatile,就是程序结束不了end不会打印
- 加了volatile,程序会打印end
然后有个要记住的东西,就是说,volatile的作用有两个
- 保证线程可见性
- 禁止指令重排序
实现原理:
可见性:JVM的内存模型,是工作内存和主内存,变量就是重主线程读取,然后在工作线程去操作它,然后写回主内存,这个过程可以将他内部的八大原子操作说出来
有时间了解一下…这个线程可见性还有一个MESI这个东西(里面是一个cpu缓存一致性的协议)
重排序:再编译上JVM会对代码做一个优化,这个优化导致写出来的代码顺序,执行时不一定就是这个顺序。
读写屏障可以了解一下
举个例子:当a和b没有关联的时候,这个时候就会可能产生重排
int a = 1 ;
int b = 2 ;
int b = 3 ;
它的这个执行顺序可能会变成 b = 2 , b = 3 , a = 1;
这样不会影响最后的结果但是顺序变了
这里还有个先行发生原则,可以了解一下。
面试一般会问单例的双重锁里面的这个volatile为什么要加上(或者问为什么要双重锁)?
扩展一下,饿汉式单例(代码略)有个问题就是在不用的时候JVM也会加载并生成这个单例,所以出现了懒汉式。。。
然后改进…加了一个if(instance == null)生成实例的这样一个判断,然后又有一个问题,就是我想保证线程安全,于是在这个获取单例的方法上再加一个锁,这个是锁的原则的,锁的范围影响要尽可能小(锁细化),于是就要锁关键部分。然后就下面就是锁住之后内部为什么还要做一个判空处理的分析。。。
核心代码
if(intance == null){
Synchronized (Singleton.class){
if(intance == null){
try{
Thread.sleep(1);
}catch(Exception e){
e.printStackTrace();
}
intance = new Singleton();
}
}
如果现在线程A和B进入这个代码块,都进入第一重判断,为空,A然后进行实例化,现在如果不经过第二重锁的话,线程B也会随之再实例化一次
问题回到刚才,为什么要加volatile…
intance = new Singleton();
这句代码在JVM里面其实是三步:
- 对象申请内存(这个时候对象是个默认值)
- 成员变量初始化
- 内存内容赋值给这个实例
然后这个重排序可能就会这样。。将第三步这个提前,也就是说成员变量没有初始化
然后第二个线程进入之后发现不为空,然后就会直接使用这个值,而不进入第二重检测的代码
面试问题volatile能代替Synchronized吗?
首先volatile不能保证原子性;
CAS
解决同样的问题的更高效的方法,使用AtomcXXX类
AtomcXXX类本身方法都是原子性的,但是不嫩保证多个方法连续调用的原子性,就好像是vector里面的方法是线程安全的,但多个线程去访问不做同步处理还是会不安全。
比如使用AtomicInteger就不用Synchronized去同步,因为保证了原子性
里面的原理就是一个CAS
cas(V ,Expected,NewValua)
if (V==E)
V=New
otherwise try again or fail
cas是cpu原语支持,就是说cas的操作是cpu指令级别上的支持,中间不能被打断。
举个例子:有一个长度为3的链表,现在想在后面插入一个节点,此时的期望值就是3(长度),然后如果是3就进行插入操作。
ABA问题:现在有一个变量A 为1,现在再做一个cas操作,将1变为2,但是现在可能有种情况就是,cas可能进行两次操作,先变为2在进行一次操作,变回1,那么就感受不到值的改变
解决方法:版本号….略(atomicStampedReference去解决)
atomic类不用锁是因为内部的Unsafe类:这个是个单例类,返回的是theUnsafe的这个值(这是jdk11.0里面的,而1.8需要反射才可以)
对于使用atomicLong来控制变量自增时的线程安全,还有一种LongAdder也可以实现。
在线程数特别多的情况下,LongAdder的效率很高,内部采用的分段锁(也是cas操作)
ReentrantLock
Lock lock = new ReentrantLock();
try{
lock.lock();
}finally{
lock.unlock();
}
- 锁的使用完后要手动解锁,lock要写在try里面
- 使用ReetrantLock的tryLock()进行尝试锁定,不管锁定与否,方法都将继续执行
- 中断等待,使用lock.lockInterruptibly()锁然后用interrupt()打断
- 公平锁,由于在一个等待队列里,如果队列里面有线程,那就原有线程先来,如果是非公平的那就直接抢占
CoutDownLatch
门栓的概念,就是一个倒数的计数,在计数为0之后才进行线程的下一步操作,用法如下:
//设置CountDownLatch的一个初始值
CountDownLatch latch = new CountDownLatch(100)//假设一百个线程
每次过一个线程就latch.countDown();//latch减小1
然后为0的时候就调用latch.await();//将线程栓住,在等于0的时候才打开
用于等着线程结束之后,然后我们在进行一个其他操作。
CycliBarrier
CyclicBarrier barrier = new CyclicBarrier(20) ;
barrier.await();
在线程数到20后才放行,继续线程之后的操作
可用于限流等等
Phaser
ReadWriteLock
Semaphore
Exchanger
LockSurport
面试相关
- 实现一个容器,提供两个方法,add、size 。 写两个线程,线程1添加10个元素到容器中。线程2 实现监控元素的个数。当个数到5个时。线程2给出提示并结束。
- 写一个固定容量同步容器, 拥有put、get方法,以及getCount 方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
AQS
ReentrantLock内部是AQS,里面又用了volatile+cas,设计模式用的是模板方法模式