栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

操作系统常见面试题目2:多线程方面

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

操作系统常见面试题目2:多线程方面

目录
  • 1. 线程
    • 1.1 线程出现的目的
    • 1.2 线程的优点有哪些?
    • 1.2 操作系统的线程和Java中的线程
  • 2. JAVA中创建线程
    • 2.1 继承Thread类,重写run()方法
    • 2.2 实现Runnable接口
    • 2.3 利用Callable和FutureTask接口实现
    • 2.4 方法之间的比较
    • 2.5 线程的操作
    • 2.6 线程的状态
  • 3. 线程安全
    • 3.1 线程不安全的原因
    • 3.2 保持线程安全的方法
  • 4. 线程同步的方法
  • 4.1 synchronized 关键字-监视器锁monitor lock
  • 4.2 volatile 关键字
  • 4.3 Java 标准库中的线程安全类
  • 5. 线程通信 Wait()和Notify() (线程之间相互通信)
  • 6. 线程池
    • 6.1 定义与使用原因
    • 6.2 线程池的创建
    • 6.3 **线程池面试常考**
  • 7. 锁策略
    • 7.1 常见的锁策略
    • 7.2 CAS
      • 7.2.1 什么是CAS
      • 7.2.2 CAS 实现了原子类
      • 7.2.3 CAS 实现了自旋锁
      • 7.2.4 CAS 解决ABA问题(即同时进行为修改与读写)
  • 9. synchronized:美[ˈsɪŋkrəˌnaɪz]
    • 9.1 特性
  • 10. Lock
    • 10.1 定义
    • 10.2 Lock比synchronized多的功能
    • 10.3 Lock与synchronized线程通信的区别
    • 10.4 Lock与synchronized在锁类型的区别

1. 线程 1.1 线程出现的目的
  • 引入进程的目的是为了并发编程,但是进程能够解决一定程度的并发问题,还不够,有两个问题
    (1):创建进程需要分配资源
    (2):销毁进程需要释放资源
    (3):频繁进程和线程的切换会导致开销较大,故设计了一个轻量级的进程,即线程(Thread)
1.2 线程的优点有哪些?

(1):创建,销毁和调度的速度都比进程快。
(2):线程就是程序内部的一条执行路径。单个路径就是单线程。

1.2 操作系统的线程和Java中的线程
  1. 操作系统的线程:通过使用PCB来进行描述
  2. Java中的线程:通过使用Thread类进行描述
2. JAVA中创建线程 2.1 继承Thread类,重写run()方法
  1. 线程实现的四步曲
    1) 定义一个线程类MyThread,继承Thread类,重写run方法
    2)新建线程对象
    3)调用start()方法启动
  2. 方法特点
    1)优点:代码简单
    2)缺点:线程类已经继承了Thread,不能够再继承其他类,不利于扩展,线程有执行结果,不可以返回。
  3. 代码展示
class MyThread extends Thread{
    
    @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();
    }
}
  1. 问题:为什么不直接调用run方法,而是调用start启动线程
    1) 直接调用run方法,会被当成普通方法进行执行,此时相当于还是单线程执行
    2)使用 t.run() 会直接调用 myThread.run()
    3)只有调用start方法,才是启动一个新的线程执行
    4)使用 t.start() 会创建一个新的 PCB ,新的 PCB 链接在链表上,然后执行 myThread.run() 方法
2.2 实现Runnable接口
  1. 线程实现的四步曲
    1) 定义一个线程类MyRunnable,实现Runnable接口,重写run方法
    2)新建线程对象MyRunnable
    3)把MyRunnable对象交给Thread对象处理
    4)启动线程t.start()
  2. 特点
    1)优点:只是实现接口,还可以继承其他类,可扩展性强
    2)缺点:编程多一层对象包装,线程有执行结果,不可以返回。即run方法无返回值。
  3. 代码展示
class MyRunnable  implements Runnable {
    
