java高级工程师面试题及答案解析干货汇总-IO和多线程篇

IO和多线程

1.介绍下进程、线程和协程的关系

进程:操作系统进行资源分配和调度的基本单位。每个进程有独立的内存空间。进程通讯就采用共享内存,MQ,管道。

线程:一个进程的最基本的执行单位。一个进程可以包含多个线程,线程就是CPU调度的基本单位。一个线程只属于某一个进程。线程之间通讯,队列,await,signal,wait,notity,Exchanger,共享变量等等都可以实现线程之间的通讯。

image.png
协程:一种用户态的轻量级线程。它是由程序员自行控制调度的。可以显示式的进行切换。
一个线程可以调度多个协程。
协程只存在于用户态,不存在线程中的用户态和内核态切换的问题。协程的挂起就好像线程的yield。
可以基于协程避免使用锁这种机制来保证线程安全。

多进程:在操作系统中,同时运行多个程序

多进程的好处:可以充分利用CPU,提高CPU的使用率

多线程:在同一个进程(应用程序)中同时执行多个线程

多线程的好处:提高进程的执行使用率,提高了CPU的使用率

注意:

  1. 在同一个时间点一个CPU中只可能有一个线程在执行
  2. 多线程不能提高效率、反而会降低效率,但是可以提高CPU的使用率
  3. 一个进程如果有多条执行路径,则称为多线程程序
  4. Java虚拟机的启动至少开启了两条线程,主线程和垃圾回收线程
  5. 一个线程可以理解为进程的子任务

单独的拿协程和线程做一个对比:

  • 更轻量: 线程一般占用的内存大小是MB级别。协程占用的内存大小是KB级别。
  • 简化并发问题: 协程咱们可以自己控制异步编程的执行顺序,协程就类似是串行的效果。
  • 减少上下文切换带来的性能损耗: 协程是用户态的,不存在线程挂起时用户态和内核态的切换,也不需要去让CPU记录切换点。
  • 协程优化的点: 协程在针对大量的IO密集操作时,协程可以更好有去优化这种业务。

1.1 线程的特点

  • 轻量级:线程的创建和销毁的开销相对较小,可以创建大量的线程。
  • 共享内存:多个线程可以共享同一块内存区域,这使得线程之间可以方便地进行数据通信。
  • 独立调度:每个线程的执行是由操作系统进行调度的,线程的调度是非确定性的,也就是说无法预测线程的执行顺序。

1.2 项目中为什么使用多线程?

压榨CPU,为了提升效率。

优化某一个接口,单线程处理,500ms,你上了多线程,可能200ms了。但是,不是所有接口都能上多线程优化,要看业务。

(IO密集)比如业务中有多个没有关联的网络IO的操作,可以上多线程并行处理,减少IO对程序性能带来的影响。

(CPU密集)比如你有一个相对比较大的数据体量做计算或者做数据的封装。可以将比较庞大的数据量做一个切分,让多个线程同时做处理,最后聚合在一起。也可以提升处理效率。

2.说说Java中实现多线程的几种方法

Thread对象就是一个线程,创建线程的常用三种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口(JDK1.5>=)
  4. 线程池方式创建

本质都是Runnable,都是一种。

因为继承Thread,间接实现了Runnableimage.png
实现Callable,需要配合FutureTask来执行,执行的是FutureTask中的run方法,而FutureTask实现了RunnableFuture的接口,RunnableFuture的接口又继承的Runnable。

image.png
使用线程池,线程池中的工作线程是Worker,Worker实现了Runnable,在构建工作线程时,会new Worker对象,将Worker传递给线程工厂构建的Thread对象。本质还是Runnable。

Runnable和Callable区别:如果启动子线程执行任务后需要有返回结果,使用Callable。

  • Runnable的run方法,无法抛出异常,返回结果就是void

  • Callable的call方法,可以抛出异常,返回结果是Object

2.1 继承Thread

实现的步骤:

  1. 创建Thread类的子类
  2. 重写run方法
  3. 创建线程对象
  4. 启动线程

案例代码

package com.bobo.thread;

public class ThreadDemo02 {

