线程面试题

本文详细阐述了线程与进程的区别,包括它们在资源分配、开销、控制能力等方面的差异,并介绍了创建线程的不同方式,如Runnable接口、Thread类和Callable接口。此外,讲解了线程池的种类、核心参数和线程锁的使用,涉及synchronized、ReentrantLock、死锁、乐观锁和悲观锁,以及分布式锁的概念和范围对比。
摘要由CSDN通过智能技术生成

1、线程与进程的关系和区别

进程
进程是一个正在执行的程序的实例,包括
线程
是进程的一个执行单元,比进程更小的独立运行的基本单位
区别
1、进程是操作系统资源分配的基本单位,而线程是处理器(cpu)任务调度和执行的基本单位。
2、从属关系不同:进程中包含了线程,线程属于进程。
3、开销不同:进程的创建、销毁和切换的开销都远大于线程。
4、拥有资源不同:每个进程有自己的内存和资源,一个进程中的线程会共享这些内存和资源。
5、控制和影响能力不同:子进程无法影响父进程,而子线程可以影响父线程,如果主线程发生异常会影响其所在进程和子线程。
6、CPU利用率不同:进程的CPU利用率较低,因为上下文切换开销较大,而线程的CPU的利用率较高,上下文的切换速度快。
7、操纵者不同:进程的操纵者一般是操作系统,线程的操纵者一般是编程人员。

2、创建线程的方式有哪些

1、实现Runnable接口,重写run();

public class MyRunnable implements Runnable{
    // 线程任务
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            System.out.println(Thread.currentThread().getName()+": java"+i);
        }
    }
}

2、继承Thread类,重写run();

public class MyThread extends Thread {
    // 线程任务!
    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            // 获得当前线程的名称!
            System.out.println(this.getName()+":itcast" + i);
        }
    }
}

以上两种没有返回值,以下两种有返回值
3、实现Callable接口,重写call(),利用FutureTask包装Callable,并作为task传入Thread构造函数;
FutureTask类简介实现了Runnable接口

/*
    Callable接口实现类
 */
 //接口类型时什么,返回值类型就是什么
public class MyCallable implements Callable<String> {

    // 线程任务:方法的返回值类取决于泛型接口的泛型变量的具体类型
    @Override
    public String call() throws Exception {
        for (int i = 1; i <= 1000; i++) {
            System.out.println("我爱Java" + i);
        }
        // 执行线程任务后返回的结果!
        return "over";
    }
}

/*
    基于Callable接口的线程实现方式的测试类
 */
public class MyCallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 创建Callable接口子类对象
        MyCallable mc = new MyCallable();
        // 将Callable接口子类对象作为FutureTask类构造方法的参数传递
        FutureTask<String> task = new FutureTask<>(mc);
        // 将FutureTask类的对象作为Thread类构造方法的参数传递
        Thread t = new Thread(task);
        // 使用Thread类的对象调用start方法启动线程
        t.start();

        /// 获得线程执行的结果!===>>> 封装到了FutureTask对象中去了!
        String s = task.get();
        System.out.println("s:" + s);

        // main线程
        for (int i = 1; i <= 1000; i++) {
            System.out.println("itcast" + i);
        }
    }
}

4、利用线程池创建线程

public class MyRunnableTest {

    public static void main(String[] args) {
        // 1. 使用工具类Executors的静态方法得到线程池对象ExecutorService!
        ExecutorService pool = Executors.newFixedThreadPool(3);
        // System.out.println(pool); // java.util.concurrent.ThreadPoolExecutor@1540e19d

        // 2. 定义Runnable接口的实现类,重写run方法(任务)并创建Runnable接口实现类的对象
        MyRunnable mr = new MyRunnable();

        // 3. 使用线程池对象ExecutorService的submit方法提交线程任务(Runnable接口实现类的对象)
        // pool.submit(mr);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    System.out.println("itheima"+i);
                }
            }
        });

        // 4. 关闭线程池(一般不做)
        pool.shutdown();

    }
}

不要在生产环境使用Executors直接创建线程,因为会出现OOM问题,应该使用ThreadPoolExecutor自定的方式来创建线程

3、线程池有哪些