    @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);
        }
    }
}
  1. 使用匿名内部类,直接重写Runnable的run方法,然后新建Thread类对象,并启动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 利用Callable和FutureTask接口实现
  1. 前两个方法的缺点
    1) 重写的run()方法均不能直接返回结果
    2)不适合需要返回线程执行结果的业务场景
  2. 解决办法
    1) JDK5.0 提供了Callable和Future Task来实现
    2) 可以得到线程执行的结果
  3. 特点
    1) 优点:实现接口,还可以实现其他的接口和父类,可拓展性强;同时可以得到线程执行的结果
    2) 缺点:代码复杂了一点
  4. 实现步骤
    1) 定义一个任务类MyCallable,实现Callable接口,重写call方法(封装要做的事情,可以带返回值)
    2)新建MyCallable实例对象;新建Future Task实例对象,然后把MyCallable实例对象放进去(用Future Task把Callable对象封装成线程任务对象)。
    3)新建Thread类对象,把MyCallable实例对象交给Thread处理。
    4)调用Thread的start()进行线程的启动,执行任务
    5)线程执行完毕后,调用Future Task实例对象的get方法,获取任务执行的结果。
  5. 代码展示
class MyCallable implements Callable{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3、创建Callable任务对象
        Callable call = new MyCallable(100);
        // 4、把Callable任务对象 交给 FutureTask 对象
        //  FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //  FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
        FutureTask f1 = new FutureTask<>(call);
        // 5、交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();


        Callable call2 = new MyCallable(200);
        FutureTask 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 方法之间的比较

内容参考
黑马程序员:https://www.bilibili.com/video/BV1Cv411372m?spm_id_from=333.337.search-card.all.click

2.5 线程的操作
  1. 线程休眠: t.sleep(500),精确时间休眠,结束还是需要进行资源抢夺
  2. 线程中断:t.join(),进入阻塞等待
  3. Thread常用方法和构造器
方法名称说明
String getName()获取当前线程的名称
void setname(String name)设置线程名字
public static ThreadcurrentThread()返回对当前正在执行线程对象的引用
public static void sleep(long time)()让线程休眠指定的时间,单位为毫秒
public void run()线程任务方法
public void start()线程启动方法
2.6 线程的状态

线程共有六种状态

  1. New(新建):线程刚被新建出来,内核没有创建PCB— ** -创建线程对象**
  2. Runnable(可运行):调用了start()方法,处于可运行状态,等待获取CPU使用权---- ** start()方法**
  3. Blocked:阻塞状态,因为某种原因放弃CPU使用使用权,处于阻塞队列中 -------- ** 无法获得所对象**
    1) 等待阻塞:使用了wait()
    2)同步阻塞:运行线程在获取对象同步锁的时候,发现已被抢占,故等待
    3)其他阻塞:执行sleep和join方法,或者是发出I/O六请求
  4. Waiting(无限等待):一个线程进入waiting状态,只能motify和notifyAll才能够唤醒 ------- ** wait()**
  5. Time Waiting(有限等待):同waiting转态,只是多了计时 -------- ** sleep()**
  6. Teminated:死亡状态,线程执行完成或者异常退出 ------ ** 全部代码运行结束**
3. 线程安全 3.1 线程不安全的原因
  1. 线程之间是并发执行的抢占式的:没有顺序,但是有些功能需要线程间有顺序(根本原因)
  2. 多个线程可能对同一个变量进行修改;一个线程在改,另一个线程在读,会发生错乱。
  3. 多个线程可能对同一个变量进行访问:
3.2 保持线程安全的方法
  1. 可见性:线程之间的可见性,一个线程修改的状态对另一个线程是可见的。(volatile、synchronized 和 final )
  2. 原子性:原子具有不可分割性,一个操作如果具有原子性,那么它是不可分割的,连续的。( synchronized 和在 lock、unlock )
  3. 有序性:线程之间的操作时有序的(volatile、synchronized)
4. 线程同步的方法
  1. 方法一:同步代码块,基于synchronized,范围小,加锁局部代码,synchronized(Object o ){}
  2. 方法二:同步方法,基于synchronized,范围大(默认用this或者当前类class对象作为锁)
  3. 方法三:Lock锁:比synchronized更加广泛的锁定操作。接下来会讲
