java多线程面试点

线程和进程

进程
进程就是程序执行的一次过程。一般代码文件存在磁盘,用.exe文件结束。系统运行一个程序就是进程的创建,运行和消亡的过程。在java中我们执行main方法其实就是启动了一个jvm进程,而main所在的就是一个线程。称为主线程。
线程
线程称为轻量级的进程。一个进程可以包含多个线程。一个类的多线程共享堆和方法区的资源,但是却拥有自己独立的程序计数器,虚拟机栈,本地方法栈。
查看java有哪些线程运行

public static void main(String[] args) throws IOException {
        //获取java线程管理MxBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //获取线程和线程堆栈信息
        ThreadInfo[] infos = threadMXBean.dumpAllThreads(false,false);
        //遍历
        for(ThreadInfo info : infos){
            System.out.println("["+info.getThreadId()+"]"+info.getThreadName());
        }
//                [6]Monitor Ctrl-Break
//                [5]Attach Listener  添加事件的线程
//                [4]Signal Dispatcher 分发处理给JVM的信号线程
//                [3]Finalizer  调用对象finalizer方法的线程
//                [2]Reference Handler 去除Reference线程
//                [1]main 主线程
    }

线程和进程关系和差别,优缺点

关系 线程是进程的更小单位,一个进程可以拥有多个线程
差别 线程共享堆和方法区。但是每个线程有着自己独立的jvm栈,本地方法栈和程序计数器。
优缺点进程是相互独立的,相对安全,方便资源的管理和保护。而线程因为共享堆和方法区,所以相对上是不利于资源的管理和保护的。
在这里插入图片描述

为什么线程的程序计数器是私有的

程序计数器作用

  1. 节码解释器通过改变程序计数器来读取指令,从而实现代码流程的控制
  2. 当线程阻塞等情况时,程序计数器会记录线程执行到了哪里。当重新开始时会从记录点开始执行。
    //私有化的主要原因:记录线程执行到的位置
    虚拟机栈和本地方法栈
    虚拟机栈创建的栈帧用来存储局部变量,操作数栈,常量池等信息。一个方法的执行到结束就表示一个出栈和入栈的过程。
    本地方法站和虚拟机栈基本功能一致,差别是本地方法栈为虚拟机栈的Native方法服务。
    栈私有化原因
    为了保证线程的局部变量不被访问到。

线程共享的堆和方法区作用

堆是进程里面最大的一块内存,主要用来存放新建的对象。方法区用来存放已经被加载的类的信息,常量,静态变量和即时编译器编译后的代码等数据。

并发和并行的区别

并发:同一时间段内,多个任务都在执行。(其实就是多线程类似,任务都在执行却不代表同一时间任务都在执行,而是同一时间段)
并行:同一时间,多个任务同时进行。

多线程的必要性

  1. 多线程是轻量级的进程。线程之间的调换和切换成本远小于进程。多CPU计算机下,多线程可以减少线程上下文切换开销
  2. 多线程是高并发的基础。
  3. 多线程可以提高CPU的利用率
  4. 在多核CPU下,单线程只能调用一个CPU,但是多线程可以调用多个CPU同时运行。

多线程可能存在的问题

线程死锁,内存泄漏,上下文切换,受限于硬件和软件的资源闲置问题。

线程的各种状态

在这里插入图片描述

什么是上下文切换

总结:上下文切换就是一个线程(任务)从被保存到在加载的一个过程。
因为一个线程只能调用一个CPU,但是多线程的线程个数有时远远大于CPU个数。所以CPU会分给线程一个时间片,当一个线程的时间片到的时候线程就会加入就绪状态,直到下次继续运行。

死锁的产生和避免

产生
当两个线程同时想拥有对方的资源,但又锁住了自己的要被调用的资源,这样两个线程就会一直在就绪状态。(一个线程在就绪状态下还锁住这其他线程需要的资源)
在这里插入图片描述

扫描二维码关注公众号,回复: 11644508 查看本文章

