Java常见面试题之多线程

本文详细介绍了Java中线程的创建方式,包括继承Thread类、实现Runnable接口和Callable接口,以及使用线程池。线程池的创建参数如核心线程数、最大线程数和拒绝策略等也进行了说明。文章还涵盖了线程的生命周期、阻塞状态以及线程终止的各种方式。此外,对比了wait()和sleep()的区别,以及start()和run()的不同。最后,讨论了synchronized同步锁、ReentrantLock可重入锁以及公平锁和非公平锁的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程的创建方式

  1. 继承Thread类,重写run方法,并调用start方法启动该线程.
  2. 实现Runnable接口,继承Thread的本质也是实现了Runnable接口,但是如果一个类已经继承了另一个类,那么它就无法继承Thread,所以需要调用Runnable
  3. 实现Callable接口,通过实现Callable接口获得的线程,可以通过Future对象获取它的返回值.有返回值的必须用Callable,没有返回值的必须实现Runnable;
  4. 使用线程池创建线程.线程池的构造函数有7个参数,核心线程数,最大线程数,线程存活时间,存活时间单位,任务队列,线程工厂,拒绝策略.

线程池的7个参数之任务队列,线程工厂,拒绝策略.

任务队列

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

线程工厂,主要用于创建线程.通过继承ThreadFactory类,重写newFixed方法

拒绝策略:当任务队列中存放的线程数量超过了队列容量执行的策略

  • AbortPolicy:拒绝并抛出异常。
  • CallerRunsPolicy:使用当前调用的线程来执行此任务。
  • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
  • DiscardPolicy:忽略并抛弃当前任务。

通过Executors工具类创建的4种线程池

  1. newCacheThreadPool,其特点是创建的线程池最小线程数为0,最大线程数为Integer.MAX_VALUE,也就是说这个线程池的没有最大线程数的限制,该线程池中的线程最大空闲时间是60秒,超时的线程会被线程池回收,并且这个线程在接收到线程任务之后不会进入到队列等待,而是会将其交给空闲的线程或者创建一个新的线程去执行.
  2. newFixedThreadPool,其特点是线程池中的线程数量固定,并且所有的线程永远不会被回收,当遇到任务之后进入到队列中等待,空闲的线程将会执行队列中的任务,队列中可以存放的任务数量没有限制.
  3. newScheduledThreadPool,它的核心线程数在创建的时候指定,最大线程数没有限制,为Integer.MAX_VALUE,并且其中的线程不会回收,任务队列中可以存放的任务数量没有限制.跟其他线程最大的区别在于,它提供了schedule方法和scheduleAtFixedRate方法,分别用于延时执行任务或循环执行任务.shcedule方法会延迟执行任务,这个任务只会执行一次.到那时scheduledAtFixedRate方法可以指定间隔时间,每间隔固定的间隔时间,就会执行一次,最终起到循环执行的效果,这个间隔时间是从线程任务启动执行为开始时间计算的,如果间隔时间小于任务执行时间,那么在当前任务执行结束之后会立即执行下一次任务.
  4. newSingleThreadExecutor,它创建的线程池中只有一个线程,该线程永远不会回收,而且如果该线程池由于异常等原因导致了线程在执行任务过程中被销毁了,那么该线程池会重新开启一个线程,继续执行上个线程剩余的任务.它主要用于需要按照一定顺序去执行任务的场景,由于该线程池中所有的任务都是由同一个线程去执行的,所以任务的执行顺序与进入队列的顺序是一致的.