4.1 synchronized 关键字-监视器锁monitor lock
  1. 修饰的对象与锁住的对象
    1) 修饰同步代码块:作用于调用的实例对象(比普通方法更加灵活,效率更高)
    2) 修饰普通方法:作用于调用的实例对象
    3)修饰静态方法/修饰类:作用于所有对象
    参考文章:我们锁住的到底是什么
  2. synchromized的特性
    1)互斥性:进行 synchromized修饰的对象中即为加锁;退出修饰对象中即为解锁(自动进行)
    2)阻塞等待:针对每一把锁维护一个等待序列,锁被占用,则线程进行等待队列中等待(上一个进程解锁后,需要换新需要锁的进程,并且需要竞争)
    3) 刷新内存:每次操作都进行Load和Save操作,刷新内存中的值。(可能会导致程序运行变慢)
    4)可重入:允许一个线程针对同一把锁,进行连续加锁。你会发生死锁现象。(synchronized和ReentrantLock都是可重入锁,而lock是不可重入锁)
  3. 保证线程安全的特性原理:
    1)可见性:保持操作是在主内存进行的。实时进行更新,不设置线程缓存
    2)原子性:加锁,不能被其他线程访问。
    3)有序性:同一个时刻只允许一条线程对其进行读写操作
4.2 volatile 关键字
  1. 定义:volatile变量是一种比sychronized关键字更轻量级的同步机制。

  2. volatile是如何保证线程同步的?
    1)可见性::一个线程修改了volatile修饰的变量,会立即同步到主内存中,每次调用都通过主内存完成。不允许线程内部
    2)有序性:内存屏障和禁止重排序

  3. 特性
    1)volatile修饰的变量读操作不增加消耗,但是写操作会增加内存消耗
    2)volatile不是安全的:volatile保证了写操作能够反映到每一个线程中,但是里面的运算不是原子操作,不是安全的。

  4. 适用的场合
    1)对变量的写操作不依赖于当前值
    2)该变量没有包含在具有其他变量不变的式子中
    因此,volatile适用于状态标记量:

4.3 Java 标准库中的线程安全类
  1. 线程不安全:ArrayList,LinkedList;HashSet,TreeSet;HashMap,TreeMap;StringBuilder
  2. 线程安全:ConcurrentHashMap,StringBuffer(核心方法都带有synchronized),String(虽然没加锁,但是是不可变独享,因为被final修饰)
5. 线程通信 Wait()和Notify() (线程之间相互通信)
  1. 功能:用于线程之间的同步。
  2. 定义:Wait()和Notify()必须在synchronized保护的代码中使用,说明一个线程暂时进入阻塞队列中,和其他唤醒其他线程获得对象锁。
  3. 种类
    1) wait():导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
    2)notify():唤醒在此对象监视器上等待的单个线程
  1. notifyAll():唤醒在此对象监视器上等待的所有线程
  1. 特点:
    1)wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类。
    2)每个对象都可以作为锁,故需要定义操作的方法也应该是对象,而不是线程。
    //https://blog.csdn.net/jushisi/article/details/103240664
  2. ** wait 和 sleep 的 区别(面试题)**
    1)wait()能够使得某个线程进入阻塞状态,有限或者无限时间;sleep()使线程等待有限时间
    2)wait 需要搭配synchronized 使用,sleep不需要
    3)sleep()唤醒两个方法(时间到和调用该线程的interrupted 方法);wait()方法会多一个notify()唤醒
    4)wait()是为了配合多线程的执行顺序,而sleep没有多线程的关系
    5)wait()是Object(()方法,而sleep是Thread类的静态方法
6. 线程池 6.1 定义与使用原因
  1. 定义:线程池是一个复用线程的技术
  2. 原因:如果每一个用户请求,都创建一个新线程来完成,那么创建新线程开销很大,会严重影响系统的性能。
6.2 线程池的创建
  1. 使用ExecutorService的实现类ThreadPoolExecutor进行创建线程池对象(七大参数)
    1) corePoolSize:指定线程池的线程数量(核心线程)
    2)maxmumPlloSize:指定线程池可支持的最大线程数(核心线程+临时线程)
    3)keepAliveTime:临时线程的最大存活时间
    4)unit:指定临时线程存活时间的单位(秒,分,时,天)
    5)workQueue:指定任务队列,新任务来了的缓冲区
    6)threadFactory:指定用哪个线程工厂创建线程
    7)handler:指定线程忙,任务满的时候,新任务来了怎么办
6.3 线程池面试常考
  1. 临时线程在什么时候创建啊?
    新任务提交时,核心线程都在忙,任务队列也满了,这个时候创建临时线程。
  2. 什么时候开始拒绝任务?
    核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候,才会开始拒绝任务