    /**
     * 线程的第一种实现方式
     *     通过创建Thread类的子类来实现
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("main方法执行了1...");
        // Java中的线程 本质上就是一个Thread对象
        Thread t1 = new ThreadTest01();
        // 启动一个新的线程
        t1.start();
        for(int i = 0 ; i< 100 ; i++){
            System.out.println("main方法的循环..."+i);
        }
        System.out.println("main方法执行结束了3...");
    }
}

/**
 * 第一个自定义的线程类
 *    继承Thread父类
 *    重写run方法
 */
class ThreadTest01 extends Thread{

    @Override
    public void run() {
        System.out.println("我们的第一个线程执行了2....");
        for(int i = 0 ; i < 10 ; i ++){
            System.out.println("子线程:"+i);
        }
    }
}

注意点:

  1. 启动线程是使用start方法而不是run方法
  2. 线程不能启动多次,多次启动会报异常。如果要创建多个线程,那么就需要创建多个Thread对象

image.png

2.2 实现Runnable接口

通过Runnable接口来实现线程程序代码和数据有效的分离。

Thread(Runnable target)
// 分配一个新的 Thread对象。

实现的步骤:

  1. 创建Runable的实现类
  2. 重写run方法
  3. 创建Runable实例对象(通过实现类来实现)
  4. 创建Thread对象,并把第三部的Runable实现作为Thread构造方法的参数
  5. 启动线程
package com.bobo.runable;

public class RunableDemo01 {

    /**
     * 线程的第二种方式
     *     本质是创建Thread对象的时候传递了一个Runable接口实现
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("main执行了...");
        // 创建一个新的线程  Thread对象
        Runnable r1 = new RunableTest();
        Thread t1 = new Thread(r1);
        // 启动线程
        t1.start();
        System.out.println("main结束了...");
    }
}

/**
 * 线程的第二种创建方式
 *   创建一个Runable接口的实现类
 */
class RunableTest implements Runnable{

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

实现Runable接口的好处:

  1. 可以避免Java单继承带来的局限性
  2. 适合多个相同的程序代码处理同一个资源的情况,把线程同程序的代码和数据有效的分离,较好的体现了面向对象的设计思想

2.3 实现Callable接口

可以获取对应的返回结果。

package com.bobo.callable;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo01 {

    /**
     * 创建线程的第三种实现方式:
     *    Callable方式
     */
    public static void main(String[] args) throws  Exception {
        // 创建一个Callable实例
        Callable<Integer> callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 获取一个线程 肯定是要先创建一个Thread对象  futureTask本质上是Runable接口的实现
        Thread t1 = new Thread(futureTask);
        System.out.println("main方法start....");
        t1.start(); // 本质还是执行的 Runable中的run方法,只是 run方法调用了call方法罢了
        // 获取返回的结果
        System.out.println(futureTask.get()); // 获取开启的线程执行完成后返回的结果
        System.out.println("main方法end ....");

    }
}

/**
 * 创建Callable的实现类
 *    我们需要指定Callable的泛型,这个泛型是返回结果的类型
 */
class MyCallable implements Callable<Integer>{

    /**
     * 线程自动后会执行的方法
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i = 1 ; i <= 100 ; i ++){
            sum += i;
        }
        return sum;
    }
}

其实Callable接口底层的实现就是对Runable接口实现的封装,线程启动执行的也是Runable接口实现中的run方法,只是在run方法中有调用call方法罢了

2.4 线程池

3.如何停止一个正在运行的线程

3.1 stop方法(不推荐)

stop方法,会直接强制停止线程,不让执行。

用了stop会有什么问题?比如ConcurrentHashMap在执行put方法时,需要先将数据扔到数组或者链表或者红黑树里,扔进去之后,还需要记录元素个数,做+1操作。如果线程执行put后,数据扔进去了,但是没执行+1,导致线程安全问题。

3.2 run方法结束

(正常结束,异常结束)

3.2.1 设置标志位flag

如果线程的run方法中执行的是一个重复执行的循环,可以提供一个标记来控制循环是否继续。

public class FunDemo02 {

    /**
     * 练习2:设计一个线程:运行10秒后被终止(掌握线程的终止方法)
     * @param args
     */
    public static void main(String[] args)  throws Exception{
        MyRunable02 runnable = new MyRunable02();
        new Thread(runnable).start();
        Thread.sleep(10000); // 主线程休眠10秒钟
        runnable.flag = false;
        System.out.println("main、  end ...");
    }
}

class MyRunable02 implements Runnable{