避免

  1. 改变代码执行顺序,破坏循环等待条件
  2. 当一个线程进一步申请不到资源时主动释放占有的资源
  3. 一次性调用所有的资源

sleep()和wait()方法区别和共同点

区别

  1. 最大区别:sleep方法不会让线程释放占有的资源,而wait可以让线程释放资源。
  2. sleep方法在休眠事件过去后会自动苏醒,而wait方法不会,它需要其他线程使用notify或者notifyAll方法唤醒。
  3. sleep通常用于线程的休眠,而wait用于线程通信
    共同点
  4. 两者都可以让线程暂停执行

线程启动为什么用start()而不用run()

线程在start方法调用后处于就绪状态。线程启动时要先获得CPU的调度,获得调度后才可以运行起来。
如果直接调用线程的run方法,就相当于在main线程里面调用这个线程的run方法一样。

对sychrnozed关键字的看法

这个关键字用来给线程资源加锁,让被锁住的资源同时只能被一个线程调用。
在老版本的jdk中它属于重量级锁,但在之后的版本中对其进行了优化,用了很多重锁来减少锁的开销。

sychrnozed关键字三大关键用法

修饰方法
相当于对当前对象加锁。进入方法时需要获得当前对象
修饰静态方法
相当于对当前类的锁,而不是对象。所以当一个线程获取当前类的非静态synchrnized方法,而另外一个线程获取当前类的静态synchronized方法不会出现线程堵塞。因为一个锁对象,一个锁类。
修饰代码块
指定对象加锁。加入代码块时需要先锁定指定的对象。
注意:不要这样写:sychronized(String s);因为String类在常量池又缓存功能。

双重校验锁实现单利模式

必要性:
当多个线程同时调用单例获取方法时,如果单例使用的是懒汉式,就可能出现有线程获得的单例为空的现象。这时就要用到双重校验锁了。

public class Test2 {
//必须要写private和volatile
//volatile主要是防止指令重拍,导致test2不为空,但是又没有初始化。
    private volatile static Test2 test2;

    private Test2(){

    }

    public Test2 getInstance(){
        //使用双重检验
        if(test2==null){
            synchronized (Test2.class){
                if(test2==null){
                    test2 = new Test2();
                }
            }
        }
        return test2;
    }

}

synchronized的JVM原理实现

修饰代码块
使用的是monitorenter和monitorexit指令,一个指向同步代码块开始,一个指向结束为止。开始monitorenter指令试图获取锁,获取到后计数器从0变为1,当指向monitorexit指令,表示代码块同步结束,计数器变为0.
修饰方法
不是指令,而是使用一个标识。jvm通过表示判断这个是不是同步方法,从而实现同步调用。

synchronized和ReentrantLock

  1. 都是可重入锁,自己内部也可以在获取外部锁住的对象。
  2. 前者依赖JVM,后者依赖API
  3. 后者比前者多一些高级功能。

关键字Volatile,防止指令重排,原理

java内存模型

  1. 内存出现问题原因:老版本时是直接从内存读取数据,但现在的版本线程会把变量储存在本地内存中,也就是寄存器。所以当主存的数据改变时,线程可能任然从本地内存读取数据,这是就会造成结果有问题。
  2. 解决方法:将变量定义为volatile类型。告诉系统这是一个不稳定的变量。是要放在主内存的。其实就是防止指令重排。
    并发编程的三个重要点
  • 原子性:一个或者多个操作时,所有操作不能受到任何因素的影响。要么都执行,要么就都不执行。sychronized关键字就可以做到原子性。
  • 可见性:当一个变量在变化时,其他线程也可以看见。volatile就可以实现。
  • 有序性:就是volatile防止指令重排。

sychronized和volatile的区别

  • volatile也可以实现线程的同步,但是它是轻量级的同步,所以效率要高于sychronized。
  • volatile只能修饰属性,但是sychronized可以修饰方法和代码块。
  • volatile不会发生阻塞,sychronized可能会发生阻塞。
  • volatile保证代码的可见性,但是不保证原子性。但是synchronized都可以保证。

