数据未更新维护不给提款怎么解决 操作系统常见面试题目2:多线程方面
目录
1. 线程 1.1 线程出现的目的 1.2 线程的优点有哪些?
(1):创建,销毁和调度的速度都比进程快。
(2):线程就是程序内部的一条执行路径。单个路径就是单线程。
1.2 操作系统的线程和Java中的线程 操作系统的线程:通过使用PCB来进行描述Java中的线程:通过使用类进行描述 1.3 线程模型 一对一:内核级线程模型,一个用户对应一个内核(浪费)一对多:用户级线程模型,多个用户对应一个内核(阻塞)多对多:用户级线程模型,多个用户对应多个内核(难度) 2. JAVA中创建线程 2.1 继承类,重写run()方法 线程实现的四步曲
1) 定义一个线程类,继承类,重写run方法
2)新建线程对象
3)调用start()方法启动方法特点
1)优点:代码简单
2)缺点:线程类已经继承了,不能够再继承其他类,不利于扩展,线程有执行结果,不可以返回。代码展示
/**
1、定义一个线程类,继承Thread类,并重写run方法
*/
class MyThread extends Thread{
/**
2、重写run方法,里面是定义线程以后要干啥
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
//新建一个线程的实例,并调用start进行执行
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
t.start();
}
}
问题:为什么不直接调用run方法,而是调用start启动线程
1) 直接调用run方法,会被当成普通方法进行执行,此时相当于还是单线程执行
2)使用 t.run() 会直接调用 .run()
3)只有调用start方法,才是启动一个新的线程执行
4)使用 t.start() 会创建一个新的 PCB ,新的 PCB 链接在链表上,然后执行 .run() 方法 2.2 实现接口 线程实现的四步曲
1) 定义一个线程类,实现接口,重写run方法
2)新建线程对象
3)把对象交给对象处理
4)启动线程t.start()特点
1)优点:只是实现接口,还可以继承其他类,可扩展性强
2)缺点:编程多一层对象包装,线程有执行结果,不可以返回。即run方法无返回值。代码展示
/**
1、定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable {
/**
2、重写run方法,定义线程的执行任务的
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
使用匿名内部类,直接重写的run方法,然后新建类对象,并启动start()
public class demo1 {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("i="+i);
}
}
};
Thread thread=new Thread(runnable);
thread.start();
}
}
2.3 利用和接口实现 前两个方法的缺点
1) 重写的run()方法均不能直接返回结果
2)不适合需要返回线程执行结果的业务场景解决办法
1) JDK5.0 提供了和 Task来实现
2) 可以得到线程执行的结果 特点
1) 优点:实现接口,还可以实现其他的接口和父类,可拓展性强;同时可以得到线程执行的结果
2) 缺点:代码复杂了一点实现步骤
1) 定义一个任务类,实现接口,重写call方法(封装要做的事情,可以带返回值)
2)新建实例对象;新建 Task实例对象,然后把实例对象放进去(用 Task把对象封装成线程任务对象)。
3)新建类对象,把实例对象交给处理。
4)调用的start()进行线程的启动,执行任务
5)线程执行完毕后,调用 Task实例对象的get方法,获取任务执行的结果。代码展示
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
/**
目标:学会线程的创建方式三:实现Callable接口,结合FutureTask完成。
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.4 方法之间的比较
内容参考
黑马程序员:
2.5 线程的操作 线程休眠: t.sleep(500),精确时间休眠,结束还是需要进行资源抢夺线程中断:t.join(),进入阻塞等待常用方法和构造器 方法名称说明
()
获取当前线程的名称
void ( name)
设置线程名字
()
返回对当前正在执行线程对象的引用
void sleep(long time)()
让线程休眠指定的时间,单位为毫秒
void run()
线程任务方法
void start()
线程启动方法
线程共有六种状态
New(新建):线程刚被新建出来,内核没有创建PCB— ** -创建线程对象** (可运行):调用了start()方法,处于可运行状态,等待获取CPU使用权—- ** start()方法** :阻塞状态,因为某种原因放弃CPU使用使用权,处于阻塞队列中 ——– ** 无法获得所对象**
1) 等待阻塞:使用了wait()
2)同步阻塞:运行线程在获取对象同步锁的时候,发现已被抢占,故等待
3)其他阻塞:执行sleep和join方法,或者是发出I/O六请求(无限等待):一个线程进入状态,只能和才能够唤醒 ——- ** wait()** Time (有限等待):同转态,只是多了计时 ——– ** sleep()** :死亡状态,线程执行完成或者异常退出 —— ** 全部代码运行结束** 2.6 停止一个正在运行的线程?
1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2)用方法中断线程。
3. 线程安全 3.1 线程不安全的原因 线程之间是并发执行的抢占式的:没有顺序,但是有些功能需要线程间有顺序(根本原因)多个线程可能对同一个变量进行修改;一个线程在改,另一个线程在读,会发生错乱。多个线程可能对同一个变量进行访问: 3.2 保持线程安全的方法 可见性:线程之间的可见性,一个线程修改的状态对另一个线程是可见的。(、 和 final )原子性:原子具有不可分割性,一个操作如果具有原子性,那么它是不可分割的,连续的。( 和在 lock、 )有序性:线程之间的操作时有序的(、) 4. 线程同步的方法 方法一:同步代码块,基于,范围小,加锁局部代码,( o ){}方法二:同步方法,基于,范围大(默认用this或者当前类class对象作为锁)方法三:Lock锁:比更加广泛的锁定操作。接下来会讲 4.0 类 4.1 是什么? 是一个工具类,可以看做是一个类的变量,为多线程的线程并发问题提供一个新的思路。 4.2 是作用什么? 当用定义一个变量,可以在多个线程之间使用,通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。线程访问的变量是固定不变的,不是动态的,每个线程访问的变量不一样。在很多情况下,比直接使用同步机制解决线程安全问题更简单,更方便,具有更好的并发性。
常见面试题剖析 4.3 使用场景 使用场景为用来解决数据库连接、管理等 4.4 存在问题 内存泄露: 为 的一个静态内部类,里面定义了Entry 来保存数据。在Entry内部使用作为key,使用我们设置的value作为value。是null了,也就是要被垃圾回收器回收了。但是( 的内部属性)生命周期和的一样,它不会回收。故出现了key没有了,但是 4.1 关键字-监视器锁 lock 修饰的对象与锁住的对象
1) 修饰同步代码块:作用于调用的实例对象(比普通方法更加灵活,效率更高)
2) 修饰普通方法:作用于调用的实例对象
3)修饰静态方法/修饰类:作用于所有对象
参考文章:我们锁住的到底是什么的特性
1)互斥性:进行 修饰的对象中即为加锁;退出修饰对象中即为解锁(自动进行)
2)阻塞等待:针对每一把锁维护一个等待序列,锁被占用,则线程进行等待队列中等待(上一个进程解锁后,需要换新需要锁的进程,并且需要竞争)
3) 刷新内存:每次操作都进行Load和Save操作,刷新内存中的值。(可能会导致程序运行变慢)
4)可重入:允许一个线程针对同一把锁,进行连续加锁。你会发生死锁现象。(和都是可重入锁,而lock是不可重入锁)保证线程安全的特性原理:
1)可见性:保持操作是在主内存进行的。实时进行更新,不设置线程缓存
2)原子性:加锁,不能被其他线程访问。
3)有序性:同一个时刻只允许一条线程对其进行读写操作 4.2 关键字
定义:变量是一种比关键字更轻量级的同步机制。
是如何保证线程同步的?
1)可见性::一个线程修改了修饰的变量,会立即同步到主内存中,每次调用都通过主内存完成。不允许线程内部
2)有序性:内存屏障和禁止重排序
特性
1)修饰的变量读操作不增加消耗,但是写操作会增加内存消耗
2)不是安全的:保证了写操作能够反映到每一个线程中,但是里面的运算不是原子操作,不是安全的。
适用的场合
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量不变的式子中
因此,适用于状态标记量:
4.3 Java 标准库中的线程安全类 线程不安全:,;,;,;线程安全:,(核心方法都带有),(虽然没加锁,但是是不可变独享,因为被final修饰) 5. 线程通信 Wait()和() (线程之间相互通信) 功能:用于线程之间的同步。定义:Wait()和()必须在保护的代码中使用,说明一个线程暂时进入阻塞队列中,和其他唤醒其他线程获得对象锁。种类
1) wait():导致当前的线程等待,直到其他线程调用此对象的( ) 方法或 ( ) 方法
2)():唤醒在此对象监视器上等待的单个线程 ():唤醒在此对象监视器上等待的所有线程 特点:
1)wait( ),( ),( )都不属于类,而是属于基础类。
2)每个对象都可以作为锁,故需要定义操作的方法也应该是对象,而不是线程。
// ** wait 和 sleep 的 区别(面试题)**
1)wait()能够使得某个线程进入阻塞状态,有限或者无限时间;sleep()使线程等待有限时间
2)wait 需要搭配 使用,sleep不需要
3)sleep()唤醒两个方法(时间到和调用该线程的 方法);wait()方法会多一个()唤醒
4)wait()是为了配合多线程的执行顺序,而sleep没有多线程的关系
5)wait()是(()方法,而sleep是类的静态方法 6. 线程池 6.1 定义与使用原因 定义:线程池是一个复用线程的技术原因:如果每一个用户请求,都创建一个新线程来完成,那么创建新线程开销很大,会严重影响系统的性能。 6.2 线程池的创建 使用的实现类进行创建线程池对象(七大参数)
1) :指定线程池的线程数量(核心线程)
2):指定线程池可支持的最大线程数(核心线程+临时线程)
3):临时线程的最大存活时间
4)unit:指定临时线程存活时间的单位(秒,分,时,天)
5):指定任务队列,新任务来了的缓冲区
6):指定用哪个线程工厂创建线程
7):指定线程忙,任务满的时候,新任务来了怎么办 6.3 线程池面试常考 临时线程在什么时候创建啊?
新任务提交时,核心线程都在忙,任务队列也满了,这个时候创建临时线程。什么时候开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候,才会开始拒绝任务线程池的大小如何确定?
线程池的大小是根据CPU的核心数和所需要处理的任务的阻塞系数来决定的,其中阻塞系数是任务的等待时间和CPU处理时间的比值。 7. 锁策略 7.1 常见的锁策略 乐观锁和悲观锁
1)乐观锁:总是假设最好的情况,认为共享资源没有修改;当对数据进行提交更新的时候,会判别一下数据有没有被修改,被修改了则不提交(使用版本号控制如果资源被修改过,那么版本号会加1,然后更新主内存资源;后面还想更新,版本还是之前的,就更新不了了)
2)悲观锁:总是假设最坏的情况,每次去修改数据,都认为别人也会去修改,所以在打算修改数据的时候,直接加锁(其他的不能进行读写操作),当修改完后,进行解锁。读写锁
1)线程主要对数据就是读操作和写操作,读操作不需要线程互斥,只有设计写操作需要线程互斥,这个时候定义一个读写锁,非常适合读多写少的情况,能够大大降低性能。重量级锁和轻量级锁(锁升级:偏向锁,轻量级锁,重量级锁)
1)一个开销小,由用户态完成
2)一个开销大,由核心态完成自旋锁(Spin Lock) 挂起等待锁
1)自旋锁:在线程抢锁失败后,不是处于挂堵塞状态,而是快速进入下一次抢锁状态,栈CPU,但是能够第一时间获取锁。
2)挂起等待锁:抢锁失败后,处于阻塞等待状态,不占CPU。lock公平锁和不公平锁
1)公平锁:遵循先来后到的原则lock
2)不公平锁:不支持先来后到,抢锁状态人人平等。可重入锁 和不可重入锁
1)可重入锁:允许同一个线程多次获取同一把锁(lock和都是可重入锁)
2)不可重入锁:不允许线程多次获取同一把锁。 7.2 CAS 7.2.1 什么是CAS CAS为 and ,比较与交换,可以认为是一种乐观锁。这个操作原子性的,CPU提供了一组CAS相关的指令
(1):比较A和B是否相等
(2):如果相等,将C写入A中(进行交换)
(3):返回操作时true(交换成功)或者false(交换失败)
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
7.2.2 CAS的缺点 ABA问题:CAS 算法是在某一时刻取出内存值然后在当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。
1)什么是ABA问题:就是可能会出现值从A转变为B,然后B转变为A的情况,有多个线程也想把A变为B,那么这就会造成发生两次A到B的情况
2)如果避免:使用版本号机制,修改一次,版本号就加上一。(例子来源文章2)
ABA问题带来的危害:
小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
线程3(默认):获取当前值50,期望更新为100,
这时候线程3成功执行,余额变为100,
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。
解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。
do while循环时间长的话开销大只能保证一个共享变量的原子性 7.2.3 CAS 实现了原子类
CAS本来就是具有原子性的,故能够很方便的实现i++操作
boolean CAS(value,oldvalue,oldvalue+1){
if(value == oldvalue){
value = expectvalue;
return true;
}
return false;
}
7.2.4 CAS 实现了自旋锁
public class SpinLock {
private Thread owner = null;
public void lock() {
while(!CAS(this.owner, null , Thread.currentThread())){
}
}
public void unlock(){
this.owner = null;
}
}
(!CAS(this.owner, null , Thread.currentThread()))
(1):通过一个CAS来判断当前锁是不是被某个线程持有
(2):如果被某个线程持有(this.owner!=null),那么就让锁进行自旋状态
(3):如果没有被某个线程持有(this.owner==null),那么就让申请的线程获得锁
7.2.5 CAS 解决ABA问题(即同时进行为修改与读写)
引入版本号,:当前的版本号和读取的版本号相同,则进行数据的修改,并且是版本号加一;否则放弃修改。
9. :美[ˈsɪŋkrəˌnaɪz]
//1)修饰实例方法
synchronized void method() {
//业务代码
}
//2)修饰静态方法
synchronized void staic method() {
//业务代码
}
//3)修饰代码块
synchronized(this) {
//业务代码
}
9.1 特性 开始是 乐观锁 ,如果 锁冲突频繁 ,那么就变为了 悲观锁 开始是 轻量级锁 ,如果所 持有的的时间比较长 ,那么就转变为 重量级锁 实现轻量级锁的时候,大概率用到了自旋锁是一种不公平锁,不支持先来后到是一种可重入锁,一个线程能够多次获取同一锁对象不是一种读写锁 9.2 锁的对象 修饰的对象与锁住的对象
1) 修饰同步代码块:作用于调用的实例对象(比普通方法更加灵活,效率更高)
2) 修饰普通方法:作用于调用的实例对象
3)修饰静态方法/修饰类:作用于所有对象
参考文章:我们锁住的到底是什么 9.3 的特性
1)互斥性:进行 修饰的对象中即为加锁;退出修饰对象中即为解锁(自动进行)
2)阻塞等待:针对每一把锁维护一个等待序列,锁被占用,则线程进行等待队列中等待(上一个进程解锁后,需要换新需要锁的进程,并且需要竞争)
3) 刷新内存:每次操作都进行Load和Save操作,刷新内存中的值。(可能会导致程序运行变慢)
4)可重入:允许一个线程针对同一把锁,进行连续加锁。你会发生死锁现象。(和都是可重入锁,而lock是不可重入锁)
9.4 保证线程安全的特性原理:
1)可见性:保持操作是在主内存进行的。实时进行更新,不设置线程缓存
2)原子性:加锁,不能被其他线程访问。
3)有序性:同一个时刻只允许一条线程对其进行读写操作
10. Lock(是前者一个实现类)
class X {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// ...
//定义需要保证线程安全的方法
public void m() {
//加锁
lock.lock();
try{
// 保证线程安全的代码
}
// 使用finally块保证释放锁
finally {
lock.unlock()
}
}
}
10.1 定义 Lock是一种比更加灵活的锁,具有更广泛的使用性。 10.2 Lock比多的功能 尝试非阻塞地获取锁:尝试去获取锁,如果没获取成功,也不会进入阻塞状态。能被中断地获取锁:获取到的锁能够响应中断,中断发生后,锁会被释放超时获取锁:在一个指定的时间之前要获得锁,如果那么就会返回,不再请求锁。 10.3 Lock与区别 存在层面:Lock是一个接口,重要的类是,底层使用的是java语言;是一个关键词,存在于JVM层面。锁的释放条件:Lock需要手动释放锁,故最好使用try ;自动释放锁锁的获取:Lock如果获取不到锁的话,不会等待;如果获取不到锁,会一直等待阻塞。Lock可以通过()指定唤醒某个线程获得锁,不能指定,由JVM决定。锁的类型:Lock是一个可重入锁,可判断,可公平锁;也是可重入锁,不可判断,非公平锁。锁的性能:Lock适用于大量同步的阶段, 使用与小量同步阶段。锁的存在:Lock可以或者对象是否存在锁,不能判断锁的存在。锁住对象:Lock只能锁住代码块;能够锁住类,方法和代码块。故使用更加方便,但是Lock功能更强。
文章参考:
1 关键字参考文章
2. 操作系统 —多线程(进阶)
3 CAS的ABA问题详解