1、Java中又哪几种方式来创建线程

1、集成Thread类,重写run方法,new对象调用start方法启动

public class ThreadDemo extends Thread {

    @Override
    public void run() {
        System.out.println("线程:" + Thread.currentThread().getName() + "执行了");

    }
                                                               
    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo();
        t1.start();
    }
}

注意:重写run方法,启动时调用的是start方法

缺点:占用了继承名额

2、实现Runnable接口,实现run方法,new Thread对象,将实现类对象作为参数传入Thread对象中,然后通过Thread对象的start方法启动

public class ThreadDemo2 implements Runnable {

    @Override
    public void run() {
        System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new ThreadDemo2());
        t1.start();
    }
}

依然要用到Thread类,Thread类其实也实现了Runnable接口,本质上与第一种相似

可以使用匿名内部类 或者 Lambda的方式去实现,会更加方便

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
    }
});
t2.start();

Thread t3 = new Thread(() -> System.out.println("线程:" + Thread.currentThread().getName() + "执行了"));
t3.start();

3、实现Callable,重写call方法,使用Thread + FutureTask才能启动

public class ThreadDemo3 implements Callable<String> {

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

        FutureTask<String> futureTask = new FutureTask<>(new ThreadDemo3());
        Thread t1 = new Thread(futureTask);
        t1.start();
        String result = futureTask.get();
        System.out.println("线程的执行结果:" + result);
    }

    @Override
    public String call() throws Exception {
        System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
        return "HelloWorld";
    }
}

好处:能拿到线程的执行结果

还是要用到Thread对象来启动,所以本质上和1、2一样

4、线程池,调用execute方法执行线程任务,需要将Runnable的实现类对象作为线程任务传入进去

public class ThreadDemo4 implements Runnable {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 20; i++) {
            executorService.execute(new ThreadDemo4());
        }
        //程序不会自动结束,需要调用shutdown方法结束程序
        executorService.shutdown();
    }

    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:" + Thread.currentThread().getName() + "执行了");
    }
}

总结,其实底层都是实现Runnable接口

2、为什么不建议使用Executors来创建线程池

Executors.newFixedThreadPool(10) 源码:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

发现创建的队列为 LinkedBlockingQueue ,是一个无界阻塞队列,如果使用该线程池执行任务,如果任务过多就会不断的添加到队列中,任务越多占用的内存就会愈多,最终可能耗尽内存,导致OOM。

Executors.newSingleThreadExecutor() 源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

同上,也是LinkedBlockingQueue,所以同样可能会耗尽内存。

总结:

除开又可能造成OOM之外,使用Executors创建线程池也不能自定义线程名称,不利于排查问题,所以建议直接使用ThreadPoolExecutor来定义线程池,这样可以灵活控制

3、线程池有哪几种状态?每种状态分别表示什么?

  • RUNNING

    表示线程池正常运行,既能接受新任务,也会正常处理队列中的任务

  • SHUTDOWN

    当调用线程池的 shutdown() 方法时,线程池旧会进入SHUTDOWN状态,表示线程池处于正在关闭状态,此状态下线程池不会接受新任务,但是会继续把队列中的任务处理完

  • STOP

    当调用线程池的 shutdownnow() 方法时,线程池就进入STOP状态,表示线程池处于正在停止状态,此状态下线程池既不会接受新任务,也不会处理队列中的任务,并且正在运行的线程也会被中断

  • TIDYING tidying

    线程池中没有线程正在运行后,线程池的状态就会自动变为TIDYING,并且会调用 terminated() 方法,该方法是空方法,留给程序员进行扩展。

  • TERMINATED

    terminated() 方法执行完之后,线程池状态就会变为 TERMINATED状态

4、Sychronized 和 ReentrantLock有哪些不同点?

SychronizedReentrantLock
Java中的一个关键字JDK提供的一个类
自动加锁与释放锁需要手动加锁与释放锁
JVM层面的锁API层面的锁
非公平锁手动指定公平锁或非公平锁
锁的是对象,锁信息保存在对象头中int类型的state属性来标识锁的状态
底层有锁升级过程(偏向锁->轻量级锁->重量级锁)没有锁升级过程trylock轻量级(自旋) 、lock重量级锁