线程的生命周期

        线程的生命周期主要分为5个阶段:

  1. 新建状态,新创建的线程处于这个阶段.
  2. 就绪状态,线程启动之后就从新建阶段处于了就绪阶段.在这个阶段,线程会参与抢夺cpu时间片
  3. 运行状态,当线程抢夺到cpu时间片之后,开始执行线程任务,也就是run方法中的任务.如果cpu时间片消耗完毕,线程任务还没有执行完毕,现成就会回到就绪状态,继续参与cpu时间片的抢夺.
  4. 阻塞状态,如果线程在运行状态中由于某些原因停止执行,陷入了阻塞状态,如:sleep wait 线程锁等,就会让出执行权,释放当前的cpu时间片,进入阻塞状态,等待某种满足某种条件之后自动唤醒或者被其他线程唤醒.如,sleep的休眠时间结束,wait休眠被notifyAll唤醒,那么这个线程就会回到就绪状态,参与cpu时间片的抢夺.
  5. 死亡状态,如果线程将线程任务执行完毕,或者线程执行过程中产生了未捕获的异常,或者调用了stop方法,就会导致线程执行结束,进入死亡状态,stop方法不能随便用,极易导致死锁.

线程的阻塞状态

        阻塞状态是指线程由于某些原因放弃了cpu的使用权,让出了cpu时间片,只能是停止运行.阻塞的情况主要分为三种:

  1. 等待阻塞,当运行中的线程调用了执行了Object中的wait()方法时,该线程会进入等待队列.
  2. 同步阻塞,当运行中的线程在获取对象的同步锁时,该锁资源已经被其他线程占用,则该线程会等待占用锁资源的线程释放锁资源.
  3. 其他阻塞,运行的线程执行Thread提供的sleep方法,或join等方法.或者发出了I/O请求,该线程会进入阻塞状态,当sleep超时,或者join超时或者I/O处理完毕之后,现成将会重新参与cpu时间片的抢夺

线程终止的几种方式

        线程终止一共有4种方式

        1.正常结束,当线程run方法中的线程任务全部执行结束之后线程就会进入死亡状态,等待回收.

        2.使用终止标记结束线程,如果当前线程中的方法中有一个while循环,当满足条件之后线程会自动结束,代码举例:

public class ExampleThread extends Thread{
    public static boolean condition = true;

    public void run(){
        while(condition){
            //该内容会一直执行
        }
    }
}

        上述示例代码我们可以看到,condition为true,所以while会一直循环,该线程将永远不会结束,但是如果我们在其他地方修改了condition的值,那么该线程就会结束,比如:

ExampleThread.condition = false;

        3.Interrupt异常退出,当我们使用了sleep 或 wait让线程陷入阻塞时,需要使用try catch 捕获InterruptExcetion异常.

public class ExampleThread extends Thread{

    public void run(){
        try{
            sleep(1000 * 60 * 60 * 24 * 365);
        }catch(InterruptExcption e){
            e.printStackTrace();
        }
    }
}

        就像上面的示例代码一样,现成开始之后会睡眠1年,如果我们调用interrupt()方法,就会使在该线程的InterruptException异常抛出点(sleep、wait、join)去抛出该异常,此时代码就会进入catch中,之后这个线程中的代码继续向下执行,最终执行完毕,这样就起到了终止线程的效果.

        4.stop终止线程,stop方法是Thread提供的方法,用于强行终止线程,但是它可能导致线程的数据混乱或丢失,慎用.

wait()和sleep()的区别

  1. wait()是Object提供的方法,sleep()是Thread提供的方法.
  2. wait()想要让线程进入休眠状态,必须用当前的锁对象去调用wait(),sleep()不需要调用锁对象.
  3. wait()在休眠之后会释放当前线程持有的锁资源,sleep()休眠之后不会让当前线程释放锁资源.
  4. sleep()在休眠时需要设置休眠时间,当休眠时间结束之后会自动唤醒.wait()也可以设置休眠时间,并且和sleep()一样也会自动唤醒.但是如果wait()不设置休眠时间,那么该线程就会一直处于等待状态,需要被吃用当前锁资源的其他线程调用锁对象的notify或notifyAll方法唤醒.

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

        start()方法用于启动一个线程,当一个线程调用了start()方法之后,就会进入就绪状态,去抢夺cpu执行权

        此时start()方法执行完毕,在调用start()的线程中不会阻塞,而是会继续执行后面的代码.

        run()方法中是当前线程要执行的线程任务,当处于就绪状态的线程抢夺到cpu时间片后开始执行run()方法中的代码,当代码执行结束之后当前线程就会进入死亡状态.