ThreadLocal

1.ThreadLocal简介:
让每个线程拥有自己独立的属性。

public class Test2 implements Runnable{

    //这就是线程独立属性的写法
    private static final ThreadLocal<SimpleDateFormat> threadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    @Override
    public void run() {
        try {
            //通过threadLocal.get()方式得到独立的属性
            //通过set()方式设置独立的属性

            //这里输出没改变之前的值
            System.out.println("ThreadName = " + Thread.currentThread().getName() +
                    "default format = " + threadLocal.get().toPattern());
            Thread.sleep(100);

            //这里测试改变后会不会影响其他线程的
            threadLocal.set(new SimpleDateFormat());
            System.out.println("ThreadName = " + Thread.currentThread().getName() +
                    "changed format = " + threadLocal.get().toPattern());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test2 test2 = new Test2();
        for(int i = 0;i < 10;i++){
            Thread t = new Thread(test2,""+i);
            Thread.sleep(100);
            t.start();
        }
    }

}

//输出结果(可见这个线程改变值没有影响其他线程的值)
ThreadName = 0 default format = yyyyMMdd HHmm
ThreadName = 1 default format = yyyyMMdd HHmm
ThreadName = 0 changed format = yy-M-d ah:mm
ThreadName = 1 changed format = yy-M-d ah:mm
ThreadName = 2 default format = yyyyMMdd HHmm
ThreadName = 2 changed format = yy-M-d ah:mm
ThreadName = 3 default format = yyyyMMdd HHmm
ThreadName = 3 changed format = yy-M-d ah:mm
ThreadName = 4 default format = yyyyMMdd HHmm
ThreadName = 4 changed format = yy-M-d ah:mm
ThreadName = 5 default format = yyyyMMdd HHmm
ThreadName = 5 changed format = yy-M-d ah:mm
ThreadName = 6 default format = yyyyMMdd HHmm
ThreadName = 6 changed format = yy-M-d ah:mm
ThreadName = 7 default format = yyyyMMdd HHmm
ThreadName = 7 changed format = yy-M-d ah:mm
ThreadName = 8 default format = yyyyMMdd HHmm
ThreadName = 8 changed format = yy-M-d ah:mm
ThreadName = 9 default format = yyyyMMdd HHmm
ThreadName = 9 changed format = yy-M-d ah:mm

ThreadLocal的原理和泄露问题

原理
将线程要独立的对象存储在ThreaLocalMap里面
泄露问题
ThreadLocalMap的key是弱引用,而value是强引用。所以ThreadLocal如果没有被强引用,垃圾回收可能会将key去除。导致value泄露问题。

线程池

为什么要用线程池

  1. 降低资源消耗
  2. 提高响应速度
  3. 提高线程的管理性能

Runnable和Callable的区别

