操作系统八股文
一,产生线程不安全的原因
1.线程是抢占式执行
导致两个线程里的先后顺序无法确定~
这样的随机性,是由操作系统内核决定的,是导致线程安全问题的根本所在。
2.多个线程修改同一个变量:
3.原子性
像++这样的操作就不是原子性的操作,我们可以加锁。
赋值或者一个步骤的操作是原子的
4.内存可见性
单线程下没有什么影响,但是到了多线程,就会引发不安全,
假设:
如果某个线程进行循环自增,那么涉及到多次LOAD,ADD,SAVE。
为了使结果读的更快,提高程序的效率,线程会把多次的LOAD和SAVE省略。变成了LOAD,ADD,ADD,SAVE,此时我另外一个线程去读取这个值,会发现值没有改变,是因为线程2还在add,值还在cpu里面。
(解决方法加上volatile,可以防止优化操作)
5.指令重排序
编译器会按照一个较为节省时间(或资源的路径去执行)
二.线程的所有状态
NEW:安排了,还没有开始工作
RUNNABLE:可工作的,又可以分为正在工作,和即将开始工作。
BLOCKED和WAITING:这几个都表示排队等待。
TERMINATED:工作完成了。
三, synchronized和volatile
synchronized:(既可以原子性,又可以保证内存可见性)
的本质功能,就是把并行变成串行,通过加锁,
锁竞争多个线程争同一个锁。
当然,为了防止程序员犯蠢,synchronized内部记录了这个锁是哪个线程所持有的。
synchronized修饰普通方法相当于,对this进行加锁,
synchronized修饰静态方法,相当于对类对象进行加锁。
内寸可见性:直接对内存进行操作。
volatile:
往往一个线程读一个线程写会用到这个volatile
计算机中一般理解成可变的,用来保证内存可见性,但不能保证原子性
四.Wait和notify(都是Object方法)
举例:比如,亿万富翁小明去银行的ATM机去取钱,同样,在他身后还有一群人准备取钱,小明发现ATM钱不够,可是他又着急取钱,此时就需要我们的工作人员把他带走,去取钱,让后面的人先取钱。
如果,工作人员,直接放,无限多的钱到ATM机中,这些人又需要同时的去竞争这个ATM。
这就好比,从一个就绪队列转换到阻塞队列中去,
Wait 的作用
1.将当前代码执行的线程,放到等待队列中。
2.释放当前锁
3. 满足一定条件被唤醒,重新获取到锁。
结果是 Wait之前证明是在Wait之前就结束了
Wait的使用
Wait的使用必须要在synchronized中且调用Wait的锁对象是同一个才可以使用
这个条件之一就是 Notify,有了Notify才能重新获取到锁。
Notify和NotifyAll
一个是唤醒Wait的线程,另外一个全部线程唤醒。
Wait和Sleep的区别
1.等待时间
Sleep可以指定一个固定时间进行阻塞等待,
Wait既可以指定时间,也可以无限等待。
2.唤醒方式
Wait使用notify来唤醒,而sleep等到时间到了或者interrup来唤醒
3.用途
Wait是用来调整线程的先后顺序,Sleep单纯让某一线程休眠,并不涉及多线程(虽然Sleep也能进行顺序控制,但是这种控制不可靠。)
五.单例模式以及单例模式的实现
(这里的设计模式就相当于棋谱一样,程序员按照相应的模式,来进行相应的操作)
懒汉模式:用到的时候才创建实例
public class Main {
static class Singleton{
//饿汉模式
private Singleton(){
}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance==null){
instance =new Singleton();
}
return instance;
}
}
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
我们仔细看这个代码会发现问题,当这个代码的main中没有实例的
instance的话,直接调用getInstance()方法会出错,涉及到线程安全问题。
所以我们需要把这个判断全部加上锁,保证这两个操作都是原子性的
(但是锁会导致资源开销变大,所以再进行判断,目的要让第一次成为线程安全的)
if(instance==null){
synchronized(Singleton.class)
if(instance==null){
instance =new Singleton();
}
return instance;
}
}
饿汉模式:开始的时候直接创建实例
public class Main {
static class Singleton{
//饿汉模式
private Singleton(){
}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
}
六.队列的分类
都知道有栈和对列,但是实际上对列才是最常用的,栈不常用。
这里的对列又分为。
优先队列:按优先级先进先出的对列
消息队列:对列里的数据有一定的分类信息,出队列的时候,不是单纯的先进先出,而是按照分类作为维度,让某个类的元素先进先出。
详细用到的场景:医院里面 做检查有很多类型。
阻塞对列:阻塞对列和以上两种对列不同,阻塞队列不为空,线程是安全的。
从以下两方面来谈
当对列为空,我们出对列会发生阻塞,直到有元素为止。
当对列满了,入队列会发生阻塞,直到有元素出队列为止。
有了阻塞对列就可以实现生产者消费者模型。
在计算机中,生产者是一组线程,消费者也是一组线程
判断一个代码是否好的标准指的是看代码,是否是高内聚,低耦合。
低耦合指的是两个代码片段关系是否,比较紧密。
用这个模型可以有效的解耦合,
1.解耦合
因为假设A要给B传输数据,只能a和b传,到时候我想让a传给c,需要该大量的代码。
因此我们使用生产者消费者模型,在a和b的中间加入一个阻塞队列。a把数据产生到对列,b从对列里面拿数据。这样就让耦合降低了。
2.肖峰填谷
有了大坝我们的水就可以得到有效的保护,避免水流太大。
把高峰填到低谷期。
当用户一大批需求冲入网关(服务器入口)会有一个阻塞队列将需求缓存,然后被具体服务器接受。避免服务器崩溃。
线程池
多进程是解决并发编程的方案,但是进程有点太重量了。(创建和销毁开销比较大)因此引入了线程,即便如此,频繁的线程的创建与销毁,还是会浪费资源。
因此有两种解决方法,
1.引入协程
2.引入线程池
线程池,指的是使用线程的时候,将提前创建好的线程,放在一个池子里,当我们需要线程的时候,直接从池子里面选取一个线程,当我们不用的话在放到池子里去。(这种操作全部在用户态进行就比较高效)
而创建和销毁线程,涉及到用户态和内核态的切换就比较低效了。
用户态和内核态
用户态就是应用程序执行的代码
内核态就是操作系统执行的代码
一般认为,用户态和内核态切换是一个开销比较大的操作
举例子:就好比我们去银行取钱,我们去取钱的话效率就比较高效,而直接让工作人员去取,效率就会变得低效,因为你是着急用的,而工作人员不着急,而是按部就班的取钱。自己取就是用户态,而给工作人员取就是用户态给内核态。
因此一旦把某个任务交给内核去做,啥时候事情可以做好,就非常难把握了。
ThreadPoolExecutor的构造方法参数都是啥意思
ThreadPoolExecutor里面包含的线程不是一成不变的,是能够根据任务量来自适应的,如果任务比较多,就会多创建一些线程,如果任务比较少,就少创建一些线程。
corePoolSize 核心线程数
maxmumPoolSize 最大线程数