Java中的守护线程

        定义:守护线程又叫后台线程,主要服务于非守护线程,当进程中所有的非守护线程执行结束,剩下的所有线程都是守护线程时,该进程也会结束运行.定义守护线程可以在启动线程之前通过setDaemon(true)设置该线程为守护线程,在守护线程中创建的线程也是守护线程.

        优先级:守护线程的主要任务是服务非守护线程,所以它的优先级较低

        示例:最经典的守护线程就是Java的垃圾回收器,当满足一定条件后,就会主动回收堆内存中可回收的内存.如果我们的进程中没有了我们想要执行的程序之后,程序就会结束运行.也就是我们创建的非守护线程全部结束之后,垃圾回收器仍然运行,但是JVM不会考虑它的运行状态,依旧会结束当前进程.

悲观锁 乐观锁

        悲观锁是指在任何数据在访问时都会给数据上锁,必须等待当前线程结束数据访问,释放线程锁之后,其他数据才可以去访问该数据.

        乐观锁是在每一次访问数据时都假设其他线程不会访问数据,不会给数据上锁,但是在更新数据时,会判断一下在此期间这个数据有没有被其他数据修改过,如果没有修改过,会直接修改,如果被修改过,就会重试或者撤销操作.

自旋锁

        自旋锁的原理是如果持有锁的线程能在很短的时间内释放锁资源,那么等待竞争锁的线程不需要做内核态和用户态之间的切换进入阻塞状态,线程可以通过自旋的方式等待持有锁的线程释放锁后立即获取锁,这样就避免用户线程和内核的切换消耗.但是自旋是需要消耗CPU的,但是消耗CPU却并不完成任何任务,如果一直获取不到线程锁,就会一直白白消耗CPU资源,所以需要设定一个自旋等待的最大时间.如果持有锁资源的线程执行时间超过了设置的最大等待时间,就会导致其他线程在最大等待时间内获取不到线程锁,其他参与锁资源竞争的线程就会就会放弃自旋,进入阻塞状态.

        自旋锁会尽可能减少线程的阻塞,在一些锁资源竞争不太激烈,并且占用锁时间非常短的代码块来说,性能将会大幅度提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的消耗.

        但如果是锁资源竞争激烈,或者持有锁资源多线程需要长时间执行同步代码块,会导致自旋锁在获取锁资源之前一直占用CPU资源做无用功,同时有大量线程竞争同一个锁资源.会导致获取锁的时间很长,此时线程自旋的资源会大于线程从阻塞状态被唤醒的消耗.