  • 前者不会返回结果或者抛出异常,后者可以。

线程池的创建
//不允许使用Executors去创建线程池,要用ThreadPoolExecutors去创建。

import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

//线程池类
public class Test2{
    //线程池的一些常数的定义
    private static final int CORE_POOL_SIZE = 5;//最小可以同时运行的线程数量
    private static final int MAX_POOL_SIZE = 10;//最大线程树
    private static final int QUEUE_CAPACITY = 100;//阻塞队列大小,当线程多时会被放在这个队列
    //当线程池的线程数量大于corepoolsize的时候,又没有任务结束,这时会等待keepalivetime
    //当过了这个时间,核心线程外的线程会被销毁
    private static final Long KEEP_ALIVE_TIME = 1L;
    //开始创建线程池,并运行线程
    public static void main(String[] args) {
        //用阿里巴巴的方式创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        //线程池的使用
        for(int i = 0;i < 10;i++){
            Runnable thread = new TestThreadPool(""+i);
            //线程加入线程池并执行
            executor.execute(thread);
        }
        //线程池的终止
        executor.shutdown();
        while(!executor.isTerminated()){
        }
        System.out.println("Over!");
    }
}

class TestThreadPool implements Runnable{
    //用来记录当前线程的名字
    private String name;
    public TestThreadPool(String name){
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println("start:"+this.name+"->"+new Date());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end:"+this.name+"->"+new Date());
    }
}

//运行结果(由结果可以知道,每次只有5个线程同时执行)
start:3->Wed Sep 09 15:40:48 CST 2020
start:2->Wed Sep 09 15:40:48 CST 2020
start:4->Wed Sep 09 15:40:48 CST 2020
start:1->Wed Sep 09 15:40:48 CST 2020
start:0->Wed Sep 09 15:40:48 CST 2020
end:3->Wed Sep 09 15:40:53 CST 2020
start:5->Wed Sep 09 15:40:53 CST 2020
end:2->Wed Sep 09 15:40:53 CST 2020
start:6->Wed Sep 09 15:40:53 CST 2020
end:4->Wed Sep 09 15:40:53 CST 2020
start:7->Wed Sep 09 15:40:53 CST 2020
end:1->Wed Sep 09 15:40:53 CST 2020
start:8->Wed Sep 09 15:40:53 CST 2020
end:0->Wed Sep 09 15:40:53 CST 2020
start:9->Wed Sep 09 15:40:53 CST 2020
end:5->Wed Sep 09 15:40:58 CST 2020
end:6->Wed Sep 09 15:40:58 CST 2020
end:7->Wed Sep 09 15:40:58 CST 2020
end:9->Wed Sep 09 15:40:58 CST 2020
end:8->Wed Sep 09 15:40:58 CST 2020
Over!

线程池里面几个重要的参数
上面的代码里面有了

多线程的Atomic原子类

原子性:一个Atomic操作一旦执行是不可以中断的。是不会被其它线程所干扰的。
作用:sychronized的消耗太高,而Atomic的原子类使用了CAS,volatile和native等实现了原子化。提高了执行效率,保证了线程的安全和同步。
JUC包中的原子类

  1. 基本原子数据类型:
    AtomicInteger:整形原子类
    AtomicLong:长整形原子类
    AtomicBoolean:布尔原子类

  2. 原子引用类型
    AtomicReference:引用类型原子类
    AtomicStampReference:原子更新带有版本号的原子类
    AtomicMarkableReference:原子跟新带有标志位的类

  3. 对象的属性修改类型
    AtomicIntegerFieldUpdater:原子更新整形字段更新器
    AtomicLongFieldUpdaer:原子更新长整形字段的更新器

AtomicInteger的使用

  1. 常用方法:
    public final int get();//获取当前的值
    public final int getAndSet(int newValue);//获取并更改为另外一个值
    public final int getAndIncrement();//获取当前的值并自增
    public final int getAndDecrement();//获取当前的值并自减
    public final int getAndAdd(int value);//获取当前的值+value的值
    boolean compareAndSet(int except,int value);//如果当前值等于except,则将值更改为value
    public final final void lazySet(int newValue);//最总更改为newValue,但是有延迟,导致其他线程可能获取原来的值

使用例子

package com.how2java.tmallweb_springboot.test;

import java.util.concurrent.atomic.AtomicInteger;

//线程池类
public class Test2{

    private AtomicInteger integer = new AtomicInteger();

    //加1
    public void increment(){
        integer.incrementAndGet();
    }

    //自减
    public void decreament(){
        integer.decrementAndGet();
    }

    //增加数值
    public void add(int value){
        integer.getAndAdd(value);
    }

    //重新赋值
    public void set(int value){
        integer.getAndSet(value);
    }

    //得到当前值
    public int get(){
        return integer.get();
    }

    public static void main(String[] args) {

        Test2 test2 = new Test2();
        System.out.println(test2.get());
        test2.add(10);
        System.out.println(test2.get());
        test2.set(50);
        System.out.println(test2.get());
        test2.increment();
        System.out.println(test2.get());
        /*
        * 0
          10
          50
          51
        * */

    }

}

猜你喜欢

转载自blog.csdn.net/qq_44771337/article/details/108468716