    boolean flag = true;

    @Override
    public void run() {
        while(flag){
            try {
                Thread.sleep(1000);
                System.out.println(new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 执行完成");
    }
}
3.2.1.1 利用中断标志位interrupt+isInterrupted

在线程中有个中断的标志位,默认是false,当我们显示的调用 interrupt方法或者isInterrupted方法是会修改标志位为true。我们可以利用此来中断运行的线程。

package com.bobo.fundemo;

public class FunDemo07 extends Thread{

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new FunDemo07();
        t1.start();
        Thread.sleep(3000);
         t1.interrupt(); // 中断线程 将中断标志由false修改为了true
        // t1.stop(); // 直接就把线程给kill掉了
        System.out.println("main .... ");
    }

    @Override
    public void run() {
        System.out.println(this.getName() + " start...");
        int i = 0 ;
        // Thread.interrupted() 如果没有被中断 那么是false 如果显示的执行了interrupt 方法就会修改为 true
        while(!Thread.interrupted()){
            System.out.println(this.getName() + " " + i);
            i++;
        }

        System.out.println(this.getName()+ " end .... ");

    }
}

3.2.1.2 利用InterruptedException(interrupt+isInterrupted)

如果线程处于阻塞的状态下,比如await,wait,在阻塞队列,sleep等等,此时要想停止它,可以让他调用interrupt,程序会抛出InterruptedException异常。我们利用这个异常可以来终止线程。

package com.bobo;

public class FunDemo08 extends Thread{

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new FunDemo08();
        t1.start();
        Thread.sleep(3000);
         t1.interrupt(); // 中断线程 将中断标志由false修改为了true
        // t1.stop(); // 直接就把线程给kill掉了
        System.out.println("main .... ");
    }

    @Override
    public void run() {
        System.out.println(this.getName() + " start...");
        int i = 0 ;
        // Thread.interrupted() 如果没有被中断 那么是false 如果显示的执行了interrupt 方法就会修改为 true
         while(!Thread.interrupted()){
        //while(!Thread.currentThread().isInterrupted()){
             try {
                 Thread.sleep(10000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
				 break;
             }
             System.out.println(this.getName() + " " + i);
            i++;
        }

        System.out.println(this.getName()+ " end .... ");

    }
}

3.2.2 直接指定共享变量
volatile boolean flag = false;
run(){
	while(!flag){
		// 处理任务!!
	}
}

扩展: 主线程启动了子线程,但是主线程凉了,子线程还在么?

这里要看子线程是用户线程,还是守护线程。

如果是用户线程,主线程凉了,不影响子线程。

如果子线程是守护线程,主线程一凉,子线程也凉!

4.介绍下线程中的常用方法

1.start

start方法是我们开启一个新的线程的方法,但是并不是直接开启,而是告诉CPU我已经准备好了,快点运行我,这是启动一个线程的唯一入口。

void	start()
// 导致此线程开始执行; Java虚拟机调用此线程的run方法。

2.run

线程的线程体,当一个线程开始运行后,执行的就是run方法里面的代码,我们不能直接通过线程对象来调用run方法。因为这并没有产生一个新的线程。仅仅只是一个普通对象的方法调用。

void	run()
// 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。

3.getName

获取线程名称的方法

String	getName()
返回此线程的名称。

4.优先级

我们创建的多个线程的执行顺序是由CPU决定的。Java中提供了一个线程调度器来监控程序中启动后进入就绪状态的所有的线程,优先级高的线程会获取到比较多运行机会

    /**
     * 最小的优先级是 1
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * 默认的优先级都是5
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * 最大的优先级是10
     */
    public final static int MAX_PRIORITY = 10;

测试发现,设置了优先级后输出的结果和我们预期的并不一样,这是为什么呢?优先级在CPU调动线程执行的时候会是一个参考因数,但不是决定因数。

5.sleep

将当前线程暂定指定的时间,

static void	sleep(long millis)
// 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。

6.isAlive

获取线程的状态。

package com.bobo.fundemo;

public class FunDemo04 {

    /**
     * isAlive方法
     * @param args
     */
    public static void main(String[] args) {

        System.out.println("main  start ...");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " .... ");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("线程的状态:"+t1.isAlive());
        t1.start();
        System.out.println("线程的状态:"+t1.isAlive());
        System.out.println("main  end ...");
    }
}

输出结果

main  start ...
线程的状态:false
线程的状态:true
main  end ...
Thread-0 .... 

7.yield

让出CPU,当前线程进入就绪状态

package com.bobo.fundemo;

public class FuneDemo06 extends Thread{

