关于线程的一些面试题

线程的生命周期

线程状态分为五种,分别是新建状态,就绪状态,运行状态,阻塞状态和终止状态。

当实例化Thread对象后, 线程就处于新建状态. 这时线程并没有执行。

当线程对象调用start()方法后,线程会从新建状态切换为就绪状态

就绪状态属于一种临时状态。处于就绪状态的线程会去抢占CPU,只要抢占成功就会切换到运行状态。

处于运行状态的线程,开始执行线程的功能。例Threadrun()方法, Callablecall()方法。在这个状态的线程分为多种可发生情况。

1. 如果失去了CPU使用权,或调用yield()方法会变为就绪状态

2. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态

3. 如果线程功能成功执行完成,或出现问题或被停止会切换为终止状态。终止状态也表示线程执行完成了。

而阻塞状态的线程停止执行。让出CPU资源。在这个状态的线程也分为多种情况

1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态

2. 如果是因为wait()变为阻塞状态,需要调用notify()notifyAll()手动切换为就绪状态 

3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态

创建线程的几种方式(如何实现多线程)

继承Thread类

        继承Thread类方式只需要创建一个Thread类的子类并重写run法就可以了。如果只使用一次,可简化 为匿名内部类的形式。相比于定义一个Thread类的子类方式更加简单。这种方式在简单点的线程使用时 可以使用。

public class Demo {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("子线程");
    }
}
//使用匿名内部类
public class Demo {
    public static void main(String[] args) {
       Thread thread = new Thread(){
           @Override
           public void run() {
               super.run();
               System.out.println("子线程");
           }
       };
       thread.start();
    }
}

 实现Runnable接口

实现Runnable接口方式需要定义一个类,让类实现Runnable接口,并重写Runnable中抽象方法run法。在run方法中添加线程功能。当需要创建线程时,把这个类示例作为Thread的构造方法参数传入。  如果希望启动线程,需要通过Thread对象调用start()方法。

public class Demo {
    public static void main(String[] args) {
       MyRunnable runnable = new MyRunnable();
       Thread thread = new Thread(runnable);
       thread.start();
    }
}
class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("子线程");
    }
}

实现Callable接口

        实现Callable接口方式需要定义一个类,实现Callable接口,实现时必须定义Callable泛型类型,表示可以通过线程执行最终返回结果类型。然后重写call方法,里面完成线程功能。最终返回一个结果。当需要实例化线程时时,需要把Callable实现类的对象作为FutureTask的构造数,FutureTaskRunnable Future接口的实现类。实例化FutureTask后,作为Thread构造参数传入。当线程执行结束后,可以通过 FutureTask获得Callable实现类中call方法的返回结果。

public class Demo {
    public static void main(String[] args) {
       MyCallable myCallable = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(myCallable);
        Thread thread = new Thread(ft);
        thread.start();
        try{
            //可以读取结果
            System.out.println(ft.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        Random random = new Random();
        int i = random.nextInt(10);
        return i;
    }
}

使用线程池

关于线程池的创建方法,优势以及线程池常用参数,具体的见下文,人家比我写的好多了。

http://t.csdnimg.cn/noQCH

几种锁的概念(如何实现线程同步)

是否必须上锁:乐观锁、悲观锁。

什么是悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

什么是乐观锁

其实是一种无锁的状态,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

像 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试(悲观锁的开销是固定的),这样同样会非常影响性能,导致 CPU 飙升。不过,大量失败重试的问题也是可以解决的,像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。

理论上来说:

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。
如何实现乐观锁
版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

CAS 算法

CAS 的全称是 Compare And Swap(比较与交换),用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令(即不可拆分,操作一旦开始,就不会被中断)。

CAS 涉及到三个操作数:

  1. V :要更新的变量值(Var)
  2. E :预期值(Expected)
  3. N :拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。

sun.misc包下的Unsafe类提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的 CAS 操作。

乐观锁存在哪些问题?

ABA 问题是乐观锁最常见的问题。
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直自旋直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:

可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

总结:

  • 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
  • 乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
  • CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
  • 乐观锁的问题:ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作。

详情可见:http://t.csdnimg.cn/6cub5 写的很棒!

是否需要程序员释放锁:内置锁和显示锁 

在Java中内置锁通常指synchronized关键字,显示锁就是Lock,他们的区别如下:

  1. 类型不同:synchronized是关键字,修饰方法和代码块,Lock是接口
  2. 加锁和解锁机制不同:

    synchronized是自动加锁和解锁,程序员不需要控制。

    Lock必须由程序员控制加锁和解锁过程, 解锁时, 需要注意出现异常不会自动解锁。

  3. 异常机制:

    synchronized碰到没有处理的异常,会自动解锁,不会出现死锁。

    Lock碰到异常不会自动解锁,可能出现死锁。所以写Lock锁时都是把解锁放入到finally{}中。

  4. Lock功能更强大:Lock里面提供了tryLock()/isLocked()方法,进行判断是否上锁成功。synchronized因为是关键字, 所以无法判断。

  5. Lock性能更优:如果多线程竞争锁特别激烈时,  Lock的性能更优。如果竞争不激烈,性能相差不大。

  6. 线程通信方式不同:

    synchronized使用wait()notify()线程通信。

    Lock使用Conditionawait()signal()通信。

  7. 暂停和恢复方式不同:

    synchronized 使用suspend()resume()暂停和恢复,这俩方法过时了。

    Lock使用LockSupportpark()unpark()暂停和恢复,这俩方法没有过时。

参考:Lock锁底层原理实现  http://t.csdnimg.cn/1u69i  插个锚点,学完回来补充。

是否能继续获得锁:重入锁和非重入锁

重入锁:某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入 JavasynchronizedReenrentLock都是重入锁。

重入锁底层实现:计数器。当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到 这个锁时,如果是可重入锁就对持有锁数量再次加1  (如果是不可重入锁,发现持有锁为1了,就不允许 多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。

锁的资源竞争形势:公平锁、非公平锁

公平锁:严格按照顺序执行。先排队先执行(FIFO( First Input First Output))。

非公平锁:多线程在等待时,如果发现可以竞争,谁竞争成功,谁获取锁。非公平锁的效率要高于公平锁。ReentrantLock默认就是非公平锁。提供了一个有参构造方法来控制是否为公平锁。

互斥锁、排它锁、共享锁、读锁、写锁、锁升级、锁降级

读锁:又叫共享锁。多个读锁可以共同执行。

写锁:又是排它锁/互斥锁。一个写锁线程执行,其他写锁线程等待。

读锁里面又用写锁,叫锁升级。  ReadWriteLock不支持锁升级。出现死锁现象。

写锁里面又用读锁,锁降级。支持。

当多个线程又有读锁,又有写锁。读锁可以同时执行,但写锁需要等待读锁执行完成,才能执行。

  • 34
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值