(1)newSingleThreadExecutor():单个线程的线程池,即线程池中每次只有一个线程工作保证所有任务按照指定顺序执行(FIFO,LIFO,优先级),单线程串行执行任务。
(2)newFixedThreadPool():创建一个定长的线程池,每提交一个任务就占用一个线程。直到线程池的最大数量,然后后面进入队列的就得在空出线程之前一直等待。
(3)newCacheThreadPool():可缓存线程池(推荐使用),当线程池大小超过了处理任务所需的线程数时,那么就回收部分线程(一般是60S内未执行)。当任务需要的线程数超过了线程池中已有的线程数时,又会创建一定量的线程来满足任务使用的需求。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(4)newScheduleThreadPool():创建一个定长的线程池,支持定时和周期性任务执行。
(5)newSingleThreadScheduledExcutor:创建一个单例线程池,定期或延时执行任务。
(6)newWorkStealingPool:创建持有足够线程的线程池来支持给定的并行级别,并通过使用多个队列,减少竞争,它需要穿一个并行级别的参数,如果不传,则被设定为默认的CPU数量。
(7)ForkJoinPool:支持大任务分解成小任务的线程池,这是Java8新增线程池,通常配合ForkJoinTask接口的子类RecursiveAction或RecursiveTask使用。
[参考内容:常用的七种线程池]

4、线程池7大核心参数

构造方法:有7个参数
参数1:核心线程数量
参数2:最大线程数
参数3:空闲线程最大存活时间(数值)
参数4:时间的单位(java.util.concurrent包的下TimeUnit枚举)
参数5:任务队列(让任务在队列中等待,一旦线程有空闲,那么任务队列的任务就会被安排执行)【任务数量超过最大线程数的线程就会放到任务队列中】
参数6:创建线程的工厂(默认按照new Thread)
参数7:任务拒绝的策略 【AbortPolicy是ThreadPoolExecutor类的一个内部类】
* 何时拒绝?提交的任务数量超过 最大线程数和任务队列的总和,那些超过的部分的任务会被拒绝!
* 如何拒绝?有四种拒绝策略(默认的策略 AbortPolicy)

四种任务拒绝策略
ThreadPoolExecutor.AbortPolicy:超出任务被丢弃并抛出异常【默认的拒绝策略】
ThreadPoolExecutor.DiscardPolicy:超出任务被丢弃但不抛异常 【不推荐】
ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列
ThreadPoolExecutor.CallerRunsPolicy:调用任务的run方法绕过线程池直接执行[main线程]

如何确定核心线程数
[参考内容:核心线程数]

5、线程常用的命令及概念

Object类的方法

void wait(); // 让当前线程进入无限等待状态,此方法必须使用锁对象来调用!
void notify(); // 唤醒正在等待对象监视器的单个线程。 此方法必须使用锁对象来调用!【保证与无限等待的那个线程(要被唤醒的那个线程)的锁对象是同一个】
void wait(long timeout); // 计时等待! 

Thread类的方法

* 构造方法:
	Thread() 分配一个新的 Thread对象。 【此线程使用默认名称 Thread-0 Thread-1。。。。。。】 
	Thread(String name) 分配一个新的 Thread对象。 【此线程有名称,是指定的name】 
	Thread(Runnable target) 分配一个新的 Thread对象。  					★★★★★
	Thread(Runnable target, String name) 分配一个新的 Thread对象。 		  ★★★★★ 
	
* 功能方法:===>>> 可以让子类继承过去使用!!!
	String getName() 返回此线程的名称。 
	void start(); // 启动线程 
	void run(); // 线程任务
	void setName(String name); // 更改线程的名称
    
	static void sleep(long millis) // 线程沉睡指定毫秒值时间
	static Thread currentThread()  // 获得当前正在执行的线程对象!

问题:sleep(long time)方法与wait(long timeout)有何区别? ===>>> 前者在等待过程中一直持有锁对象!后者在等待过程中会释放锁对象!

6、线程的生命周期

在这里插入图片描述

7、线程锁

7.1 synchonized与lock

synchronized
java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法,锁是当前实例对象,也就是this
  • 静态同步方法,锁是当前类的Class对象(字节码对象,类名.class)
  • 同步代码块,锁是括号里面的对象

从功能来看
lock和synchronized都是java中解决线程安全问题的一个工具

从特性来看
1、synchronized是一个同步关键字,lock是juc包里面提供的一个接口,这个接口他有很多的实现类,其中包括Reentrantlock这个重入锁的实现。

从锁粒度来看
synchronized可以通过两种方式控制锁的粒度:
1、把synchronized关键字修饰在方法层面
2、修饰在同步代码块上
并且我们可以通过synchronized加锁对象的生命周期,来控制锁的作用范围,比如锁对象是静态对象,或者类对象,那么这个锁就是属于全局锁,如果锁对象是实例对象,那么这个锁的范围取决于这个对象的生命周期。
lock:
1、包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于Lock实例的生命周期。
2、Lock比Synchronized的灵活性更高,Lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()这两个方法就行,同时Lock还提供了非阻塞的竞争锁方法tryLock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁。
3、Synchronized由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外Synchronized锁的释放是被动的,就是当Synchronized同步代码块执行完以后或者代码出现异常时才会释放。
4、Lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized只提供了一种非公平锁的实现。