    public FuneDemo06(String threadName){
        super(threadName);
    }

    /**
     * yield方法  礼让
     *
     * @param args
     */
    public static void main(String[] args) {
        FuneDemo06 f1 = new FuneDemo06("A1");
        FuneDemo06 f2 = new FuneDemo06("A2");
        FuneDemo06 f3 = new FuneDemo06("A3");

        f1.start();
        f2.start();
        f3.start();
    }

    @Override
    public void run() {
        for(int i = 0 ; i < 100; i ++){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(i%10 == 0 && i != 0){
                System.out.println(Thread.currentThread().getName()+" 礼让:" + i);
                Thread.currentThread().yield(); // 让出CPU
            }else{
                System.out.println(this.getName() + ":" + i);
            }
        }
    }
}

8.线程通信的方法

1. wait/notify

wait()方法使线程进入等待状态,直到其他线程调用notify()或notifyAll()方法将其唤醒。
notify()方法唤醒一个等待中的线程,notifyAll()方法唤醒所有等待中的线程。

2. wait(long timeout)/notify

wait(long timeout)方法使线程进入等待状态,直到其他线程调用notify()方法将其唤醒,或者等待时间超过指定的timeout时间。
notify()方法唤醒一个等待中的线程。

3. join

当前线程将被阻塞,直到另一个线程执行完毕。 比如主线程执行t1.join(),主线程需要等待t1执行完之后,再执行。

package com.bobo.fundemo;

public class FunDemo05 {