5、ThreadLocal有哪些应用场景?底层是如果实现的?

  1. ThreadLocal 是 Java 中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。

  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Nao的key为ThreadLocal对象,Map的value为需要缓存的值。

    image-20231123212206959

  3. 如果在线程池中使用 ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象回收,但线程池中的线程不会被回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏(给一份数据分配了内存,这份数据没有用,但是又被强引用,无法销毁),解决办法是,在使用了ThreadLocal对象后,手动调用ThreadLocal的remove方法,手动清除Entry对象。

    image-20231123212738996

  4. ThreadLocal经典的应用场景就是链接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

6、ReentrantLock分为公平锁和非公平锁,那底层分别是如何实现的?

首先不管是公平锁还是非公平锁,底层实现都会使用AQS来进行排队,区别在于线程在使用lock()方法加锁时:

  1. 如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队。
  2. 如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁,竞争失败才会排队。

公平锁的底层执行流程:

ReentrantLock公平锁加锁.png

非公平锁的底层执行流程:

ReentrantLock非公平锁加锁.png

另外,不管是公平锁还是非公平锁,一旦没有竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

7、Sychronized的锁升级过程是怎么样的?

  1. 偏向锁:在锁对象的对象头中记录一下当前获取到这把锁的线程ID,该线程下次如果又来获取这把锁就可以直接获取到了,也就是支持锁重入
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
  3. 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞。
  4. 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就不会有唤醒线程的操作,阻塞和唤醒这两个操作都是需要操作系统去完成的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。

todo:重点深挖

8、Tomcat中为什么要使用自定义类加载器?

一个Tomcat中会部署多个应用,而每个应用中都存在很多类,并且各个应用中的类都是独立的,全类名可能会相同,比如一个订单系统中可能存在 com.zyf.User类,一个库存系统中也可能存在com.zyf.User类,一个Tomcat不管内部部署了多少个应用,Tomcat启动之后就是一个Java进程,也就是一个JVM,所以如果Tomcat中只存在一个类加载器,比如默认的 AppClassLoader ,那么就只能加载一个com.zyf.User类,这是由问题的,而在Tomcat中,会为部署的每个应用都生成一个类加载器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的类隔离,不出现冲突。另外Tomcat还利用自定义加载器实现了热加载功能

image.png

9、JDK、JRE、JVM之间的区别

  • JDK(Java SE Development Kit),Java标准开发包,它提供了编译运行Java程序所需的各种工具和资源,包括Java编译器Java运行时环境,以及常用的Java类库
  • JRE( Java Runtime Environment) ,Java运行环境,用于运行Java的字节码文件。JRE中包括了JVM以及JVM工作所需要的类库,普通用户而只需要安装JRE来运行Java程序,而程序开发者必须安装JDK来编译、调试程序。
  • JVM(Java Virtual Machine),Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件。

我们写Java代码,用txt就可以写,但是写出来的Java代码,想要运行,需要先编译成字节码,那就需要编译器,而JDK中就包含了编译器javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是JVM(Java虚拟机),专门用来执行Java字节码的。

如果我们要开发Java程序,那就需要JDK,因为要编译Java源文件。
如果我们只想运行已经编译好的Java字节码文件,也就是*.class文件,那么就只需要JRE。
JDK中包含了JRE,JRE中包含了JVM。
另外,JVM在执行Java字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是有可能不一样的,所以就导致不同操作系统上的JVM是不一样的,所以我们在安装JDK时需要选择操作系统。
另外,JVM是用来执行Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运行,比如Apache Groovy, Scala and Kotlin 等等。

10、hashCode() 与 equals() 之间的关系

在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常来说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashCode来做一些提前的判断,比如:

  • 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
  • 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象
  • 如果两个对象相等,那么他们的hashCode就一定相同

在Java的一些集合类的实现中,在比较两个对象是否相等时,会根据上面的原则,会先调用对象的hashCode()方法得到hashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同,如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals方法的实现会比较重,逻辑比较多,而hashCode()主要就是得到一个哈希值,实际上就一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode想比较一下。

所以我们就需要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法,一定要保证能遵守上述规则。

HashMap get方法源码:

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

可以看到是先比较hash值相等再判断equals是否相等

11、String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高

12、泛型中extends和super的区别

  1. 表示包括T在内的任何T的子类
  2. 表示包括T在内的任何T的父类

注意:泛型只是编译期的限制