性能方面
synchronized在性能方面和lock相差不大,在实现上会有一个区别synchronized引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock则用到了自旋锁的方式实现性能优化。

7.2 可重入锁是什么?

可重入锁是多线程并发编程里面比较重要的一个概念,在运行的某个函数或者代码,因为抢占资源或者中断,导致这个函数或者代码运行过程中被中断了,那么等到中断的程序执行结束以后,重新进入到这个函数的代码里面,再运行的时候,并且运行的结果不会发生改变,这个函数或者代码就是可重入的,所以重入锁就是一个线程如果抢占到了互斥锁的资源,在锁释放之前,再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。绝大部分锁都是可重入的,如,Synchronized和ReentrantLock,不支持可重入的锁,如,JDK8里的读写锁,Stampedlock,锁的可重入性,主要解决问题是避免死锁的问题,因为一个已经获得同步锁的一个线程,在释放锁之前再次去竞争锁的时候,相当于是自己等待自己释放锁的一个情况,就会导致死锁,

7.3 死锁、产生死锁的条件

7.4 volatile与Synchronized比较

volatile的作用
1、线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2、顺序一致性:禁止指令重排序。
JMM(Java内存模型)
在这里插入图片描述
没有volatile关键字修饰实例变量或者类变量,那么在一个线程修改了共享变量后,不会把修改的值刷新到主内存,所以别的线程自然不会读取到新的值来修改自己本地内存的变量,这就是volatile的可见性

两者的区别区别
1、Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
2、Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
3、Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
4、多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
5、volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

7.5 死锁、产生死锁的条件

什么是死锁
死锁就是两个或者两个以上线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象。
死锁产生的原因
1、互斥条件:一个资源只能被一个线程占有,当这个资源被占用后其他线程就只能等待。
2、不可剥夺条件:当一个线程不主动释放资源时,此资源一直被拥有线程占有。
3、请求并持有条件:线程已经拥有一个资源后仍然不满足,有尝试请求新的资源。
4、环路等待条件:产生死锁一定是放生了线程资源环路链。

7.6 乐观锁

什么是乐观锁
在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有跟新过这个数据。
两种乐观锁
1、原子类的CAS机制
2、版本号控制,主要用于数据库层面的并发控制
CAS
原子类
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
AtomicInteger:原子型Integer,可以实现原子更新操作。

public AtomicInteger()// 初始化一个默认值为0的原子型Integer [重要]
public AtomicInteger(int initialValue)// 初始化一个指定值的原子型Integer
int get(): // 获取值
int getAndIncrement(): // 以原子方式将当前值加1,注意,这里返回的是自增前的值。[重要]
int incrementAndGet(): // 以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data): // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):  // 以原子方式设置为newValue的值,并返回旧值。

案例

public class VolatileAtomicThread implements Runnable {

    // 创建一个int类型对应的原子类(给定初始值为0)
    AtomicInteger i = new AtomicInteger();

    @Override
    public void run() {

        // 对该变量进行++操作,100次
        for (int x = 0; x < 100; x++) {
            
            // 先对原子类里面的int类型的数值+1,然后返回!【+1动作,都是在一个原子内完成的】
            int count = i.incrementAndGet();
            System.out.println("count =========>>>> " + count);
        }
    }

}

原子类CAS机制

CAS的全成是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS可以将read-modify-write转换为原子操作,这个原子操作直接由处理器保证。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

在这里插入图片描述
在这里插入图片描述

  • 比较再交换:可以将read-modify-write转换为原子操作!
    ①在内存地址V当中,存储着值为10的变量。
    ②此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
    ③在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
    ④线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
    ⑤线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
    ⑥这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
    ⑦线程1进行SWAP,把地址V的值替换为B,也就是12。

7.7 悲观锁

什么会悲观锁
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
实现方式
加锁(Synchronized和Lock的实现类都是悲观锁)

7.8 自旋锁

8、线程锁、进程锁、分布式锁

**线程锁:**主要用来给方法、代码块加锁。当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。

**进程锁:**也是为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制(操作系统基本知识)。

**分布式锁:**当多个进程不在同一个系统之中时,使用分布式锁控制多个进程对资源的访问。

范围大小:分布式锁——大于——进程锁——大于——线程锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值