    /**
     * 线程的合并
     *     join方法
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("main  start ...");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0 ; i < 10; i++){
                    System.out.println(Thread.currentThread().getName() + " 子线程执行了...");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        });
        t1.start();
        try {
            t1.join(); // 线程的合并,和主线程合并  相当于我们直接调用了run方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end ...");
    }
}

输出结果:

main  start ...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
main end ...

Join方法本质是基于synchronized以及wait和notify实现的。直接针对当前线程对象加锁,然后wait挂起线程,wait判断的逻辑是t1线程是否存活(isAlive)。如果t1线程存活,主线程WAITING,如果t1线程凉凉了,isAlive会返回false,主线程不用挂起了,被唤醒。

4. Lock和Condition接口

Lock接口提供了比synchronized关键字更灵活的锁机制,Condition接口提供了更灵活的等待/通知机制。通过Lock接口的lock()方法获取锁,unlock()方法释放锁;通过Condition接口的await()方法使线程等待,signal()方法唤醒一个等待中的线程,signalAll()方法唤醒所有等待中的线程。

5. BlockingQueue阻塞队列

BlockingQueue是一个支持阻塞操作的队列,当队列为空时,获取元素的线程将被阻塞,直到队列中有可用元素;当队列满时,插入元素的线程将被阻塞,直到队列有空闲位置。

5.介绍下线程的生命周期

5.1 Java的6种线程状态

Java中的Thread类里有枚举,规定了,只有6种

image.png

  • NEW:新建
  • RUNNABLE:就绪

BLOCKED,WAITING,TIME_WAITING,本质上一样,都是CPU无法分配时间片。

  • BLOCKED:synchronized没拿到锁,阻塞。

  • WAITING:Unsafe.park(),JUC包下的类在挂起线程时,用的都是这个。

  • TIMED_WAITING:Unsafe.park(time,unit),默认阻塞这么久,会被自动唤醒,变为RUNNABLE状态

  • TERMINATED:结束

所谓线程挂起,上面三种,都是线程挂起。

5.2 操作系统的5种线程状态

  1. 新建状态(New):线程对象被创建后,但还没有调用start()方法时的状态。
  2. 就绪状态(Runnable):线程对象调用start()方法后进入就绪状态,表示线程可以被调度执行。
  3. 运行状态(Running):线程被调度执行后进入运行状态。
  4. 阻塞状态(Blocked):线程在执行过程中可能因为某些原因被阻塞,例如等待输入输出、线程休眠等。
  5. 结束状态(Terminated):线程执行完任务后进入结束状态。

图例如下:

image.png

生命周期:对象从创建到销毁的全过程。

线程的生命周期:线程对象(Thread)从开始到销毁的全过程。

image.png

线程的状态:

  1. 创建,Thread对象
  2. 就绪状态,执行start方法后线程进入可运行的状态
  3. 运行状态,CPU运行
  4. 阻塞状态,运行过程中被中断(等待阻塞,对象锁阻塞,其他阻塞)
  5. 终止状态,线程执行完成

5.3 线程池5种状态

  • running,正常接收+处理
  • shutdown,不接受+处理
  • stop,不接受+不处理
  • tidying,马上死亡
  • terminated,死亡

6.为什么wait, notify和notifyAll这些方法不在thread类里面?

JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。

7.为什么wait和notify方法要在synchronized里用?

wait和notify是在持有synchronized锁时,

  • wait方法是让持有锁的线程释放锁资源,并且挂起
  • notify方法是让持有锁的线程,去唤醒之前执行wait方法挂起的线程,让被唤醒的线程抢锁。

至于为何要在持有synchronized时,才能执行wait和notify,是因为在调整线程存放的队列时,需要持有当前synchronized锁里面的ObjectMonitor,没持有,不让操作。

扩展:在ObjectMonitor里,为什么有了cxq还要有EntryList?

synchronized到了重量级锁时,会利用CAS拿锁。image.png

源码地址:https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp

  • cxq(单向):当竞争激烈时,锁持有时间比较长的时候,将线程扔到cxq队列里,挂起。

  • EntryList(双向):缓冲上面的情况。

    • 为了避免大量线程追加到cxq队列的头部或者是尾部(默认头部),造成压力过大。
    • 当线程拿锁时,在重量级锁的情况下,也会走CAS,当自旋失败没拿到锁,优先扔到EntryList。

重量级锁怎么定义的:查看对象的对象头里的MarkWord里的标识

8.synchronized和ReentrantLock的区别

相似点
  这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,可重入锁。也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。
区别
  这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,它只能用于同步代码块或方法,它是jvm级别锁,获取和释放锁由jvm自动管理,无需手动控制。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁接口,需要lock()和unlock()方法配合try/finally语句块来完成。

  Synchronized进过编译,会在同步块的前后分别形成monitorentermonitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。Java中synchronized通过在对象头设置标记,达到了获取锁和释放锁的目的。所以synchronized是可重入锁

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。

2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁是非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象

8.1 Synchronized和lock的区别

Synchronized和Lock都是用于实现线程之间的同步的机制,但它们之间有一些区别。

1.锁的类型
Synchronized是Java中的关键字,它只能用于同步代码块或方法。而Lock是一个接口,Java提供了多种实现该接口的锁,如ReentrantLock、ReadWriteLock等。
使用方式:Synchronized是隐式锁,它的获取和释放由JVM自动管理,无需手动控制。
Lock是显式锁,需要手动调用lock()方法获取锁,并在合适的地方调用unlock()方法释放锁。

2.可中断性
在获取锁时,如果线程无法获取到锁,Synchronized会一直等待,直到获取到锁
Lock提供了可中断性,即可以在等待获取锁的过程中,中断线程的等待。

3.公平性
Synchronized只是非公平锁
Lock支持公平锁和非公平锁

4.条件变量
Lock提供了Condition接口,可以通过该接口实现线程之间的等待/通知机制。
Synchronized借助于**Object的wait()、notify()**和notifyAll()方法来实现。

总的来说,Synchronized是一种简单、易用的同步机制,适用于大多数情况下。而Lock提供了更多的灵活性和功能,适用于一些复杂的同步场景。在性能方面,Lock通常比Synchronized更加高效,(除非)但使用Lock需要手动管理锁的获取和释放,容易出错。

8.2 Synchronized可以锁什么?

锁本质:monitor enter和monitor exit字节码指令的一个reference类型参数,即要锁定和解锁的对象。

  • 对象,比如synchronized(变量名),synchronized(this),锁对象为该对象
  • 方法
    • 非静态方法,锁对象为该方法对应的对象
    • 静态方法,锁对象为该方法的类对象

当对象被锁,只有里面使用synchronized修饰的方法被阻塞,其他非synchronized修饰的方法正常被调用。

8.3 Synchronized与volatile、ThreadLocal区别?

在这里插入图片描述

9.什么是线程安全

  线程安全就是说多线程访问同一段代码,不会产生不确定的结果。
  如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
  像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
  不管运行时环境如何,调用者都不需要额外的同步措施。Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
  相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制
(4)线程非安全
  这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。

10.Thread类中yield方法的作用

  yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

11. 简述一下你对线程池的理解

优点
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。线程池有池化技术,规避频繁创建和销毁。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。并发大了,CPU要来回在线程之间切换分配时间片,如果线程太多,资源都浪费在线程切换上了。线程池可以指定好工作线程的个数,超过了,甩拒绝策略。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

11.1 创建线程池的方式

11.1.1 Java自带的构建方式(6种)

单例的Single:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。这个线程池只有一个核心线程工作。

定长的Fixed:创建固定大小的线程池,只有核心线程。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

非定长的Cached:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。如果线程60s不执行任务,就会被回收。

执行定时任务的Scheduled:创建一个大小无限的线程池,但是核心线程池固定,此线程池支持定时以及周期性执行任务的求。如果闲置,非核心线程池在default_keepalivemillis内被回收。

单例定时的SingleThreadSchedule:这个线程池它和ScheduledThreadPool很相似,只不过它的内部也只有一个线程

使用forkJoinPool的WorkStealing:它主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数。第一步是Fork拆分,第二步是Join汇总。每个线程都有自己独立的任务队列

11.1.2 自定义线程池(推荐)

Java自带的4种线程池,看源码发现它们的参数都是固定的,并不能满足实际工作的需要。

怎么使用呢?如下代码,

public ThreadPoolExecutor(int corePoolSize,
                               int maximumPoolSize,
                               long keepAliveTime,
                               TimeUnit unit,
                               BlockingQueue<Runnable> workQueue,
                               RejectedExecutionHandler handler) 

参数(7个)含义:

corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序

image.png

11.1.2.1 怎么自定义核心线程数呢?

公式:CPU核数 * CPU利用率 * (1 + w/s)

简单点,应用:

  • IO密集型:CPU核数 * 2
  • IO计算密集型:CPU核数

专业点,应用:
压测时,动态监控线程数指数,动态调整

12.线程池工作原理

提交一个任务到线程池中,线程池的处理流程如下:
image.png
简单流程:

  • 1、走构建核心线程
  • 2、扔阻塞队列
  • 3、构建非核心线程
  • 4、拒绝策略
    • 核心线程空闲时,在干嘛? 在阻塞队列执行take方法,等新任务呢。
    • 如果核心线程个数已经满了,那么任务会扔到阻塞队列,让核心线程处理。
    • 如果核心线程个数没达到要求,会构建新的核心线程,去处理这个任务。

12.1 核心线程参数设置为0,任务怎么处理?

如果核心线程数设置为0,任务会直接扔到阻塞队列。

但是现在出现了一个场景,阻塞队列有任务,但是没有线程,现在的情况叫任务饥饿

此时,会构建一个非核心线程,去处理阻塞队列中的任务。而且线程池的最大线程数,最少设置1。

核心线程数可以设置为0,但是最大线程数最少设置为1。最大线程数 = 核心线程数 + 非核心线程数。

12.2 线程池的工作线程如何区别核心和非核心的?怎么区分的?

线程池里,不区分核心线程跟非核心线程。仅仅是在创建的时候,基于有参构造的corePoolSize和maximumPoolSize,做个判断,工作线程最终都是由thread.start启动的。

举个栗子,如果线程池核心线程为3个,最大线程为4个。此时工作线程是4个,3个核心,1个非核心满满登登。

此时一个核心线程抛异常,结束了,那么会再创建一个核心线程么?

不会。因为线程池不区分核心和非核心,里面只判断个数,如果有一个工作线程凉了,那还是3个工作线程,满足参数的要求。

12.3 核心线程会结束么?

1、默认不会,但是可以设置参数,然核心线程也有超时时间。allowCoreThreadTimeOut,默认为false,可以设置为true。

2、本身不区分核心还是非核心,如果线程在抛出异常等原因,导致结束后,只会根据个数判断,是否满足核心线程参数要求。

13.线程池的拒绝策略有哪些?

主要有4种拒绝策略:

  1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
  2. CallerRunsPolicy:只用调用者所在的线程来处理任务
  3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务,也不抛出异常

14.线程安全需要保证几个基本特性?

原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。

15.说下线程间是如何通信的?

线程之间的通信有两种方式:共享内存和消息传递。

共享内存
  在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

image.png

例如线程A与线程B之间如果要通信的话,那么就必须经历下面两个步骤:
1.线程A把本地内存A更新过得共享变量刷新到主内存中去。
2.线程B到主内存中去读取线程A之前更新过的共享变量。
消息传递
  在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在Java中典型的消息传递方式,就是wait()和notify(),或者BlockingQueue。

线程通信的方法

1. wait/notify

wait()方法使线程进入等待状态,直到其他线程调用notify()或notifyAll()方法将其唤醒。
notify()方法唤醒一个等待中的线程,notifyAll()方法唤醒所有等待中的线程。

2. wait(long timeout)/notify

wait(long timeout)方法使线程进入等待状态,直到其他线程调用notify()方法将其唤醒,或者等待时间超过指定的timeout时间。
notify()方法唤醒一个等待中的线程。

3. join

join()方法使一个线程等待另一个线程执行完毕。当一个线程调用另一个线程的join()方法时,当前线程将被阻塞,直到另一个线程执行完毕。

package com.bobo.fundemo;

public class FunDemo05 {

    /**
     * 线程的合并
     *     join方法
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("main  start ...");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0 ; i < 10; i++){
                    System.out.println(Thread.currentThread().getName() + " 子线程执行了...");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            }
        });
        t1.start();
        try {
            t1.join(); // 线程的合并,和主线程合并  相当于我们直接调用了run方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end ...");
    }
}

输出结果:

main  start ...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
Thread-0 子线程执行了...
main end ...

4. Lock和Condition接口

Lock接口提供了比synchronized关键字更灵活的锁机制,Condition接口提供了更灵活的等待/通知机制。通过Lock接口的lock()方法获取锁,unlock()方法释放锁;通过Condition接口的await()方法使线程等待,signal()方法唤醒一个等待中的线程,signalAll()方法唤醒所有等待中的线程。

5. BlockingQueue阻塞队列

BlockingQueue是一个支持阻塞操作的队列,当队列为空时,获取元素的线程将被阻塞,直到队列中有可用元素;当队列满时,插入元素的线程将被阻塞,直到队列有空闲位置。

16.说说ThreadLocal的原理

ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。

相比于synchronized的做法是用空间来换时间。

在开发中会用到的方式就是传递参数

ThreadLocal有两个内存泄漏问题:

  • key: key会在玩花活使用ThreadLocal时 ,在局部声明ThreadLocal,局部方法已经执行完
    毕,但是线程会指向ThreadLocalMap,ThreadLocalMap的key会指向ThreadLocal对象,这会导致ThreadLocal会被对象不回收。所以ThreadLocal在设计时,将key的引用更改为了弱引用,如果再发生上述情况,此时ThreadLocal只有一个弱引用指向,可以被正常回收。
  • value: 如果是普通线程使用ThreadLocal,那其实不remove也不存在问题,因为线程会结束,销毁,线程一销毁,就没有引用指ThreadLocalMap了,自然可以回收。但是如果是线程池中的核心线程使用了ThreadLocal,那使用完毕,必须要remove,因为核心线程不会被销毁(默认),导致核心线程结束任务后,上一次的业务数据还遗留在内存中,导致内存泄漏问题。

ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用。

弱引用的目的是为了防止key的内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。

image.png
ThreadLocal是Java提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以在一个线程关联的不同业务模块之间传递信息,比如事务ID、Cookie等上下文相关信息。

17.IO干了什么?解释下:同步、异步、阻塞、非阻塞

IO干什么的?
在这里插入图片描述

同步和异步指的是:当前线程是否需要等待方法调用执行完毕,才执行下一步操作。 接口数据还未准备就绪时,线程是否被阻塞挂起。

阻塞和非阻塞指的是:接口数据还未复制,线程是否被阻塞挂起。

同步&异步其实是处于框架这种高层次维度来看待的,而阻塞&非阻塞往往针对底层的系统调用方面来抉择,也就是说两者是从不同维度来考虑的。

这四个概念两两组合,会形成4个新的概念,如下:

同步阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,则客户端则被服务端堵塞了,所以客户端会一直等待服务端的响应,此时客户端不能做其他任何事,服务端也不会接受其他客户端的请求。这种通信机制比较简单粗暴,但是效率不高。
在这里插入图片描述

同步非阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候虽然客户端会一直等待响应,但是服务端可以处理其他的请求,过一会回来处理原先的。这种方式很高效,一个服务端可以处理很多请求,不会在因为任务没有处理完而堵着,所以这是非阻塞的。
在这里插入图片描述

异步阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,但是客户端不会等待服务器响应,它可以做其他的任务,等服务器处理完毕后再把结果响应给客户端,客户端得到回调后再处理服务端的响应。这种方式可以避免客户端一直处于等待的状态,优化了用户体验,其实就是类似于网页里发起的ajax异步请求。

异步非阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候的任务虽然处理时间会很久,但是客户端可以做其他的任务,因为他是异步的,可以在回调函数里处理响应;同时服务端是非阻塞的,所以服务端可以去处理其他的任务,如此,这个模式就显得非常的高效了。
在这里插入图片描述

18.什么是BIO?

BIO同步并阻塞 ,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。
在这里插入图片描述

BIO方式 适用于连接数目比较小且固定的架构 ,这种方式对服务器资源要求比较高,并发局限于应用中JDK1.4以前的唯一选择,但程序直观简单易理解。

BIO阻塞在哪里?阻塞在操作系统的recv()函数。
在这里插入图片描述

19.什么是NIO?

NIO同步非阻塞 ,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多复用器轮询到连接有I/O请求时,才启动一个线程进行处理。

在这里插入图片描述
NIO方式 适用于连接数目多且连接比较短(轻操作)的架构 ,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。

NIO的优势又是什么呢?selector + channel。
在这里插入图片描述

20.什么是AIO?

AIO异步非阻塞 ,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。

AIO属于NIO包中的类实现,其实 IO主要分为BIO和NIO ,AIO只是附加品,解决IO不能异步的实现。在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作。

21.介绍下IO流的分类

image.png

  • 字节流继承InputStream和OutputStream
  • 字符流继承自InputSteamReader和OutputStreamWriter

字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。

在选择流类型时,需要考虑到处理的数据类型。如果处理的是文本数据,应选择字符流;如果处理的是二进制数据或非文本数据,应选择字节流。

22.IO使用了哪些设计模式

1、装饰器模式

在不改变原有对象的情况下拓展其功能,比如
在这里插入图片描述

2、适配器模式

协调接口不兼容,比如
在这里插入图片描述

3、工厂模式

newInputStream 创建InputStream对象(静态工厂)等

4、观察者模式

NIO中的文件目录监听服务

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值