Synchronized同步锁

        synchronized可以把任意一个非null的对象作为锁对象.属于独占式的悲观锁,同时属于可重入锁.

        作用范围:

  1. 作用在方法上时,锁对象相当于this.
  2. 作用在静态方法上是,锁对象是当前类所在的类对象
  3. 作用在一个同步代码块时,需要手动传入一个对象,会锁住所有以该对象为锁的代码块.

        核心组件

  1. Wait Set,调用wait 方法被阻塞的线程被放置在该组件中.
  2. Contention List 竞争队列,所有请求锁的线程首先被放在这个竞争队列中.
  3. Entry List ,Contention List中有资格成为候选资源的线程被移动到Entry List中.
  4. OnDeck 任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck.
  5. Owner,当前已经获取到锁资源的线程被称为Owner.
  6. !Owner,当前释放锁的线程.

        实现

  1. JVM每次从队列尾部取出一个线程作为OnDeck,但是并发情况下,Contention List会被大量的并发线程访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到Entry List中作为候选竞争线程.
  2. Owner线程会在unlock时,将Contention List中的部分线程迁移到Entry List中,并制定Entry List中的某个线程为OnDeck线程,一般是最先进入到Entry List的那个线程.
  3. Owner线程并不会直接把锁传递给OnDeck线程,而是将锁竞争的权利交给OnDeck线程,它需要重新竞争锁.这种行为成为竞争切换.
  4. OnDeck线程在获取到锁资源后会变为Owner现成, 没有得到锁资源的线程仍然停留在Entry List中.如果Owner线程被wait方法阻塞,就会进入到Wait Set中,直到被notify或者notifyAll方法唤醒之后会重新进入到Entry List
  5. 处于Contention List,Entry List,Watit Set中的线程处于阻塞状态,该阻塞是由操作系统完成的.
  6. synchronized是非公平锁,synchronized在线程进入到Contention List时,等待的线程会先尝试自旋获取锁,如果获取不到就进入Contention List,对于已经进入队列的线程来说,是不公平的.并且通过自旋获取锁的线程还可以能抢占OnDeck线程的锁资源.
  7. 每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在代码块前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位实现的.
  8. synchronize的是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能会给线程加锁消耗的时间比有用操作的时间消耗还多.

ReenTrantLock

        ReentrantLock实现了Lock接口并且实现了其中定义的方法,是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了可响应中断锁,可轮询锁请求,定时锁等避免线程死锁的方法.

主要方法:

  1. void lock():执行此方法时,如果锁处于空闲状态,当前线程就会获取到锁,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁.
  2. boolean tryLock(),如果锁可用,则获取锁,并立即返回true,否则返回false,该方法和lock方法的区别在于
  3. void unlock(),执行此方法时,当前线程将会释放持有的锁,锁只能由持有者释放,如果线程并不持有锁,执行此方法可能会导致发生异常.
  4. Condition newCondition(),条件对象,获取等待通知组件.该组件和当前的锁绑定,当前线程只有获取了wait方法,调用后,线程会释放锁.
  5. tryLock  尝试获得锁,在调用时所没有被线程占用,获得锁,
  6. tryLock(long timeout,TimeUnit unit); 如果锁在给定的等待时间内没有被另一个线程保持,则获得该锁

Synchronized与ReentrantLock

        1.ReenTrantLock通过lock方法和unlock方法来进行加锁与解锁操作,synchronized是JVM自动加解锁,ReenTrantLock加锁后需要手动解锁.为了避免程序出现异常导致无法解锁的情况,使用ReenTrantLock需要再finally中使用unlock进行解锁.

        2.ReenTrantLock是可中断,公平锁

公平锁与非公平锁

        非公平锁,JVM按随机,就近原则分配锁的机制称为不公平锁,ReenTrantLock在构造函数中提供了是否公平锁的初始化方式.默认为非公平锁,非公平锁的实际执行效率要远远超出公平锁,除非程序有特殊需要,否则最常用的分配机制是非公平锁.

        公平锁,公平锁指的是锁的分配机制是公平,通常先对锁提出获取请求的线程会被分配到锁,ReenTrantLock在构造函数中可以初始化为公平锁.

Condition类和Object类锁方法

        1.Condition类的await方法和Object类的wait方法是等效的.

        2.Condition类的signal方法和Object类的notify方法是等效的.

        3.Condition类的signalAll方法和Object类的notifyAll方法是等效的.

        4.ReentrantLock可以唤醒指定条件的线程,Object的唤醒是随机的.

tryLock 、lock 和 lockInterruptibly

        tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit)可以增加时间限制,如果超过该时间还没获得锁,返回false,

        lock能获得锁就返回true,不能获的锁就会一直等待,直到获得锁为止.

        lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock不会抛出异常,lockInterruptibly会抛出异常.

可重入锁

        可重入锁也叫递归锁,同一个线程在获得到某个锁之后,可以重复获取该锁不会阻塞自己,也不会形成死锁.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值