操作系统初识
计算机:
硬件的组成:冯诺伊曼体系结构/普林斯顿体系结构
软件的组成:(OS操作系统)内核部分【内核态】、用户部分【用户态】
进程:系统分配资源的最小单位
线程:系统调度的最小单位。轻量级“进程”
优点:创建的时候占用更少的资源,并且多个线程之间可以共享资源
注意事项:cpu和输入设备或输出设备之间不能直接交互,必须依靠中间的存储部分(内存)
内存 vs 硬盘:
1.内存的读写速度要远远大于磁盘,内存读写速度纳秒,硬盘读写速度是微秒级别;
2.内存的价格远大于硬盘;
3.内存中的数据不能持久化,而磁盘可以持久化
PCB(进程管理模块)结构体
1.pid(主键ID、身份标识)
2.状态信息(就绪、运行、阻塞、终止)
3.优先级
4.记账信息(防止cpu资源分配不均)
5.一组指针(需要使用的资源)
6.上下文(当分配到cpu资源时执行,然后没有cpu资源之后,暂时保存自己当前的状态,等待下一次执行,这个过程就叫做一个上下文 )
进程 vs 线程
线程:1.系统调度资源的最小单位
线程是进程执行的最小单位,也是进程执行的实际单位。
2.一个进程中最好包含一个线程;
3.线程必须依附进程当中。线程是进程实质工作的一个最小单位
进程就好比工厂,线程就好比一个一个流水线。
进程是系统分配资源的最小单位;线程是系统调度的最小单位。
进程不可以共享资源;而线程可以共享资源。
线程可以共享的资源:
1.打开的文件
2.内存(对象)
线程不可共享的资源:
1.上下文
2.记账信息
3.状态
4.线程栈信息
5.优先级
当线程的数量到达某个合适的值是最好的,如果有太多的线程,就会出现线程之间的争抢和cpu的过度调度问题,而cpu调度是需要消耗系统资源,所以线程不是越多越好。
那多少线程是最好的?
答:要看具体的应用场景。
密集计算的cpu任务、io(文件读写)型任务。
当使用的场景是计算型任务时,线程的数量等于cpu的数量是最好的,io型任务理论上线程的数量越多越好。
线程的创建方式:
1.继承thread类是实现线程的创建线程
2.实现runnable接口方式
3.实现callable接口方式(可以得到线程执行之后的结果)
继承thread类的方式的缺点:java语言的设计当中只能实现单继承,如果继承了thread类,也就不能继承其他类。
第二类:实现runnable 接口的方式(java不能多继承,但可以实现多个接口)
第三类,第六种写法,实现callable
面试题:使用两个线程来打印“AABBCCDD”
线程分类:
1.后台线程(守护线程)
2.用户线程(默认线程)
守护线程是用来服务用户线程,用户线程就是上帝,守护线程就是服务员。
进程退出:没有用户线程运行,进程就会结束。
守护线程的注意事项:
1.守护线程设置必须在调用start();
2.设置守护线程在开始线程start()之后,那么程序就会报错,
3.在守护线程里面创建的新线程,默认情况下全部都是守护线程;
Start 和run之间的区别;
1.run属于普通方法;而start属于启动线程的方法。
2.run方法可以执行多次,而start方法只能执行一次。
线程中断:
1.使用全局定义的变量来终止线程。
2.使用线程提供的终止方法interrupt 来终止线程。
3. 使用线程提供的方法stop来终止
使用全局自定义标识终止线程执行的时候比较温柔,当它收到了 终止指令之后,会把当前手头的任务执行完再终止,使用interrupt在收到终止指令之后会立马结束执行。
两种终止线程的方式:
Interrupt() 和 isinterrupted()的区别
Interrupt是全局的方法,它判断完之后会重置线程的状态。
t1.interrupt();
interrupt()方法的作用就是将线程中的终止状态从默认的false改为true。
线程的状态:
1.新建(new)
2.执行状态(runnable【running/ready】)
3.timed_watting(超时等待状态)
4.waiting(等待)
5.阻塞(blocked)
6.终止状态
线程安全问题:
1.CPU抢占执行(万恶之源);
2.非原子性;
3.编译器优化(代码优化);
编译器优化在单线程下没问题,可以提升程序的执行效率,但是在多线程下就会出现混乱,从而导致线程不全的问题
4.内存可见性 volatile
5.多个线程同时修改同一个变量
Java中解决线程安全问题的方案:
1.锁(让多线程排队执行)
2.使用私有变量
锁:
1.synchronized 加锁和释放锁【jvm层面的解决方案,自动帮我们进行加锁和释放锁】
实现原理(操作系统/jvm/java)
2.lock 手动锁【程序员自己加锁和释放锁】
死锁:
定义:在多线程编程中(两个或两个以上的线程)因为资源抢占而造成线程无限等待的问题
线程和锁的关系:1对多;一个线程可以拥有多把锁,而一把锁只能被一个线程拥有
死锁关键代码:
synchronized(lockA){
Thread.sleep(1000);
synchronized(lockB){
//...
}
}
死锁操作的4个条件(同时满足):
1.互斥条件:一个资源只能被一个线程持有,当被一个线程持有之后就不能被其他线程持有(不可改变)
2.请求拥有条件:一个线程持有了一个资源之后,又试图请求另个资源(可被修改的)
3.不可剥夺条件:一个资源被一个线程拥有之后,如果这个线程不释放此资源,那么其他线程不能强制获得此资源(不可改变)
4.环路等待条件:多个线程在获取资源时形成了一个环形链(可被修改的)
如何解决死锁问题?
从以下两个条件入手,修改以下条件任意一个:
1.请求拥有条件
2.环形等待条件(最容易实现)
解决死锁可以通过控制获取锁的顺序来解决;破坏条件4
线程的工作方式:
1.现在自己的工作内存中找变量
2.
Sleep 休眠缺失: 必须有明确的结束数据。
线程的通讯机制:一个线程的动作可以让另一个线程感知到就线程通讯。
wait(休眠)/notify(喊醒)/notifyAll(唤醒全部)
面试问题1: wait 为什么要加锁?
答:wait在使用的时候被需要释放锁,在释放锁之前必须要有一把锁,所以要加锁。
面试问题2:wait为什么要释放锁?
答:wait默认是不传任何值的时候表示永久等待,这样就会造成一把锁被一个线程一直持有,为了避免这种问题的方法,所以在使用wait时一定要释放锁。
wait(休眠)/notify(喊醒)/notifyAll(唤醒全部)使用注意事项:
1.在使用以上方法时必须加锁;
2.加锁对象和 wait(休眠)/notify(喊醒)/notifyAll的对象必须保持一致
3.一组 wait(休眠)/notify(喊醒)/notifyAll必须是同一个对象
4. notifyAll只能唤醒当前等待线程
Thread.sleep(0)VS Object.lock(0) 区别:
1.sleep它是Thread的静态方法:而lock是Object的方法;
2.sleep(0)立即触发一次cpu资源的抢占,lock(0)表示永久等待下去。
wait和sleep的区别?
相同点:
1.都可以让当前的线程休眠
2.都必须要处理一个interrupt异常
不同点:
1.wait来自于object中的一个方法,而sleep来自于thread
2.传参不同,wait可以没有参数,而sleep必须有一个大于等于0的参数
3.wait使用时必须加锁,sleep使用时不用加锁
4.wait使用时会释放锁,而sleep不会释放锁。
5.wait默认不传参的情况下会进入WAITING状态,而sleep会进入 TIMED_WAITING
为什么wait会放到object中而不是thread?
答:wait操作必须要加锁和释放锁,而锁又是属于对象级别而非线程级别(线程和锁是一对多的关系,也就是一个线程可以拥有多把锁),为了灵活起见(一个线程当中会有多把锁),就把wait放在object 。
LockSupport注意事项:
wait 和 locksupport区别:
相同点:
1.两个都可以让线程进行休眠
2.二者都可以参数或不传参,并且二者线程状态也是一致的
不同点:
1.wait必须要配合synchronized一起使用(必须加锁),而locksupport不许加锁。
2.wait只能唤醒全部或随机的一个线程,而locksupport可以唤醒指定线程。
volatile轻量级解决“线程安全”的方案:
private static volatile boolean flag = false;
volatile作用:
1.进制指令重排序
2.解决线程可见性的问题。实现原理:
注意事项 :volatile 不能解决原子问题。
锁操作的关键步骤:
1.尝试获取(如果成功拿到锁加锁,不成功排队等待)
2.释放锁
synchronized注意事项:
在进行加锁操作的时候,同一组业务锁对象一定是同一个锁对象。
synchronized原理:
1.操作:互斥锁mutex
2.JVM:帮我们实现的监视器锁的加锁和释放锁的操作
3.Java:
a)锁对象mutex
b)锁存放的地方:变量的对象头
Lock使用:
//1.创建手动锁
Lock lock = new ReentrantLock();
//2.加锁
lock.lock();
try{
number++;
}finally{
//3.释放锁
lock.unlock();
注意事项:一定要把lock()放在try外面。
1.如果将lock()方法放在try里面,那么当try里面的代码出现异常之后,那么就会执行finally里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。
2.如果将lock()方法放在try里面,那么当执行finally里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。
synchronized 锁机制是非公平锁
公平锁可以按顺序执行,而非公平锁执行的效率更高
在java中所有锁默认的策略都是非公平锁
Lock默认的锁策略也是非公平锁,但是lock可以显示的声明为公平锁。
synchronized使用场景
1.使用synchronized 来修饰代码块(加锁对象可以自定义);
2.使用synchronized 来修饰静态方法(加锁对象是当前的类对象)
3.使用synchronized 可以用来修饰普通方法 (加锁对象是当前类的实例)
Lock只能用来修饰代码块
1.volatile和synchronized 有什么区别?
Volatile可以解决内存可见性问题和禁止指令重排序,但volatile不能解决原子性问题:synchronized 是用来保证线程安全,也就是synchronized 可以解决任何关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作;原子性问题。。。)
2.synchronized 和lock之间的区别?
1.synchronized 既可以修饰代码块,又可以修饰静态方法或者普通方法,而lock只能修饰代码块。
2.synchronized 只有非公平锁策略,而Lock既可以是公平锁也可以是非公平锁。(Reentrantlock 默认是非公平锁,也可以构造函数设置true声明它为公平锁)。
3.使用Reentrantlock更加灵活(比如tryLock)
4.synchronized 是自动加锁和释放锁的,而Reentrantlock需要自己加锁和手动释放锁。
线程的缺点:
1.线程的创建需要开辟内存资源:本地方法栈、虚拟机栈、程序计数器等线程私有变量的内存,频繁的创建和消耗会带来一定的性能开销。
2.使用线程不能很好的管理任务和友好的拒绝任务。
3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
线程池的创建方式总共包含7种:
线程池创建方式1:创建固定个数的线程池
经典易错题:创建了10个线程来执行2个任务,问当前程序创建了几个线程?
答:启动2个线程。
线程池的执行流程:当拿到一个任务之后,会判断当前线程池里面的线程数量是否到达了最大值,如果没有达到创建新的线程执行任务;当任务来了之后,线程线程池的线程数量已经是最大值,并且没有空闲线程,当前的任务会被放到线程池的任务队列里面等待执行。
自定义线程池行为(设置线程池命名规则、线程池的优先级)
线程池创建方式2(创建带缓存的线程池):
适用场景:有大量的短期任务的时候。
线程池创建方式3(创建可以执行定时任务的线程池):
参数1:线程池的任务
参数2:定时任务延迟多长时间开始执行
参数3:定时任务的执行频率
参数4:配合参数2和参数3使用的时间单位
线程池创建方式4(创建单个执行定时任务的线程池):
经典面试题:单个线程的线程池有什么意义?
答:1.无需频繁的创建和销毁线程。2.可以更好的分配和管理以及存储任务(任务队列)。
线程池创建方式5(单个线程的线程池):
线程池创建方式6( JDK 8+): 根据当前的工作环境(CPU 核心数、任务量)异步线程池
synchronized:同步
同步:按照某种规则按顺序执行的就叫同步。
同步执行的流程:
1.main调用线程池
2.线程池执行完之后
3.关闭线程池,main也会随之关闭
异步执行的流程:
1main调用异步线程池
2.异步线程池后执行,对于main线程来说异步线程池已经执行完成,关闭main线程
线程池创建方式7:
Executors 创建线程池的问题:
1.线程数量不可控(线程的过度切换和争取-》程序执行比较慢)
2.任务数量不可控(任务数无限大Integer.MAX_VALUE),当任务量比较大的情况下就会造成内存溢出异常(00M)
线程:
1、线程创建需要开辟一定的内存空间(本地方法栈、虚拟栈、程序计数器)性能开销。
2、当任务量比较大的情况下,不能友好的进行拒绝。
线程池的优点:
1、避免频繁创建和销毁所带来的性能开销;
2、可以友好的拒绝任务;
3、更多的功能;执行定时任务。
线程池6种创建方式:
1、创建固定的线程池(任务数取向无限大、建议谨慎使用);
2、创建带缓存的线程(根据任务的数量生成对应的线程数,所以它适用于短期大量任务);
3、创建可以执行定时任务的线程池。
4、创建单个执行定时任务的线程池。
5、创建单个线程池「a)频繁的创建和消耗b)更好的分配和执行任务,并且可以将任务放到任务队列」
6、根据当前的工作环境(cpu、任务量)生成对应的线程池;
线程池执行流程:
线程池的2种执行方式:
1.execute执行(new Runnable)无返回值的
2.submit执行(new Runnable)无返回值/new Callable有返回值)
执行方法区别:
1.execute 只能执行 Runnable任务,它是无返回值;submit它既能执行 Runnable无返回值的任务,也能执行Callable有返回值的任务。
2.execute执行任务如果有00M异常会将异常打印到控制台;submit执行任务出现了00M异常时不会打印异常。
线程池的特征:
线程池相比于线程来说是长生命周期,即使没有任务了,也会运行并等待任务。
线程池的关闭:
1.shutdown:拒绝新任务加入,等待线程池中的任务队列执行完之后,再停止线程池。
2.shutdownNow:拒绝执行新任务,不会等待任务队列中的任务执行完成,就停止线程池。
ThreadLocal使用方法:
1.set:将私用变量存储到线程中
2.get:取线程中的变量。
3.remove:从线程中移除变量(脏读、内存溢出)
4.initialvalue:初始化
5.withinitial:初始化
initialvalue + get 正常存取操作。
initialvalue + set+ get?
A.先执行初始化方法、再执行set方法、再执行get
B.先执行initialValue、再执行get
C.先执行set方法、再执行get方法
面试题:什么情况下不会执行initialValue?为什么不会执行?
答:set之后就不会执行了。ThreadLocal是懒加载的,当调用了get方法之后,才会尝试执行initialValue(初始化)方法,尝试获取一下ThreadLocal set的值,如果获取到了值,那么初始化方法永远不会执行。
withInitial方法(静态方法)
ThreadLocal缺点:
1.不可继承
子线程不能读取到父线程的值。
2.脏读(脏数据)
在一个线程中读取到了不属于自己的数据。
线程使用ThreadLocal不会出现脏读->每个线程都使用的是自己的变量值和 ThreadLocal。
线程池里面使用ThreadLocal就会出现脏数据,线程池会复用线程,复用线程之后,也会复用线程中的静态属性,从而导致了某些方法不能被执行,于是就出现了脏数据的问题。
脏数据的解决方案:
1、避免使用静态属性(静态属性在线程池中会复用)
2、使用remove解决。
3.内存溢出问题(最长出现的问题)、打开数据库连接但未关闭
内存溢出:当一个线程执行完之后,不会释放这个线程所占用内存,或者释放内存不及时的情况都叫做内存溢出。->线程不用了,但线程相关内存还得不到及时的释放。
内存溢出:ThreadLocal+线程池
线程池是长生命周期。
ThreadPool -> Thread -> ThreadLocal ->内存不会关闭 ->00M
分析内存溢出:
线程池是长生命周期,而线程是执行完任务线程就结束了(线程相关的资源都会释放掉)。
经典面试题:hashmap 和 threadlocalmap 处理hash冲突的区别?
答:hashmap 使用的链表法,而threadlocalmap使用的是开放寻址法。
为什么要这样实现?
答:开放寻址法它的特点和使用场景是数据量比较少的情况下性能更好;而hashmap里面存储的数据通常情况下是比较多,这个时候使用开放寻址法效率就比较低,这个时候最好使用链式法。
面试题: 为什么将ThreadLocal中的key设置为弱引用?
答:为了最大长度的避免00M