7. 锁策略 7.1 常见的锁策略
  1. 乐观锁和悲观锁
    1)乐观锁:总是假设最好的情况,认为共享资源没有修改;当对数据进行提交更新的时候,会判别一下数据有没有被修改,被修改了则不提交(使用版本号控制如果资源被修改过,那么版本号会加1,然后更新主内存资源;后面还想更新,版本还是之前的,就更新不了了)
    2)悲观锁:总是假设最坏的情况,每次去修改数据,都认为别人也会去修改,所以在打算修改数据的时候,直接加锁(其他的不能进行读写操作),当修改完后,进行解锁。
  2. 读写锁
    1)线程主要对数据就是读操作和写操作,读操作不需要线程互斥,只有设计写操作需要线程互斥,这个时候定义一个读写锁,非常适合读多写少的情况,能够大大降低性能。
  3. 重量级锁和轻量级锁
    1)一个开销小,由用户态完成
    2)一个开销大,由核心态完成
  4. 自旋锁(Spin Lock) 挂起等待锁
    1)自旋锁:在线程抢锁失败后,不是处于挂堵塞状态,而是快速进入下一次抢锁状态,栈CPU,但是能够第一时间获取锁。
    2)挂起等待锁:抢锁失败后,处于阻塞等待状态,不占CPU
  5. 公平锁和不公平锁
    1)公平锁:遵循先来后到的原则
    2)不公平锁:不支持先来后到,抢锁状态人人平等。
  6. 可重入锁 和不可重入锁
    1)可重入锁:允许同一个线程多次获取同一把锁(lock和synchronize都是可重入锁)
    2)不可重入锁:不允许线程多次获取同一把锁。
7.2 CAS 7.2.1 什么是CAS
  1. CAS为compare and switch,比较与交换,可以认为是一种乐观锁。
  2. 这个操作原子性的,CPU提供了一组CAS相关的指令
    (1):比较A和B是否相等
    (2):如果相等,将C写入A中(进行交换)
    (3):返回操作时true(交换成功)或者false(交换失败)
    boolean CAS(value,oldvalue,expectvalue){
        if(value == oldvalue){
            value = expectvalue;
            return true;
        }
        return false;
    }
7.2.2 CAS 实现了原子类

CAS本来就是具有原子性的,故能够很方便的实现i++操作

    boolean CAS(value,oldvalue,oldvalue+1){
        if(value == oldvalue){
            value = expectvalue;
            return true;
        }
        return false;
    }
7.2.3 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.4 CAS 解决ABA问题(即同时进行为修改与读写)

引入版本号,:当前的版本号和读取的版本号相同,则进行数据的修改,并且是版本号加一;否则放弃修改。

9. synchronized:美[ˈsɪŋkrəˌnaɪz] 9.1 特性
  1. 开始是 乐观锁 ,如果 锁冲突频繁 ,那么就变为了 悲观锁
  2. 开始是 轻量级锁 ,如果所 持有的的时间比较长 ,那么就转变为 重量级锁
  3. 实现轻量级锁的时候,大概率用到了自旋锁
  4. 是一种不公平锁,不支持先来后到
  5. 是一种可重入锁,一个线程能够多次获取同一锁对象
  6. 不是一种读写锁
10. Lock 10.1 定义
  1. Lock是一种比synchronized更加灵活的锁,具有更广泛的使用性。
10.2 Lock比synchronized多的功能
  1. 尝试非阻塞地获取锁:尝试去获取锁,如果没获取成功,也不会进入阻塞状态。
  2. 能被中断地获取锁:获取到的锁能够响应中断,中断发生后,锁会被释放
  3. 超时获取锁:在一个指定的时间之前要获得锁,如果那么就会返回,不再请求锁。
10.3 Lock与synchronized线程通信的区别

1)synchronized中,使用wait和notify,notifyall来进行线程间的通信,但是notify,具体是哪一个线程来获取锁,这是由JVM决定的
2)Lock使用Condition接口与newCondition() 方法实现线程通信,使用Condition接口可以选择性通知某一个线程,更加准确和灵活。

10.4 Lock与synchronized在锁类型的区别

1)synchronized是不公平锁,固定了,改变不了
2)Lock可以是公平锁, 也可以是不公平锁,可以自行设定。

文章参考:
volatile 关键字参考文章
https://blog.csdn.net/wwzzzzzzzzzzzzz/article/details/123680001

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/860593.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号