解锁并发编程中常见面试题

线程和进程:

  • 一个进程对应一个内存中运行的应用程序,线程是进程的一个执行路径
  • 一个进程至少要有一个线程,一个进程内的多个线程可共享进程资源(堆、方法区)

线程和进程区别:

  • 进程是系统资源分配的基本单位,线城市处理器任务调度和执行的基本单位
  • 进程有独立的内存空间,线程的运行空间依赖于进程,但是每个线程有独立的栈(虚拟机栈&本地方法栈)和程序计数器
  • 一个进程内的多个线程可共享进程资源,但是进程之间相互独立
  • 多个进程之间一个崩溃不影响其他进程,但是一个线程崩溃会导致进程死去

线程生命周期:

新建 就绪 运行 阻塞 死亡

新建状态:刚建立一个线程对象后,处于新建状态的,此时线程有自己的内存空间,通过调用start方法进入就绪状态。

就绪状态:处于就绪状态的线程已经具备了运行条件,处于线程就绪队列等待CPU分配。通过Cpu调度获取CPU资源,线程就进入运行状态并自动调用自己的run方法,执行run中的任务。

运行状态:就绪状态的线程获得CPU资源开始执行线程功能代码,处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

运行状态线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源,手动实现使用yield()方法。

当发生如下情况是,线程会从运行状态变为阻塞状态:

  1. 线程调用sleep方法主动放弃所占用的系统资源
  2. 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  3. wait()方法是线程变为阻塞,需要notify / notifyAll
  4. 程序调用了线程的suspend方法将线程挂起,不释放锁,可能造成死锁
  5. 当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

 阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

死亡状态:当线程的run()方法执行完,或者stop() 强制性地终止,就认为它死去。但是不推荐使用stop() 这是一个过期的方法,推荐使用interrupt()。

interrupt()方法:用于中断线程,将被调用线程状态设置为“中断”,但是不会停止线程,需要用户自己监视线程状态并做出处理

Java线程中,sleep()和wait()区别

  1. 属类不同 ,wait(long) 是Object中方法 sleep(long)是Thread类中的静态方法
  2. 唤醒机制不同,wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()sleep()是到指定时间自动唤醒。
  3. 锁机制不同,wait(long)释放锁,sleep(long)只是让线程休眠,不会释放锁
  4. 使用位置不同,wait()必须持有对象锁,sleep()可以使用在任意地方
  5. 方法类型不同,wait()是实例方法 sleep()是静态方法

notify与notifyAll的区别:

  1. notify()唤醒一个线程,具体是哪一个由JVM确定,而且与优先级无关
  2. notifyAll()是唤醒所有等待状态的线程,将等待池中全部线程转移到锁池中,全部参与锁的竞争,竞争成功的线程进入就绪状态,不成功的线程留在锁池中等待所释放继续竞争

wait() 、notify() 、notifyAll() 为什么被定义在Object类中:

因为任何对象都可以作为锁,且以上方法都是作为等待对象的锁或者唤醒线程,但是java中没有任何可以给对象使用的锁,再加上java类默认继承Object类,所有将这三个给任意对象使用的方法定义在了OPbject类中

sleep和yield方法的区别:

sleep调用后线程处于阻塞状态,yield之后线程处于就绪状态

sleep方法给其他线程运行机会是不考虑线程优先级,会给低优先级线程机会,yield方法只会给同优先级或者高优先级线程机会


并发编程的三要素:

原子性:一个或者多个操作要么全部执行成功,要么全部失败

                解决方案:synchronized、Lock 可以解决原子性问题

可见性:一个线程对共享变量的修改,另外一个线程应立刻看见

                解决方案:synchronized、Lock、volatile 可以解决原子性问题

有序性:程序执行的顺序严格按照代码的先后顺序,在实际的运行中,CPU可能会对指令进行重排

                解决方案:Happens-Before

多线程程序有几种实现方式:

  1. 继承 Thread 类 重写run方法,调用start()方法启动线程
  2. 实现 Runnable接口 重写run方法,创建Runnable接口实现类实例 r,利用r为target创建Thread类对象,调用start()方法启动线程
  3. 创建Callable 接口实现类,重写call() 方法,该方法有返回值,通过类对象创建FutureTask对象f,同时实现Future和Runnable 接口,再以f为target创建Thread类对象t,调用start()方法启动线程,最后可以调用futureTask对象get 方法获取返回值。注意:get()方法执行时会阻塞主线程
  4. 使用Executors工具类创建线程池

注:在创建线程时,线程类构造方法和静态方法是被线程所在线程调用的,线程中的run方法才是被线程自身调用的。

run()方法和start()方法的区别:

  • start()方法是线程启动的开关
  • run()方法是线程功能的具体执行,相当于线程内部的一个函数
  • start()方法只能调用一次,run()方法能调用多次
  • start()之后线程处于就绪状态,run()分配CPU资源处于执行状态

为什么不能直接调用run()方法:run是线程内部的一个函数,调用start()是开启一个线程,此时主线程和子线程会依据时间片交替执行,但是直接调用run()则是在main主线程下直接调用一个普通方法,不存在多线程工作。

为了更好地看出区别线程内部使用死循环打印,代码示例:

public class MyThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        while (true) {
            System.out.println("主线程打印");
        }
    }
}
class MyThread extends Thread{
        @Override
        public void run() {
            while (true){
                System.out.println("子线程打印");
            }

    }
}

此时主线程调用start()方法,效果如下:

子线程打印
子线程打印
子线程打印
子线程打印
主线程打印
主线程打印
主线程打印
主线程打印

如果将start()改为run(),效果如下:

public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run();
        while (true) {
            System.out.println("主线程打印");
        }
    }
子线程打印
子线程打印
子线程打印
子线程打印
子线程打印

 由此可以看出,当直接调用run()方法时,run()方法内代码没有执行完是不继续向下执行的,也就没了多线程的说法。


上下文切换:首先明白一个原理,多线程在逻辑上是并发执行,实际上CPU的一个核心同一时间内只能被一个线程使用,所以CPU采取时间片轮转调度,给每个线程分配时间片交换执行。上下文切换就是指:一个线程时间片结束,保存当前状态,然后重新回到就绪状态把CPU让给其他线程使用的过程。

线程调度算法与线程调度策略:

  • 抢占式调度模型:优先让可运行池中优先级高的线程占用,JVM使用的就是该算法
  • 分时调度模型:给每个线程平均时间片,让所有的线程轮流获得CPU的使用权,优先级相同时,随机选一个

调度策略:

  • 线程调度器会优先选择优先级较高的线程运行,以下情况会终止线程运行
  • 线程中使用了yield方法
  • 线程中使用了sleep方法进入阻塞状态
  • 线程由于IO操作受到阻塞
  • 另一个更高优先级的线程出现

线程的优先级:

每一个线程都有优先级,一般来说,高优先级线程在运行时有加高的优先级,依赖线程调度实现。线程的优先级是可以自定义的,但是不能保证高优先级线程一定会在低优先级线程之前运行。线程优先级是一个int类型变量,从1—10,优先级递增。

什么线程死锁:两个或两个以上的线程在执行过程中抢占统一资源造成多个线程通时被阻塞

造成死锁的必要条件:

  • 互斥条件:线程对分配的资源you排他性,同一时间内一个资源只能被一个线程占用
  • 请求与保持条件:一个线程印请求资源被占用而发生阻塞是,对已有资源保持不放
  • 不剥夺条件:线程已占有资源在使用完之前不能被强行剥夺,只能自己用完释放
  • 循环等待条件:发生死锁时,等待线程必然会形成闭环,永久堵塞

线程池参数各自的含义:

  1. corePoolSize核心线程数
  2. workQueue阻塞队列 队列为空时, 阻塞获取任务 队列放满时, 阻塞添加任务
  3. queueCapacity:任务队列最大长度
  4.  maximumPoolSize最大线程数 核心线程都被使用中,且任务队列已满时,线程池会创建新的线程执行任务,直到线程池中的线程数量达到maximumPoolSize。
  5. keepAliveTime线程最大空闲时间
  6. handler:拒绝策略
  7. allowCoreThreadTimeOut:核心线程超时是否被销毁

线程池工作原理:

线程数小于核心线程数,创建线程直到达到指定的核心线程数,线程数大于等于核心线程数,且任务队列未满,将新任务放入任务队列,有新任务,但是任务队列放满, 查看线程数是否等于最大线程数,小于 创建新线程,等于,触发拒绝策略。线程池中存在空闲线程且空闲时间超过了线程最大空闲时间,会进行销毁,默认销毁直到线程池中线程数等于核心线程数。

线程池中拒绝策略有哪些:

  1. AbortPolicy: 丢弃新任务,抛出异常,提示线程池已满(默认)。
  2. DisCardPolicy: 丢弃任务,不抛出异常。
  3. DisCardOldSetPolicy: 将消息队列中最先进入队列的任务替换为当前新进来的任务。
  4. CallerRunsPolicy: 由调用该任务的线程处理, 线程池不参与, 只要线程池未关闭,该任务一直在调用者线程中。

常见线程池对象有哪些:

SingleThreadExecutor:

它只会创建一条工作线程处理任务,采用的阻塞队列为LinkedBlockingQueue, 底层为链表

newFixedThreadPool:

  • 它是一种固定大小的线程池
  • corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads
  • keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即销毁,但这里keepAliveTime无效
  • 阻塞队列采用了LinkedBlockingQueue, 底层为链表
  • 实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。

newCachedThreadPool():

  • 它是一个可以无限扩大的线程池
  •  它比较适合处理执行时间比较小的任务
  • corePoolSize为0,maximumPoolSize为 Integer的最大值,意味着线程数量可以 Integer的最大值
  • keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死
  • 采用SynchronousQueue(同步队列)装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程

newScheduledThreadPool():

  • 它采用DelayQueue存储等待的任务
  • 它会根据time的先后时间排序,若time相同则根据sequenceNumber排序
  • DelayQueue队列:底层使用数组实现, 初始容量为16, 超过16个任务, 数组扩容, 每次扩容为之前的1.5倍
  • 工作线程会从DelayQueue取已经到期的任务去执行
  • 执行后也可以将任务重新定时, 放入队列中

newWorkStealingPool():

工作原理(工作窃取算法):把一个Thread 分叉(fork)成多个子线程。让多个子线程执行本来一个线程应该执行的任务。最后把多个线程执行结果合并

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值