当前位置:   article > 正文

2022年java知识点汇总,面试大全!超级全面,逐步完善!_java基础知识总结 超详细

java基础知识总结 超详细

文章目录

一、java基础知识

1.1 String类相关

  1. String、StringBuilder和StringBuffer

​ String是不可变的,后两者可变,StringBuffer线程安全,StringBuilder线程不安全,但是StringBuilder效率高;

​ 另外,StringBuffer和Stringbuilder都没有重写equals方法,在进行equals比较时,还是比较的是地址,而不是值

public static void main(String[] args) {
        String a="abc";
        String b="abc";
        System.out.println(a==b);
        System.out.println(a.equals(b));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

​ 结果都是:true true ,因为都在常量池中,比较的是值,地址也相同;

 public static void test1(){
        String a="abc";
        String b=new String("abc");
        System.out.println(a==b);
        System.out.println(a.equals(b));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

​ 此时结果是:false true,==比较的是地址,equals比较的是值;

  String t0 = new String("hello") + new String("world");
        t0.intern();
        String t1 = "helloworld";
        System.out.println(t0 == t1);
        
这个运行结果,在JDK1.8中是true,1.7中是false,JDK1.7之前的版本中,intern方法会优先在方法区的运行时常量池中查找是否已经存在相同的字符串,倘若已经存在,则返回已存在的字符串,否则则在常量池中添加一个字符串常量,并返回字符串。从JDK1.7开始,HotSpot虚拟机将字符串常量移至Java Heap,intern方法的实现也发生了变化,首先还是会先去查询常量池中是否已经存在,如果存在,则返回常量池中的字符串,否则不再将字符串拷贝到常量池,而只是在常量池中保存字符串对象的引用。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. String几个经典方法作用解析
equals(Object obj): 比较字符串的内容是否相同,区分大小写;
equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
contains(String str):判断大字符串中是否包含小字符串
startsWith(String str):判断字符串是否以某个指定的字符串开头
endsWidth(String str):判断字符串是否以某个指定的字符串结尾
isEmpty():判断字符串是否为空
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. 为什么String用final 修饰
为了安全,多个线程对String进行读取时,不会发生线程安全问题,因为final是不可变性;
还能保证Arrays数组的安全;
实现字符串常量池,可提高效率,实现对内存的优化;
标记为final。不可被继承,保证String类型的对象只能是String类型,不会是其他子类型;
String 对象是缓存在String池中,由于缓存的字符串是在多个客户端之间共享,因此存在风险,因此通过String类不可变来规避这种风险

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  1. String类常用方法
char charAt(int index);//返回指定索引处的char值

int compareTo(Object o);//把这个字符串跟另一个字符串比较

String concat(String str);//连接两个字符串

boolean contentEquals(StringBuffer sb);//当且仅当字符串与指定的StringBuffer有相同顺序的字符时候返回真

String substring(int beginIndex);
String substring(int beginIndex,int endIndex); //截取字符串

int indexOf(String str, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
例:获取指定字符串中某个子串出现的次数
public static int strNum(String obj,String str){
        int fromIndex = 0;
        int count =0;
        while (true){
            int index = str.indexOf(obj,fromIndex);
            if(-1 != index){
                fromIndex = index +1;
                count ++;
            }else {
                break;
            }
        }
        return count;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  1. String类可以有多长
   临时变量一般是存在java堆中的,String类的长度理论上取决于传入的byte数组长度,byte[]数据数组最大长度理论上应该是Integer.MAX_VALUE,但是实际从ArrayList源码可以看出,应该是Integer.MAX_VALUE-8,但是还是会受到java堆可分配内存大小的限制
   如果String变量是一个全局变量,其变量是存在java方法区的,此时他的长度取决于.class描述全局String类型变量的数据结构,如果是u2(描述是2个字节的数据类型),这意味着最大是65535。
   如果是字符数,一个UTF-8编码的字符对应3个字节,所以此时可容纳的字符数应该是65535/3;
   

  • 1
  • 2
  • 3
  • 4
  • 5
  1. HashCode、equals、==区别?

    equals() 定义在JDK的Object中,通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等,默认的equals方法等价于“==”,所以一般要重新equals方法,若两个对象的内容相等,则返回true,否则返回false

    在String中,“==”用来比较两个变量的内存地址是否相同,而equals用来比较内容是否相同(因为String重写了equals方法)

        String a="abc";
        String b="abc";
        String c =new String("abc");
        System.out.println(a==b); // true
        System.out.println(a.equals(b));//true
        System.out.println(a==c);//false
        System.out.println(a.equals(c)); //true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

HashCodes()是获取哈希码,确定该对象在哈希表中的位置,仅当创建了某个类的散列表,该类的hashCode()才有用,

在散列表中,若两个元素相等,那么他们的散列码一定相同,但是反过来,若两个对象相等,他们的hashCode()并不一定相等;

1. 2 多线程

1.2.1 实现多线程的方式

​ 主要有5种方式,继承Thread类,实现Runnable接口,实现Callable接口通过FutureTask包装器来创建Thread线程,使用线程池 接口ExecutorService结合Callable、future实现有返回结果的多线程,前两种没有返回值,后两种有返回值,还可以使用Spring的@Async注解非常方便的实现多线程。

  1. 继承Thread类重写run方法创建线程

    Thread 类本质上是实现了Runnable接口的一个实例,启动线程是通过Thread类的start()方法,start方法是一个native方法,将 会启动一个新线程,并执行run()方法;

public class Mythread extends Thread {

    @Override
    public void run() {
        System.out.println(Mythread.currentThread().getName());
    }

    public static void main(String[] args) {
        Mythread mythread = new Mythread();
        mythread.start();

        Mythread mythread1 = new Mythread();
        mythread1.start();
    }
}
结果:
Thread-0
Thread-1
Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  1. 实现Runnable接口
    如果自己的类已经继承了另外一个类,则无法再继承,此时需要实现接口
public class RunnableTest  extends ParentDemo implements Runnable{

    @Override
    public void testDemo() {
        System.out.println("123");
    }

    @Override
    public void run() {
        System.out.println("这是Runnable执行结果");
    }

    public static void main(String[] args) {
        RunnableTest runnableTest=new RunnableTest();

        Thread thread=new Thread(runnableTest);
        thread.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

​ 当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run();

​ 继承Thread类存在局限性,因为java是单继承多实现,所以如果继承Thread类就不能在继承其它类, 其次,Thread类更适合开启多个线程完成多个任务的场景,而实现Runnable接口更适合一个任务多个线程去完成的场景。

  1. 实现Callable接口通过FutureTask包装器来创建Thread线程
public class CallAbleTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10000; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        CallAbleTest callAbleTest = new CallAbleTest();
        FutureTask<Integer> result = new FutureTask<>(callAbleTest);

        new Thread(result).start();
        new Thread(result).start();

        try {
            Integer sum = result.get();
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

​ Callable 对象实际属于Executor框架的功能类;
​ Callable 可以在任务结束的时候提供一个返回值,Runnable没有;
​ Callable 中的call方法可以抛出异常,Runnable不能;
​ 运行Callable可以拿到一个future对象,future对象表示异步计算的结果,可提供检查运算是否结束的方法,由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,在这种情况之下,就可以使用future来监视目标线程调用call方法的情况,当调用future的get方法以获取结果时,当前线程就会被阻塞,直到call方法结束返回结果

  1. 使用线程池接口ExecutorService结合Callable、Future实现有返回结果的线程
public class ExecutorTest {

    static class MyCallable implements Callable<Object> {
        private String taskNum;

        MyCallable(String taskNum) {
            this.taskNum = taskNum;
        }

        @Override
        public Object call() throws Exception {
            System.out.println(">>>>" + taskNum + "开启任务");
            Thread.sleep(1000);
            System.out.println(">>>>" + taskNum + "任务结束");
            return taskNum;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("开始执行-------");
        //创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //创建多个有返回值的任务
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < 5; i++) {
            Callable c = new MyCallable(i + " ");

            //执行任务并获取Future对象
            Future f = pool.submit(c);
            list.add(f);
        }
        //关闭线程池
        pool.shutdown();

        for (Future f : list) {
            System.out.println(">>>" + f.get().toString());
        }
        System.out.println("程序运行结束--------");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

​ Executors类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口,ExecutorService中提供submit()方法,传递一个Callable,或者Runnable,返回Future,如果Executor没有完成Callable的计算,调用返回Future对象的get()方法将阻塞,直到计算完成。

  1. Spring的@Async注解实现多线程

​ 首选需要配置线程参数

#核心线程数,当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程,设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时退出
spring.task.pool.corePoolSize=20
#最大线程数,当线程数大于等于corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务
spring.task.pool.maxPoolSize=60
#线程空闲时间,当线程空闲时间达到keepAliveSeconds(秒)时,线程会退出,直到线程数量等于corePoolSize,如果allowCoreThreadTimeout=true,则会直到线程数量等于0
spring.task.pool.keepAliveSeconds=1
#任务队列容量,当核心线程数达到最大时,新任务会放在队列中排队等待执行
spring.task.pool.queueCapacity=400

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

​ 创建线程池配置类

@Configuration
@EnableAsync
public class AsyncTaskConfig {

    @Autowired
    private ThreadPoolCOnfig threadPoolCOnfig;

    public ThreadPoolTaskExecutor myThreadPool(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        //设置参数
        executor.setCorePoolSize(threadPoolCOnfig.getCorePoolSize());
        executor.setMaxPoolSize(threadPoolCOnfig.getMaxPoolSize());
        executor.setQueueCapacity(threadPoolCOnfig.getQueueCapacity());
        executor.setKeepAliveSeconds(threadPoolCOnfig.getKeepAliveSeconds());
        executor.setAllowCoreThreadTimeOut(true);
        executor.setThreadNamePrefix("MyThreadPool");

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

​ 目前String已经实现5种线程池

1. SimpleAsyncTaskExecutor :不是真的线程池,这个类不重用线程,默 认每次调用都会创建一个新的线程。
2. SyncTaskExecutor: 这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
3. ConcurrentTaskExecutor: Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个    类。
4. SimpleThreadPoolTaskExecutor: 是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此    类。
5. ThreadPoolTaskExecutor : 最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

@Async正常使用默认线程池,为SimpleAsyncTaskExecutor,一般有两种调用,一种是无返回值的调用,一种是有返回值的Future调用。

基于@Async无返回值调用,直接作用在类,使用方法上,加上@Async注解;
有返回值的Future调用,返回类型设置为Future<Object>,需要我们在方法中捕获异常并处理,或者在调用方调用Future.get时捕获异常进行处理

有返回值CompletableFuture调用,在jdk1.8中提供了强大的Future拓展功能,可以简化异步编程的复杂性,并且提供函数式编程能力

private static final ThreadPoolExecutor SELECT_POOL_EXECUTOR = new ThreadPoolExecutor(10, 20, 5000,
            TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat("selectThreadPoolExecutor-%d").build());
         // tradeMapper.countTradeLog(tradeSearchBean)方法表示,获取数量,返回值为int
         // 获取总条数
         CompletableFuture<Integer> countFuture = CompletableFuture.supplyAsync(() -> tradeMapper.countTradeLog(tradeSearchBean), SELECT_POOL_EXECUTOR);
         // 同步阻塞
         CompletableFuture.allOf(countFuture).join();

         // 获取结果
         int count = countFuture.get();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

​ 默认线程池存在比较多的弊端,一般不用Executors去创建,推荐使用ThreadPoolExecutor方式,Executors方法各个弊端如下

newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

@Async 默认是使用SimpleAsyncTaskExecutor,该线程池默认是来一个任务创建一个线程,如果系统不断创建线程,最终会导致占用内存过高,引发OutOfMemoryError错误,但是它本身也存在一个限流机制,ConcurrencyLimit属性,其大于等于0,开启限流机制,等于-1,关闭限流

@Async 存在自定义线程池,方法如下
实现接口AsyncConfigurer
继承AsyncConfigurerSupport
配置自定义的TaskExecutor配置参数代替内置的任务执行器
不管是继承还是重新实现接口,都需要指定一个线程池,并且重新实现getAsyncExecutor()方法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1.2.2 线程状态结构图

2020126155605111.jpg

初始状态(new):new一个线程实例,线程进入初始状态;

就绪状态(ready):

  • 调用线程的start()方法,线程进入就绪状态,当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态;
  • 当前线程时间片段用完了,调用当前线程的yield()方法,当前线程进入就绪状态;
  • 锁池里的线程拿到对象锁后,进入就绪状态;

运行中状态(Running):线程调用run方法,进入运行态;

阻塞状态(Blocked): 线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态;

等待(waiting): 此状态的线程不会被CPU分配执行时间,要等待被显示的唤醒,否则将无限等待;

超时等待(TIMED_WAITING): 和等待状态类似,但是此状态不需要无限期等待,当达到一定时间,会被自动唤醒;

终止状态(TERMINATED): 当线程run()方法完成,或者主线程main()方法完成,我们就可以认为是终止的,一旦终止,不能复生,此时调用start()方法会抛出异常;

1.2.3 线程常用方法

start(): 用于开启多线程,使线程处于就绪态,当CPU分配时间片后可 执行run()方法,并且start()方法是一个synchronized修饰的方法,而run只是一个普通方法,start()可实现并发

run(): 是一个普通方法,当线程调用了start()方法,一旦获取CPU调度,处于运行态,线程就会调用这个run();

wait(): 线程等待,让线程进入阻塞状态,2种方式,一种无限等待,需要使用notify()方法唤醒,另外一种,设置等待时间,时间一到自动唤醒wait(long time), wait()方法属于Object方法,释放CPU执行权,同时释放锁,同时wait()方法只能在同步代码方法中或者同步代码块中使用,使用时需要捕获InterruptedException异常

sleep(): 强迫一个线程睡眠N毫秒,释放CPU资源,不释放锁,使用地点也不需要限制,但是需要捕获异常,而且sleep()是Thread类的方法

isAlive(): 判断一个线程是否存活

currentThread(): 获取当前线程

yield(): 线程礼让,暂停当前正在执行的线程对象,虽然让出CPU时间,但是不会让线程阻塞,线程任然处于可执行状态,随时可再分的CPU执行时间

getPriority()与setPriority(): 设置获取线程优先级

getId():

join(): 线程自闭,等待其他线程终止,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪。

package com.example.day1.one;

public class Mythread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Mythread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Mythread mythread = new Mythread();
        Mythread mythread1 = new Mythread();
        Mythread mythread2 = new Mythread();

        mythread.start();
        //mythread.join()
        
        mythread1.start();
        //mythread1.join()
        
        mythread2.start();
        //mythread2.join()

    }
}

结果:如果不加join(),在第一个线程还没结束的时候,第二个或者第三个就可能开始执行,如果加上join(),一定是第一个执行完,第二个执行,然后第三个执行
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

interrupted(): 判断是否被中断,并清除当前中断状态;

interrupt(): 中断线程,设置中断标志位;

isInterrupted(): 判断是否被中断;

isDaemon()与setDaemon(boolean on): 设置一个线程为守护线程(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)

stop():

notify(): 唤醒一个线程继续运行

notifyAll(): 唤醒所有线程进入争抢锁状态

1.2.4 线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有:协同式线程调度和抢占式线程调度;

协同式线程调度:线程的执行时间由线程本身控制,线程把自己工作执行完成之后,要主动通知系统切换到另外一个线程上

抢占式线程调度: 每个线程都将由系统来分配时间,线程的切换不由线程本身决定

1.2.5 线程池原理

线程池作用:
降低系统资源消耗,通过重用已存在的线程,降低线程创建与销毁造成的消耗;
提高系统响应速度,有任务到达,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
方便线程并发数管理;

  • 1
  • 2
  • 3
  • 4
  • 5

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHAh7hpE-1618197750701)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210301204725267.png)]

​ submit(task): 可用来提交Callable或Runnable任务,并返回代表此任务的Future对象

​ shutdown(): 在完成已提交的任务后封闭办事,不在接管新任务

​ shutdownNow(): 停止所有正在履行的任务并封闭办事;

​ isTerminated(): 测试是否所有任务都履行完毕了;

​ ThreadPoolExecutor参数解析

Ctl: 对线程池的运行状态和线程池中有效的数量进行控制的一个字段,包含2部分信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount)

五种状态:Runing:可接受新任务,并且处理已经添加的任务,线程池的初始化状态就是running
shutdown: 不接受新任务,但是可以处理已经添加的新任务,调用shutdown()方法,由running变成shutdown
stop: 不接受新任务,不处理已添加的任务,并且还会中断正在处理的任务;
tidying: 当所有任务终止,ctl记录的任务数为0,线程池就会变成tidying状态,此时会执行钩子函数terminated();
terminated: 当线程池处于tidying状态并且执行了terminated()方法,就会变成此状态;

参数解析
corePoolSize:线程池核心线程数,如果当线程池中的线程数为corePoolSize,继续提交的任务将被保存到"阻塞队列中",等待被执行,如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程
maximumPoolSize:线程池中允许的最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程任务,前提是当前线程数少于maximumPoolSize;
keepAliveTime: 线程池维护线程所允许的空闲时间,当线程池中的线程数大于corePoolSize的时候,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
unit: keepAliveTime的单位
workQueue: 用来保存等待被执行的任务的阻塞队列,任务必须实现Runnable接口,有如下几种阻塞队列
   1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按   FIFO排序任务;
   2. LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务
   3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态;
   4. priorityBlockingQuene:具有优先级的无界阻塞队列;
   5. DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列
   6. LinkedTransferQueue:由链表构成的无界阻塞队列;
   7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列,队列头部和尾部都可以添加和移除元素,多线程并发时,可将锁的竞争降到一半;

threadFactory :ThreadFactory,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
Handler:线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
  1、AbortPolicy:直接抛出异常,默认策略;
  2、CallerRunsPolicy:用调用者所在的线程来执行任务;
  3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义策略。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

​ **注意:**通过execute或者submit向线程池提交任务。任务提交的时候先提交给核心线程(corePoolSize);

​ 如果核心线程满了,就将任务放到workQueue里面去排队等待;如果队列也满了(取决用的什么队列,以及

​ 设置的大小),就会将新进来的任务提交给非核心线程,非核心线程数量等于maximumPoolSize -

​ corePoolSize,非核心线程使用之后会被回收。如果非核心线程也满了,那么就执行相应的拒绝策略

​ RejectedExecutionHandler。

​ 原理解析

1. execute方法解析:先获取当前工作线程数,-->添加任务到核心线程上去执行-->核心线程数满了,线程池状态是running,则添加到任务队列中去-->添加任务之后,保证线程池状态还是running,否则移除刚才添加的任务,并知心拒绝策略;

2. 关键在于AddWork方法,先判断状态是否是running,shutdown时队列是否有数据,判断工作线程是否大于容量;

3. worker内部类,继承AbstractQueuedSynchronizer并且实现了Runnable接口


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

1.2.6 几种常见线程池

​ newFixedThreadPool (固定数目线程的线程池)

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

​ 核心线程数和最大线程数大小一样;

​ 没有所谓的非空闲时间,keepAliveTime为0;

​ 阻塞队列为无界阻塞队列LinkedBlockingQueue;

特点

​ 如果线程数少于核心线程数,创建核心线程执行任务;

​ 等于核心线程数,把任务添加到阻塞队列;

​ 任务执行完毕去阻塞队列获取任务继续执行;

ExecutorService executor = Executors.newFixedThreadPool(10);
                    for (int i = 0; i < Integer.MAX_VALUE; i++) {
                        executor.execute(()->{
                            try {
                                Thread.sleep(10000);
                            } catch (InterruptedException e) {
                                //do nothing
                            }
            });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

​ 因为是无界阻塞队列,最大可容纳INTEGER.MAX个,可能会导致OOM,适用于处理CPU密集型的任务,适用

​ 于执行长期的任务

​ newCachedThreadPool(可缓存线程的线程池)

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

特点

​ 核心线程数为0;

​ 最大线程数为Integer.MAX_VAULE,阻塞队列是SynchronousQuene;

​ 非核心线程空闲存活时间为60秒;

ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName()+"正在执行");
            });
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

​ 用于执行大量短期的小任务;

​ newSingleThreadExecutor(单线程的线程池)

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

特点:

​ 核心线程数和最大线程数都是1;

​ 阻塞队列是LinkedBlockingQueue;

​ keepAliveTime 为0

​ 适用于串行执行任务的场景,一个任务一个任务的执行

​ newScheduledThreadPool(定时及周期执行的线程池)

 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  • 1
  • 2
  • 3
  • 4

特点:

​ 最大线程数为Integer.MAX_VALUE

​ 阻塞队列是DelayedWorkQueue

​ keepAliveTime为0

​ scheduleAtFixedRate() :按某种速率周期执行

​ scheduleWithFixedDelay():在某个延迟后执行

技能提升

为什么不用Executors创建线程池?

​ 因为LinkedBlockingQueue.offer方法,如果使用LinkedBlockingQueue队列而不限制大小的话,就是一个无

​ 边界的队列,会导致内存溢出问题,一般采用构造函数ThreadPoolExecutor来创建,并指定容量,另外还可

​ 以使用开源库,apache的guava,可以提供ThreadFactoryBuilder来创建线程池

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

1.2.7 线程池参数设计规则

​ 首先需要理解一些概念:

​ CPU密集型,也叫计算密集型,指的是系统的硬盘,内存性能相对CPU要好很多,CPU读/写I/O(硬盘/内存),

​ IO在很短的时间就可以完成,CPU占用率很高。

​ IO密集型,指系统的CPU性能相对硬盘,内存要好很多,此时,系统大部分状况是CPU在等IO(硬盘/内存)读写

​ 操作,CPU利用并不高

​ 一般情况下,一个服务器只部署一个应用并且只有一个线程池,

​ 针对CPU密集型:线程数=n+1,n为CPU核数

​ IO密集型:线程数=2n+1

​ 通常应该考虑线程的执行时间,可以这么设置

​ 最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数;

​ 另外,也可根据情况动态调整;

技术文章:美团线程池实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

1.2.8 ThreadLocal

​ 实现线程同步,除了用synchronized,另外也可以用ThreadLocal,前者是用时间换空间,保证每次只有

​ 一个线程去访问,后者采用空间换时间,ThreadLocal在每个线程中都是独立的,一个变量在每一个线程中都

​ 存在其实例的引用,各个线程之间没有关系。

​ **原理:**其内部有一个ThreadLocalMap内部类,ThreadLocalMap内部有个Entry内部类。

​ 在每个线程Thread内部有一个ThreadLocalMap 类型的成员变量ThreadLocals,其就是用来存储实际的变

​ 量副本,键值为当前ThreadLocal变量,value 为变量副本;

​ 最开始,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会

​ 对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副

​ 本变量为value,存到threadLocals

使用场景

​ 在同一个线程的不同开发层次中共享数据

  1. 建立一个类,并在其中封装一个静态的ThreadLocal变量,使其成为一个共享数据环境;
  2. 在类中实现访问静态ThreadLocal变量的静态方法(设值和取值)

​ 最常见的场景是用来解决数据库连接、Session管理等

private` `static` `ThreadLocal<Connection> connectionHolder
= ` `new` `ThreadLocal<Connection>() {
public` `Connection initialValue() {
  ` `return` `DriverManager.getConnection(DB_URL);
}
};
public` `static` `Connection getConnection() {
return` `connectionHolder.get();
}



  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
private  static  final  ThreadLocal threadSession =  new  ThreadLocal();
 
public  static  Session getSession()  throws  InfrastructureException {
     Session s = (Session) threadSession.get();
     try  {
         if  (s ==  null ) {
             s = getSessionFactory().openSession();
             threadSession.set(s);
         }
     }  catch  (HibernateException ex) {
         throw  new  InfrastructureException(ex);
     }
     return  s;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

ThreadLocal内存泄露问题

内存泄露:程序在申请内存后,无法释放已申请的内存空间,特别是内存泄露堆积,危害很大,不会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

强引用:一个对象具有强引用,不会被垃圾回收器回收,当内存空间不足,java虚拟机宁愿抛出异常,使程序异常终止,也不回收这种对象,
如果想取消强引用和某个对象的关联,可以显示的将引用赋值为null;

弱引用:JVM进行垃圾回收的时候,无论内存是否充足,都会回收被弱引用关联的对象,在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用

每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链,永远无法回收,造成内存泄漏

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用;

所以,每次使用完ThreadLocal都要调用他的remove()方法清除数据,将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如何在子线程中获取主线程threadLocal中set()方法设置的值

​ 可以使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程

​ 中就可以通过get方法获取到主线程set方法设置的值了,

​ InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法,当在主线程

​ 中创建InheritableThreadLocal实例对象后,当前线程Thread对象中维护了一个inheritableThreadLocals变

​ 量,它也是ThreadLocalMap类型,在创建子线程的过程中,将主线程维护的inheritableThreadLocals变量的

​ 值复制到子线程维护的inheritableThreadLocals变量中,这样子线程就可以获取到主线程设置的值了

1.2.9 volatile

​ volatile是java提供的一种轻量级的同步机制,java中包含两种在内的同步机制,同步块(或方法)和volatitle

​ 变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是

​ volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

并发编程3个基本概念

  1. 原子性

  2. 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

    在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的,java提供了volatitle来保证可见性,当一个变量被volatitle修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

  3. 有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象,Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

锁的互斥和可见性

  1. 互斥:一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据
  2. 可见性:它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的**。**如果没有同步机制提供的这种可见性保证,线程看到的共享变 量可能是修改前的值或不一致的值,这将引发许多严重问题

volatile 变量提供理想的线程安全,必须同时满足下面两个条件

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中;

JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile;

volatitle变量特性

  1. 保证可见性,不保证原子性,当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的volatile变量缓存无效;
  2. 禁止指令重排:重排序操作不会对存在数据依赖关系的操作进行重排序,重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2. 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行;

volatitle不适用的场景

  1. 不适合复合操作;可用synchronized代替,或者Lock,或者CAS操作,java并发包里AtomicInteger()类

volatitle原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入主存;
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

单列模式的双重锁为啥要加volatitle

如果发生指令重排,会导致程序初始化错误

第一重锁,判断是否初始化,第二重锁,判断是否被多次实例化

class Singleton{
    // 确保产生的对象完整性
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance==null) { // 第一次检查
            synchronized (Singleton.class) {
                if(instance==null) // 第二次检查
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Memory Barrier(内存屏障)

  • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile语义中的内存屏障

  • 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  • 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

final语义中的内存屏障

  • 新建对象过程中,构造体中对final域的初始化写入(StoreStore屏障)和这个对象赋值给其他引用变量,这两个操作不能重排序;
  • 初次读包含final域的对象引用和读取这个final域(LoadLoad屏障),这两个操作不能重排序;

优秀博文:https://blog.csdn.net/cy973071263/article/details/104383636

1.2.10 synchronized

资源共享解决方案

  1. 使用static关键字修饰要共享的变量,将其变为全局变量,也就是放到了JVM主内存中,实现资源共享;
  2. 实现Runnable接口,因为实现Runnable接口的线程所操作的资源对象本质是同一个对象;

synchronized原理

synchronized用来保证原子性和可见性,是利用锁的机制来实现线程同步,synchronized能保证原子性,可见性,有序性,和可重入性(避免死锁)

一个变量在同一个时刻只允许一个线程对其进行lock,持有一个锁的两个同步块只能串行执行,这就保证了原子性和有序性;

在对一个变量进行unlock操作前,必须也先把此变量同步回主内存中,这就实现了可见性

synchronized用法

synchronized可以修饰静态方法,成员函数(非静态方法),还可以直接修饰代码块,但是上锁的资源只有两类,对象和类

  1. 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  2. 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
  3. 修饰一个代码块,指定加锁对象:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,如果synchronized后面括号括起来的是****一个类*,那么*作用的对象是这个类的所有实例对象*;如果synchronized后面括号括起来的是*一个对象实例*,那么*作用的对象是这个对象实例****;

同步方法:是通过头部标志位用ACC_synchronized标志,告诉JVM这是一个同步方法;

同步代码块:通过Moninorenter和Monitorexit指令,会让线程在执行时,使其持有的锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个线程在尝试获得与对象相关联的Monitor锁的所有权的时候;

java对象头

在java中,每个对象都会有一个monitor对象(监视器),JVM中,对象分三部分组成(对象头、实例数据、填充数据)

img

monitor对象是实现锁机制的基础,线程获取锁本质是线程获取Java对象对应的monitor对象。*重量级锁就是通过**ObjectMonitor**实现的,也就是说重量级锁是基于对象的**monitor**来实现的*

ObjectMonitor中有2个队列,—WaitSet和—EntryList

JVM对synchronized的优化

在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的。Java 的线程是映射到操作系统的原生线程之上的(详见Java线程和操作系统线程的关系)。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态

jdk1.6作出了大量的优化,主要有自旋锁、适应性自旋锁、锁消除、锁粗化等优化方法,又增加了两个锁的状态:偏向锁,轻量级锁,所以现在synchronized一共有四种锁状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

synchronized锁优化方法

锁膨胀:根据实际情况进行膨胀升级,依次是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。也就是说对象对应的锁是会根据当前线程申请,抢占锁的情况自行改变锁的类型。

**偏向锁:**减少同一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,使用偏向锁也就去掉了这一部分的负担,也取消掉了加锁和解锁的过程消耗。

*轻量级锁在无竞争的情况下使用* *CAS 操作**去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉*

轻量级锁:偏向锁只允许一个线程获得锁,轻量级锁是允许多个线程获得锁,但是只允许他们顺序拿,不允许存在竞争,轻量级锁的加锁和解锁都是通过CAS操作实现

自旋锁:轮询申请锁,在轻量级锁转重量级锁之间,可能会存在自旋;

重量级锁:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。它通过操作系统的互斥量和线程的阻塞和唤醒来实现锁机制;

各自优缺点

img

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在****JIT****编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。锁消除可以节省毫无意义的请求锁的时间。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有局部变量,不存在所得竞争关系。

主要通过"逃逸分析"技术来实现,判断一个同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程;

锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁

博文连接:https://blog.csdn.net/cy973071263/article/details/104388899

1.2.11 ReentrantLock

加锁可以使用 synchronized 关键字,也可以使用 ReentrantLock 对象。synchronized 加锁后,会在同步代码块内添加 monitorenter他monitorexit这两个字节码指令。而 ReentrantLock 加锁,则是api层面实现的,它主要依赖于 Unsafe类的线程挂起和恢复功能。

ReentrantLock 重入锁,实现lock接口的一个类,支持可重入性,公平锁和非公平锁,

可重入获取锁,加锁核心方法是nonfairTryAcquire;

释放锁:tryRelease方法;

ReentrantLock 与synchronized区别

  • 实现方式:synchronized由JVM实现,所有JDK版本都支持,ReentrantLock 由JDK实现,1.5以后的版本才支持;
  • 性能差异:JDK1.6在Synchronized中引入了自旋锁、偏向锁、轻量级锁的优化后,性能和ReentrantLock差不多,之前的版本Synchronized效率比ReentrantLock低很多;
  • Synchronized加锁和解锁都是由JVM自动完成的,ReentrantLock需要手动加锁和解锁;
  • ReentrantLock独有的三大功能:
    • 可以指定是公平锁还是非公平锁,Synchronized只能是非公平锁
    • 可以结合Condition类实现有条件的分组唤醒,Synchronized结合wait/notify/notifyAll只能随机唤醒一个线程或者全部唤醒;
    • 提供了能够中断等待锁的线程的机制
  • Synchronized比较好定位异常,因为其生成线程dump文件的时候,能包括锁定信息,能标识死锁或者其他异常问题来源,而ReentrantLock只是JDK中的一个普通类,JVM并不知道哪些线程拥有Lock对象;
  • Synchronized以监视器模式实现锁,而ReentrantLock是采用AQS模式实现锁,支持响应中断、超时、尝试获取锁,同时可关联多个条件队列,Synchronized只能关联一个

ReentrantLock指定公平锁和非公平锁

​ 公平锁和非公平锁描述的是多个线程竞争锁的时候要不要排队,直接排队的就是公平锁;先尝试插队,插队失败再排队的就是非公平锁

//ReentrantLock两个构造方法,ReentrantLock默认是非公平锁,第二个传入参数能够声明是公平还是非公平
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ReentrantLock结合Condition类实现有条件的分组唤醒

如果有三个线程Thread1,Thread2,Thread3按顺序打印A、B、C,可以使用ReentrantLock结合Condition

public class ReentrantLockTest {

    private static final Lock lock = new ReentrantLock();

    private static Condition conditionA = lock.newCondition();

    private static Condition conditionB = lock.newCondition();

    private static Condition conditionC = lock.newCondition();

    public static volatile int permit = 0;

    public static void main(String[] args) {
        Thread thread1 =new Thread(() -> {
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=0) {
                        conditionA.await();
                    }
                    System.out.println("A");
                    permit++;
                    conditionB.signal();
                }
                conditionB.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread thread2 = new Thread(() ->{
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=1) {
                        conditionB.await();
                    }
                    System.out.println("B");
                    permit++;
                    conditionC.signal();
                }
                conditionC.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        Thread thread3 = new Thread(() ->{
            try {
                Thread.sleep(200);
                lock.lock();
                for (int i = 0; i < 10; i++) {
                    while ((permit % 3) !=2) {
                        conditionC.await();
                    }
                    System.out.println("C");
                    permit++;
                    conditionA.signal();
                }
                conditionA.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        });

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

上面的方法还可以优化一下,去掉取余运算

public class AbcTest implements Runnable {

    //打印次数
    private static final int PRINT_COUNT = 10;
    //锁
    private final ReentrantLock reentrantLock;

    //本次要打印所需要的condition
    private final Condition thisCondition;

    //下次要打印所需要的condition
    private final Condition nextCondition;

    //要打印的字符
    private final char printChar;

    public AbcTest(ReentrantLock reentrantLock, Condition thisCondition,
                   Condition nextCondition, char printChar) {
        this.reentrantLock = reentrantLock;
        this.thisCondition = thisCondition;
        this.nextCondition = nextCondition;
        this.printChar = printChar;
    }

    @Override
    public void run() {
        //进入临界区
        reentrantLock.lock();
        try {
            for (int i = 0; i < PRINT_COUNT; i++) {
                System.out.println(printChar);
                //使用nextCondition唤醒下一个线程
                //因为只有一个线程,所以signal或者signalAll都可以
                nextCondition.signal();
                //不是最后一次,则通过thisCondition等待被唤醒
                //此处要加判断,不然10次后会死锁
                if (i < PRINT_COUNT - 1) {
                    try {
                        thisCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();

        Thread thread = new Thread(new AbcTest(lock, conditionA, conditionB, 'A'));
        Thread thread1 = new Thread(new AbcTest(lock, conditionB, conditionC, 'B'));
        Thread thread2 = new Thread(new AbcTest(lock, conditionC, conditionA, 'C'));

        thread.start();
        Thread.sleep(100);
        thread1.start();
        Thread.sleep(100);
        thread2.start();
        Thread.sleep(100);
    }
}

方法二:通过一个锁和一个状态变量来实现(推荐)
public class AbcTest1 {

    //状态变量
    private volatile int state = 0;

    //打印线程
    private class Printer implements Runnable {

        private static final int PRINT_COUNT = 10;
        //打印锁
        private final Object printLock;

        //打印标志位和state变量相关
        private final int printFlag;

        //后续线程的线程打印标志位
        private final int nextPrintFlag;

        //打印字符
        private final char printChar;

        public Printer(Object printLock, int printFlag, int nextPrintFlag, char printChar) {
            this.printLock = printLock;
            this.printFlag = printFlag;
            this.nextPrintFlag = nextPrintFlag;
            this.printChar = printChar;
        }

        @Override
        public void run() {
            //获取打印锁,进入临界区
            synchronized (printLock) {
                //联系打印printCount次
                for (int i = 0; i < PRINT_COUNT; i++) {
                    //循环检验标志位,每次都阻塞然后等待唤醒
                    while (state != printFlag) {
                        try {
                            printLock.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                    System.out.print(printChar);
                    //设置状态变量为下一个线程标志位
                    state = nextPrintFlag;
                    //一个线程在操作,另外两个在等待,所以需要notifyAll();
                    printLock.notifyAll();
                }
            }
        }
    }

    public void test() throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(new Printer(lock, 0, 1, 'A'));
        Thread threadB = new Thread(new Printer(lock, 1, 2, 'B'));
        Thread threadC = new Thread(new Printer(lock, 2, 0, 'C'));

        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();
        Thread.sleep(100);
    }

    public static void main(String[] args) throws InterruptedException {
        AbcTest1 abcTest1 = new AbcTest1();
        abcTest1.test();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222

三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75

public class AddNumberTest {
    //计数器
    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    private class Printer implements Runnable {
        //总打印次数
        private static final int TOTAL_PRINT_COUNT = 5;

        //每次打印5次
        private static final int PER_PRINT_COUNT = 5;

        private final ReentrantLock reentrantLock;

        //本线程的Condition
        private final Condition thisCondition;
        //前一个线程的Condition
        private final Condition afterCondition;

        public Printer(ReentrantLock reentrantLock, Condition thisCondition, Condition afterCondition) {
            this.reentrantLock = reentrantLock;
            this.thisCondition = thisCondition;
            this.afterCondition = afterCondition;
        }

        @Override
        public void run() {
            //进入临界区
            reentrantLock.lock();
            try {
                //循环打印5次
                for (int i = 0; i < TOTAL_PRINT_COUNT; i++) {
                    //打印操作,每次打印5个数
                    for (int j = 0; j < PER_PRINT_COUNT; j++) {
                        //以原子方式将当前值加1
                        //incrementAndGet返回的是新值(加1后的值)
                        System.out.print(atomicInteger.incrementAndGet());
                    }
                    //操作完成通知后面的线程
                    afterCondition.signalAll();
                    if (i < TOTAL_PRINT_COUNT - 1) {
                        try {
                            //本线程释放锁并等待唤醒
                            thisCondition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } finally {
                reentrantLock.unlock();
            }
        }
    }

    public void test() throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition conditionA = reentrantLock.newCondition();
        Condition conditionB = reentrantLock.newCondition();
        Condition conditionC = reentrantLock.newCondition();

        Thread threadA = new Thread(new Printer(reentrantLock, conditionA, conditionB));
        Thread threadB = new Thread(new Printer(reentrantLock, conditionB, conditionC));
        Thread threadC = new Thread(new Printer(reentrantLock, conditionC, conditionA));

        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();
    }

    public static void main(String[] args) throws InterruptedException {
        AddNumberTest addNumberTest=new AddNumberTest();
        addNumberTest.test();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

ReentrantLock提供了能够中断等待锁的线程的机制

ReentrantLock主要是靠ReentrantLock#lockInterruptibly()方法来提供一种可中断的机制

public class LockDemo {

    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                System.out.println(System.currentTimeMillis() + " -> thread-1 获取了锁");
            } catch (InterruptedException e) {
                System.out.println(System.currentTimeMillis() + "-> thread-1 被中断");
            } finally {
                //lock.unlock();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                System.out.println(System.currentTimeMillis() + "-> thread-2 获取到了锁");
                Thread.sleep(3000);
                System.out.println(System.currentTimeMillis() + "-> thread-2 中断thread-1");
                threadA.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        threadB.start();
        Thread.sleep(200);
        threadA.start();
    }
}
输出
1614852259509-> thread-2 获取到了锁
1614852262510-> thread-2 中断thread-1
1614852262510-> thread-1 被中断
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

ReentrantLock内部有一个成员变量,sync,它继承自AbstractQueuedSynchronizer类,同时 sync 有两个实现类,NonfairSync 和 FairSync类,分别代表着非公平锁模式和公平锁模式。ReentrantLock内部的 sync 成员变量默认是非公平锁模式。

其实 ReentrantLock 的逻辑有一大半依靠 AbstractQueuedSynchronizer 类实现,它就是大名顶顶的 AQS,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

先简单介绍AQS

AbstractQueuedSynchronizer(简称AQS),提供原子式管理状态、阻塞和唤醒线程功能以及队列模型的简单框架

static final class NonfairSync extends Sync {
	...
	final void lock() {
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
			acquire(1);
		}
  ...
}
这段代码含义为:
a. 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前    线程设置为独占线程。
b. 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入      Acquire方法进行后续处理。

在看公平锁
tatic final class FairSync extends Sync {
  ...  
	final void lock() {
		acquire(1);
	}
  ...
}
Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢?

结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire方法,而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

AQS整体架构

img

AQS框架共分为5层:API层、锁获取方法层、对列方法层、排队方法层、数据提供层;

**AQS核心思想:**如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态,如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中;

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配

img

AQS使用一个volatitle的int成员变量来表示同步状态(state),通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对state值修改,下图(AQS数据结构)

img

waitStatus: 当前节点在队列中的状态

thread: 表示处于该节点的线程

prev: 前驱指针

predecessor:返回前驱节点,没有抛出npe

nextWaiter: 指向下一个处于CONDITION状态节点

next: 后续指针

线程锁的2种模式

shared: 表示线程以共享的模式等待锁;

exclusive: 表示线程正在以独占的方式等待锁;

JUC中AQS应用场景

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svZVQcnv-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210304190836375.png)]

AQS主要是加锁acquire()方法,解锁release()方法以及state标识

AQS解析:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

美团技术团队:https://tech.meituan.com/

1.2.12 Unsafe

功能:

普通读写:读写一个Object对象的field,直接从内存中的一个地址读写

volatitle读写:可以保证可见性和有序性;

有序写:保证有序性不保证可见性;

直接内存操作:申请内存,重新申请内存,内存初始化,释放内存,内存复制

CAS相关:提供int,long,和Object的CAS操作;

偏移量相关:staticFieldOffset方法用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。objectFieldOffset方法用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量。staticFieldBase方法用于返回Field所在的对象。arrayBaseOffset方法用于返回数组中第一个元素实际地址相对整个数组对象的地址的偏移量。arrayIndexScale方法用于计算数组中第一个元素所占用的内存空间。

线程调度:LockSupport中的park和unpark方法正是通过Unsafe来实现的,park挂起线程,unpark唤醒线程;

类加载:defineClass方法定义一个类,用于动态地创建类。
defineAnonymousClass用于动态的创建一个匿名内部类。
allocateInstance方法用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。
shouldBeInitialized方法用于判断是否需要初始化一个类。
ensureClassInitialized方法用于保证已经初始化过一个类。

内存屏障:loadFence:保证在这个屏障之前的所有读操作都已经完成。
storeFence:保证在这个屏障之前的所有写操作都已经完成。
fullFence:保证在这个屏障之前的所有读写操作都已经完成。

unsafe应用

  • 内存操作:内存分配、释放、拷贝等,java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法;

    为什么使用堆外内存:对垃圾回收停顿的改善,堆外内存直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响;

    提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存;

    DirectByteBuffer是java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现;

    创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放

unsafe应用解析:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

1.2.13 并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用

1. 2.13 多线程相关面试题

  1. 执行execute()方法和submit()方法的区别是什么呢?

    execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

    2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完;

  2. JUC包中的原子类是哪4类?

    基本类型:

    • AtomicInter:整型原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean:布尔型原子类

    数组类型:

    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray :引用类型数组原子类

    引用类型:

    • AtomicReference:引用类型原子类
    • AtomicStampedRerence:原子更新引用类型里的字段原子类
    • AtomicMarkableReference :原子更新带有标记位的引用类型

    对象的属性修改类型:

    • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
    • AtomicLongFieldUpdater:原子更新长整形字段的更新器
    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
  3. AtomicInteger的使用

    public final int get() //获取当前的值
    public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    public final int getAndIncrement()//获取当前的值,并自增
    public final int getAndDecrement() //获取当前的值,并自减
    public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
    public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
    
    inCrementAndGet();//实现++操作,原子性,不需要加锁也是线程安全的;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    AtomicInteger主要使用CAS+volatitle和native方法保证原子性,从而避免synchronized的高开销,设计到Unsafe方法;

  4. AQS组件总结

    • Semaphore(信号量)-允许多个线程同时访问:synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源;
    • CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行;
    • CyclicBarrier(循环栅栏):CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞;
  5. CountDownLatch 和CycliBarrier有什么不同

    CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能,一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行

    CyclicBarrier通过它可以实现让一组线程等待至某个状态之后再全部同时执行,CyclicBarrier可以被重用

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kv71Wceh-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210305102855558.png)]

  1. volatitle关键字场景

    • 状态标记量

    • 双重检查

      public class Singleton {
          private volatile static Singleton instance = null;
       
          private Singleton() {}
       
          public static Singleton getInstance() {
              if (instance == null) {
                  synchronized (Singleton.class) {// 1
                      if (instance == null) {// 2
                          instance = new Singleton();// 3
                      }
                  }
              }
              return instance;
          }
      }   
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • 独立观察

    • 开销较低的读-写锁策略

  2. 如何终止一个线程

    一般是当使用run()或者call()方法时,执行完毕线程会自动结束,如果要手动结束一个线程,可以使用volatitle布尔变量来退出run()方法的循环或者是取消任务来中断线程;

  3. 生产者消费者模型

    public class ProductAndConsumer {
    
        final Lock lock = new ReentrantLock();
        final Condition notFull = lock.newCondition();
        final Condition notEmpty = lock.newCondition();
    
        final Object[] items = new Object[100];
    
        int putptr, takeptr, count;
    
        public void put(Object x) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) {
                    notFull.await();
                }
                items[putptr] = x;
                if (++putptr == items.length) {
                    putptr = 0;
                }
                ++count;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public Object take() throws InterruptedException {
            lock.lock();
            try {
                while (count == 0)
                    notEmpty.await();
                Object x = items[takeptr];
                if (++takeptr == items.length)
                    takeptr = 0;
                --count;
                notFull.signal();
                return x;
            } finally {
                lock.unlock();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
  4. 什么是FutureTask

    表示一个可以取消的异步运算,它有启动和取消运算、查询运算是否完成和取回运算结果等方法,此类提供了对 Future 的基本实现。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。可使用 FutureTask 包装 Callable 或 Runnable 对象。因为 FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行

  5. Java中interrupt 、interrupted 和 isInterruptedd方法的区别?

    interrupt 中断线程,如果没有中断,则该线程的checkAccess方法会被调 用,可能抛出SecurityException异常;

    如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException;

    如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。

    如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。
    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。

    interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程

  6. java中的同步集合与并发集合有什么区别?

    都支持线程安全,主要区别体现在性能和可扩展性,还有如何实现线程安全的;

    同步:HashMap,Hashtable,HashSet,Vector,ArrayList相对比他们并发实现(如ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteHashSet)会慢很多,造成此种原因是因为锁,同步集合会加锁,而并发集合不会加锁,并发集合实现线程安全是通过先进和成熟的技术像锁剥离,比如ConcurrentHashMap分段;

  7. fail-fast和fail-safe机制

    fail-fast即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常

    fail-safe 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

优秀博文:https://blog.csdn.net/hzau_itdog/article/details/91359145

优秀博文:https://blog.csdn.net/qq_36860032/article/details/89928062

java多线程实现消费者生产者模型:https://www.jb51.net/article/183079.htm

1.3 集合

1.3.1 理论知识

img

img

Collection接口是List、set、Queue的父级接口;

Set接口有三个常用实现类,HashSet、TreeSet;EnumSet;

List下有ArrayList和Vector;

1.3.2 原理及使用场景

1.3.1 集合相关面试题

  1. Collection框架中实现比较要怎么做?

    • 实体类实现Compareable接口,并实现compareTo(T t)方法,此法为内部比较器;

    • 创建一个外部比较器,外部比较器要实现Comparator接口的compare(T t1,T t2)方法

      1. 定义外部类Student,无需实现任何接口
      2. 定义外部比较器实现Comparator接口
      public class StudentComparator implements Comparator<Student>{
          @Override
          public int compare(Student o1, Student o2) {
              if (o1.getAge() > o2.getAge()) {
                  return 1;
              }else if (o1.getAge() == o2.getAge()) {
                  return 0;
              }else{
                  return -1;
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
  2. ArrayList和Vector的区别(是否有序,是否重复、数据结构、底层实现)

    • 两者都实现了List接口,都是有序集合,都允许重复,底层都是数组,查询快,增删改慢(查询是对引用地址的访问,不需要遍历,增删慢是因为每次都需要向前或者向后移动元素,插入元素还需要判断是否扩容,扩容是需要创建一个新的数组,增加length再将元素放入较为繁琐);
    • ArrayList线程不安全,Vector线程安全,前者效率高
    • ArrayList每次增长为原来的0.5倍,Vector增长为原来的一倍,两者都可以设置初始空间大小,Vector还可以设置增长空间大小;
  3. ArrayList和LinkedList区别?

    • ArrayList是基于数组实现,LinkedList基于双向链表实现;
    • 随机访问List(get和set),ArrayList效率高,因为LinkedList是线性的数据存储方式,需要移动指针从前往后依次查找;
    • 增加删除时,LinkedList效率要高,只需要改变指针的地址,不影响其他;
    • 自由性不同,ArrayList需要手动设置固定大小的容量;
    • ArrayList会预留空间,而LinkedList需要存储节点信息和节点指针信息;
  4. ArrayList和Set的区别?

    • set元素无序、不重复,有两个实现类HashSet和TreeSet,HashSet是根据对象的Hash值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能,TreeSet则是二叉树的方式存储元素,可以对集合进行排序;
  5. ArrayList、Vector、LinkedList、Set遍历方式

    • For循环遍历ArrayList

    • 增强for循环foreach 遍历ArrayList

    • 迭代器Iterator遍历

      Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
      	System.out.print(iterator.next() + "  ");
      		}
      
      • 1
      • 2
      • 3
      • 4
    • 双向迭代器listIterator

      ListIterator listIterator = list.listIterator();
      	while (listIterator.hasNext()) {
      		System.out.print(listIterator.next() + "  ");
      		}
      
      • 1
      • 2
      • 3
      • 4
  6. HashMap与HashTable区别?

    • HashMap线程不安全、HashTable 线程安全,HashMap允许null键值,hashTable 不允许;

    • HashMap线程不安全是因为主要考虑到了多线程环境下进行扩容可能出现HashMap死循环,hashtable 单个方法操作是线程安全的,但是符合操作时不一定能保证,容易导致越界异常

    • 继承的父类不同,hashMap继承自AbstractMap类,而hashtable继承自Dictionary类,不过都同时实现了map,Cloneable(可复制)、Serializable(可序列化)三个接口

    • 当需要线程安全的时候也可以实现ConcurrentHashMap,效率也比hashTable高,因为其使用了分段锁;

    • 扩容不同,HashTable默认初始化大小是11,之后每次扩容,容量是原来2n+1倍,HashMap默认是16,每次扩容为原来的2倍,

      hashtable会尽量使用素数,基数,hashMap总是使用2的幂作为哈希表大小;

    • 计算hash值方法不同,HashTable直接使用对象的hashCode,**hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数法来获得最终的位置。**所以比较耗时,hashMap是将hash表的大小固定在2的幂,这样在取模运算时,不需要做除法运算,只需要做位运算,效率比除法高,但是相应的hash冲突也多了,hashMap也采取了分散法来打散运算结果;

    • 或者可以使用Collections的synchronizedMap方法使hashMap具有同步的能力,LinkedHashMap记录了插入顺序(有序)

    • synchronizedMap使用互斥锁+synchronized关键字实现线程安全

  7. HashMap遍历?

    1.通过Map.keySet来遍历key和value
     for (String key : map.keySet()) {
          System.out.println("key= "+key+" and value= "+map.get(key));
       }
       
    2. 通过Map.entrySet使用iterator遍历key和value
    Iterator<Map.Entry<String, String>> it = 
                    map.entrySet().iterator();
            while(it.hasNext()){
                Map.Entry<String, String> entry = it.next();
                System.out.println("key= "+entry.getKey()+" and value= "+entry.getValue());
            }
            
    3. 通过Map.values()遍历所有的value,但是不能遍历key;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  8. Map集合java8新特性?

    • 结构上,1.7中底层是数组+链表形式,1.8中是数组+链表+红黑树(解决了链表太长导致的查询变慢的问题);
    • 初始化方式:1.7 infateTable(),1.8直接集成了扩容函数resize()
    • hash值计算方式:1.7中9次扰动=4次位运算+5次异或运算,1.8中 2次扰动=1次位运算+1次异或运算
    • 存放规则:1.7 无冲突存放数组,有冲突,数组+链表,1.8无冲突数组,有冲突,链表长度小于8,数组+链表,链表>8,加红黑树
    • 插入数据方式:1.7 头插法,1.8尾插发
  9. HashMap扩容机制?

    • 初始容量是16,扩容因子是0.75,扩容完成,容量为2n,当容量达到16*0.75=12,就可能需要扩容;
    • 为什么长度是2的幂:减少冲突(碰撞)的次数,提高效率,如果是2的幂,length-1转化为二进制是1111…的形式,如果不是2的幂,则length-1转化为二进制是1110…,最后一位都是0,会导致空间浪费,会增加碰撞几率;
  10. hashSet和TreeSet的区别

    • **HashSet底层使用了Hash表实现。**保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true
    • **TreeSet底层使用了红黑树来实现。**保证元素唯一性是通过Comparable或者Comparator接口实现
  11. 解决hash冲突方法?

    • 拉链法
    • 线性探测再散列法
    • 二次探测再散列法
    • 伪随机探测再散列法
    • 使用2次扰动函数(hash函数)来降低哈希冲突的概率;
    • 使用红黑树进一步降低遍历的时间复杂度;
  12. 数组和集合List之间的转换?
    主要通过Arrays.asList以及List.toArray方法

  13. hashMap的put方法的具体流程?

    1. 如果table为空或者长度为0,即没有元素,使用resize方法扩容;
    2. 计算插入存储的数组索引i,如果数组为空,即不存在hash冲突,则直接插入数组;
    3. 插入时如果发生hash冲突,则进行判断:
       a. 判断table[i]的元素key是否与需要插入的key相同,若相同则直接使用新的vlue覆盖掉旧的value,使用equals方法;
       b. 继续判断,需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接在树中插入or更新键值对,如果是链表,则在链表中插入或者更新键值对;链表长度>8,则转为红黑树;
    4. 插入成功,检查存在的键值对数量size>最大容量,大于则进行扩容;
       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  14. jdk1.8,Map为什么要引入红黑树

    主要是为了解决二叉树的缺陷,二叉树在特殊情况下会变成一条线性结构(会跟链表一样,造成很深的问题),遍历查询会非常慢,而红黑树在插入新数据时可能通过左旋、右旋、变色操作保持平衡,可以解决查找数据慢,链表深度太深的问题,当然链表很短就不需要引入红黑树;

  15. 为什么hashMap在链表长度大于8的时候转为红黑树?
    主要是考虑到树需要存储节点占用空间是普通节点的2倍,节点足够多的时候,可以一空间换时间,保证效率,通常情况下链表长度很难达到8,在链表长度为8时性能已经很差;

1.4 文件流以及IO

1.4.1 理论知识以及结构图

这里写图片描述

这里写图片描述

1.4.2 实际操作

1.4.3 NIO、BIO、AIO

  1. 介绍

    BIO:同步阻塞IO,在读写操作完成之前,线程一直阻塞;

    NIO:多路复用同步非阻塞IO,提供了Chanel、selector、buffer等新的概念

    AIO: 异步非阻塞IO

    NIO主要有三大核心:channel(通道)、Buffer(缓冲区)、Selector(选择区)

    channel(通道):双向的,可进行读操作,也可进行写操作,主要实现有FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel;

    Buffer:缓冲区

    Selector(选择区):能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理;

  2. 多路复用三种实现?

    • **select:**它仅仅知道,有I/O事件发生,并不知道是那个流,只能无差别轮询所有流,找出能读或者写入的流,操作流程

      使用copy_from_user 从用户空间拷贝fd_set到内核空间;

      注册回调函数_pollwait

      遍历所有fd,调用其对应的poll方法;

      以tcp_poll为例,其核心实现就是_pollwait

      __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列;

      poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值;

      如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠;

      把fd_set从内核空间拷贝到用户空间;

      **缺点:**单个进程所打印的fd是有限制的,通过FD_SETSIZE设置,默认1024;

      都会把fd从用户态拷贝到内核态,这个开销在fd很多时很大;

    • poll:将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,没有最大连接数的限制,基于链表存储;

    • 链接:https://blog.csdn.net/weixin_39662142/article/details/110396979

1.4.4 文件流相关面试题

  1. 字节流和字符流那个好,怎么选择?

    大多数情况使用字节流,大多数IO都是直接操作磁盘文件,所以一般都是以字节的形式进行;

    如果操作IO需要通过内存中频繁处理字符串的情况,就用字符流,因为字符流具备缓冲区,提高了性能;

  2. 什么是java序列化,如何实现java序列化?

    处理对象流的一种机制,将对象进行流花,可以对流化后的对象进行读写操作,一般实现Serialize接口;

  3. BufferedReader属于那种流,主要用来做什么?

    用于处理流中的缓冲流,可以将读取的数据存在内存里面,有readLine方法,用来读取一行;

1.5 锁

1.5.1 锁的定义、原理、作用

1.5.2 锁的分类以及实现方法

img

1.5.3 锁的实际应用

1.5.4 锁优化、以及使用过程中的问题

1.5.5 锁相关面试题

1.6 网络协议

1.6.1 网络协议原理

1.6.2 网络协议分类

1.6.3 网络协议运用

1.6.4 http请求相关问题

1.6.5 相关面试题

  1. Get请求和Post请求的区别?

    • get请求可被缓存,将保留在浏览器历史记录中,可被收藏为书签,post都不可以
    • get请求不要再处理铭感数据时使用;
    • get请求有长度限制,post没有;
  2. DNS使用的协议

    TCP和UDP都有使用,UDP报文最大长度是512字节,而TCP则超过512字节,DNS查询超过512字节,尽量使用TCP;

    区域传送使用TCP:可以保证数据准确性,还有传输数据量大的问题;

    域名解析使用UDP协议:不会经过TCP三次握手,字节数少,此时DNS负载低,响应快;

  3. 幂等

    一个幂等操作的特点是其任意多次执行所产生的影响均与第一次执行的影响相同,幂等函数或者幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数,这些函数不会影响系统状态;

    http get请求可以认为是幂等;

    http post请求不是幂等,put幂等,patch非幂等,delete是幂等;

  4. http、UDP、TCP区别?

    • 网络七层模型,应用层、表示层、会话层、传输层、网络层、数据链路层、物理层;
    • http:超文本传输协议,是一种无状态协议,客户端发送请求,服务器接收请求,经过处理返回给客户端;
    • TCP: 传输控制协议,是面向连接的协议,在发数据前必须先和对方建立可靠的链接,一个TCP要经过3次对话才能建立起来,其属于传输层;
    • UDP:用户数据报协议,面向非连接,传输不可靠,用于传输少量数据
  5. TCP三次握手?四次挥手?

    • TCP建立过程,A-B,B收到信息会用一个带有确定应答的(ACK)和同步序列号(SYN)标志位的数据段响应主机A,A收到数据在发送确认应答,没有应用层的数据;
    • TCP断开(4次握手):主机A完成数据传输后,将控制位FIN置为1,提出停止TCP连接,主机B收到FIN作出响应,确认这一方向上的TCP连接关闭,将ACK置1,B再反方向提出关闭请求,将FIN置1,主机A对主机B的请求进行确认,将ACKz置1,
  6. cookies和session区别?

    • Cookiess是一种能够让网站服务器把少量数据存储到客户端的硬盘或者内存;
    • cookie数据存在客户浏览器上,session数据存储在服务器上;
    • cookie不是很安全,但是可以加密,cookie可以过期,session可以被销毁;
  7. TCP粘包和拆包区别?解决策略?

  8. 正向代理、反向代理区别联系?应用?

    • 正向代理:是一个位于客户端和原始服务器之间的服务器,代理客户端和原始服务器的,服务器端并不知道真正的客户端到底是谁,隐匿的是客户端,配置的也是客户端
    • 反向代理:服务器的代理,客户端不知道真正的服务器是谁,隐匿的是服务端,配置的也是服务端
  9. 一次完整http请求过程?
    域名解析–>发起TCP的3次握手–>建立TCP连接后发起httpq请求–>服务器响应http请求,浏览器得到html代码–>浏览器解析html代码,并请求html代码中的资源–>浏览器对页面进行渲染呈现给用户;

  10. TCP如何保证可靠传输?

    • 三次握手
    • 将数据截断为合理的长度,应用数据被分割成TCP认为最适合发送的数据块
    • 超时重发,当TCP发出一个段后,它启动一个定时器,如果不能及时收到一个确认就重发
    • 对于收到的请求,给出确认响应
    • 校验出包有错,丢弃报文段,不给出响应
    • 对失效数据进行重新排序,然后才交给应用层

1.7 tomcat、webSocket等服务器知识

1.7.1 tomcat介绍、原理、作用

1.7.2 tomcat使用

1.7.3 tomcat 调优

1.7.4 webSocket原理、作用

1.7.5 相关面试题

  1. tomcat使用哪种IO,为什么?
    tomcat默认使用的是BIO,客户系统使用BIO的时候往往为每一个web请求引入多线程,每个web请求一个单独的线程,如果并发量上去,线程数就上去了,CPU忙着线程切换,所以BIO不适合高吞吐量,高可伸缩性的web服务器;

    NIO是使用单线程(单个CPU)或者少量的多线程来接受socket,可以大大提高web服务器的可伸缩性;

  2. 有哪些服务器?区别和用途?

    • 有tomcat服务器、Jetty服务器、Jboss服务器、Glassfish服务器、Websphere服务器、WebLogic服务器;
    • Tomcat和jetty都是一种Servlet引擎,都支持标准的servlet规范和javaEE规范;
    • jetty架构是基于Handler 来实现的,tomcat是基于容器设计的;
    • jetty可以同时处理大量连接而且可以长时间保持连接,适合做web聊天应用等等,可以减少服务器内存开销,从而提高服务器性能,默认采用NIO,在处理I/O请求更占优势,适合处理静态资源,tomcat适合处理生命周期短的,不适合处理静态资源;
    • Jboss不仅是Servlet容器,还是EJB容器
  3. Tomcat组成?

    • 主要有Container和Connector以及相关组件构成;
    • Server: 整个tomcat服务器,包含多组服务,负责管理和启动各个Service,同时监听8005端口发过来的shutdown命令,用于关闭整个容器;
    • Service:tomcat封装的、对外提供完整的、基于组件的Web服务,包含Connectors、Container两个核心组件,各个Service之间独立,但是共享同一个JVM资源;
    • Connector: tomcat与外部世界的链接器,监听固定端口接收外部请求,传递给Container、并将Container处理结果返回给外部;
    • Container :用于管理Servlet生命周期,调用servlet相关方法;
    • Loader:封装了java ClassLoader,用于Container加载类文件

1.8 反射原理

1.8.1 反射原理、底层

定义:指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用他的任意一个方法,动态获取信息,以及动态调用对象的方法叫做java的反射机制;

应用: 生成动态代理,面向切面编程

常见实现方法

  • 对象.getClass()
  • Class.forName(className);
  • 类名.class;

三种实现方式之间的区别:

  • 类加载方式不同,Class.forName()属于动态加载类,在代码运行时加载指定类,Class.class属于静态加载类在代码编译时加载指定;
  • Object.getClass()取决于对象的产生方式,即可以是静态加载类(通过new创建的对象)也可以是动态加载类;
  • **Class对象创建方式不同:**Class.forName()运行阶段,JVM使用类装载器,将类装入内存中,并对类进行初始化(静态代码块、非静态代码块、构造函数调用以及静态变量)最后返回Class对象;
  • class,编译阶段,JVM使用类装载器,将类装入内存中,并对类进行初始化操作,最后返回Class的对象;对于GetClass(),没有其他操作

1.8.2 反射应用、常见方法

  1. 获取所有公共构造函数:getConstructors()

  2. 获取所有的构造函数:getDeclaredConstructors()

  3. 操作方法:

    • 使用Class获取对应方法:getMethods(),获取所有的公共方法,包括父类的公共方法;

    • getDeclaredMethods(),获取所有本类的方法,包括本类的私有方法;

    • getDeclareMethod(String name,Class<?>…parameterTypes)

      获取指定方法名称的方法,和访问权限无关;

  4. 创建对象的方法:

    • 构造函数创建,newInstance(Object initargs)
    • Class类中创建:newInstance();
    • 注意,如果是私有的构造方法,反射默认是无法直接执行的,可以找到父类的Accessible(boolean flag)的方法,设置为true,即可忽略访问权限;
  5. Method执行方法

    • invoke(Object obj,Object…args):执行方法;
  6. Class中获取字段:

    • getFields(),获取当前class所表示类中所有的public字段,包括继承的字段;
    • getDeclaredFields(),获取当前Class所表示类中所有的字段,不包括继承的字段
    • getField(String name):
    • getDeclaredField(String name):获取指定名字的字段,但是不包括继承的字段;
  7. JDK动态代理反射机制?

    • 动态代理:使用Proxy类的newProxyInstance方法创建代理对象,使用InvocationHandler来实现增强的逻辑(通常是创建一个InvocationHandler接口的实现类,在其invock方法中实现增强的逻辑),利用拦截器加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;

    • CGLib代理:使用MethodInterceptor接口实现增强逻辑,使用Enhancer生成代理对象,利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理;

    • 如果目标对象实现了接口,默认情况下会采用jdk的动态代理实现AOP;

    • 如果目标对象实现了接口,可以强制使用CGLIB实现AOP

    • 如果目标对象没有实现接口,必须使用CGLIB库;并在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class = “true”/>

1.8.3 反射相关面试题

  1. java序列化的作用和方法?

    将对象状态转化为字节流,以后可以通过这些值再生出相同状态的对象,用于存储和传输,可以实现数据的持久化,可以把数据永久保存到硬盘上,序列化可以实现远程通信,转成在网络上传输的字节序列;

    **方法:**java.io.ObjectOutputStream对象输出流,他的writeObject(Object obj)可以对参数指定的obj对象进行序列化;

    ObjectInputStream对象输入流,他的readObject()方法可以从输入流读取字节序列,再把它们反序列化成为一个对象;

    实现Serializable或者Externalizable接口,可以实现序列化;

    serialVersionUID :它决定着是否能够反序列化成功,相当于一个版本号,在反序列化的时候会进行序列号对比;

    谷歌的一些工具也可以实现序列化;

    继承中方法执行顺序

    父类静态方法>子类静态方法>父类普通方法>父类构造方法>子类普通方法>子类构造方法

1.9 对象相关知识

  1. 静态内部内和非静态内部类?匿名内部类、静态内部类、非静态内部类初始化顺序?
    • 静态内部类方法和变量可以是静态的也可以是非静态的,静态方法可以在外层通过静态调用,而非静态方法必须创建类的对象才能调用;
    • 非静态内部类不能有静态成员(方法、属性);
    • 静态内部类只能访问外部类的静态成员,无法访问非静态成员,非静态可以访问所有;
    • 一般静态内部类先执行
  2. 继承中静态方法,构造方法,普通方法执行顺序问题?
  3. 抽象类与接口?
  4. 方法重载与重写?

1.20 基本数据类型

  1. 各种数据类型占用字节数?
  2. 各种数据类型范围?

1.21 进制转换

1.22 计算机原理

二、JVM

2.1 JVM内存模型

  1. 内存模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-07gnCuuc-1618197750710)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210308094500874.png)]

img

  • **程序计数器:**一块较小的内存空间,当前线程所执行的字节码的行号指示器,分支、循环、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,程序计数器是线程私有的,唯一一个没有OOM的区域
  • **java虚拟机栈:**线程私有,java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息),在此区域规定了2种异常情况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出stackOverflowError异常,虚拟机栈也支持动态扩展长度,但是如果不能申请到足够内存,也会抛出outOfMeeoryError异常;
  • **本地方法栈:**为虚拟机使用到的Native方法服务,也会抛出stackOverflowError异常和outOfMeeoryError异常
  • java堆:java内存中最大的一块,所有线程共享的一块内存,存放对象实例,也是垃圾收集器管理的主要区域,可分为新生代、老年代,再细致一点就是Eden空间、From Survivor空间、To Survivor空间;
  • 方法区:与java堆一样,也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,也会出现outOfMeeoryError异常;
  • **运行时常量池:**是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放,当常量池无法再申请到内存会抛出OutOfMemoryError异常;
  • 直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
  1. 堆内存分配
    • 新生代,存放新生的对象,一般占据堆1/3空间,新生代又分为Eden区,ServivorFrom, ServivorTo三个区
    • Eden区:java新生对象出生地;
    • ServivorFrom:上一次GC幸存者,作为这一次GC的被扫描者;
    • ServivorTo:保留了一次 MinorGC 过程中的幸存者;

2.2 类加载机制

2.3 垃圾回收机制、工具、算法

  1. 判断对象是活着还是死亡算法?

    • 引用计数算法:有引用计数加1,引用失效计数减1,任何时刻计数器为0,就表示不会再被使用,但是存在一个问题就是,很难解决对象循环互相引用的问题
    • 在主流实现中,大部分都是通过可达性分析来判断对象是否存活,核心思想:通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,表示对象不可用,可作为GC Roots的对象包括:虚拟机栈中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象,本地方法栈中JNI引用的对象
  2. 垃圾收集算法

    • 标记-清除算法:效率不高,容易产生大量不连续的内存碎片;
    • **复制算法:**将内存按容量化为大小相等的2块,每次只使用一块,一块用完,将这块还活着的对象复制到另外一块,再把使用过的一次性清除,以空间换时间;分为一块较大的Eden空间和2块较小的Survivor空间;
    • 标记-整理:针对老年代的垃圾回收算法,先标记,再整理-清除;
  3. 垃圾收集器?

    • Serial收集器:单线程版收集器,可能会造成停顿,新生代,复制算法;
    • ParNew收集器:Serial收集器的多线程版,采用复制算法,用在新生代,可以通过-XX:parallelGCThreads参数来控制收集的线程数;
    • Parallel Scavenge收集器:使用复制算法的手机器,又是并行的收集器
    • Serial Old收集器:使用标记-整理算法,作用与老年代;
    • parallel Old收集器:多线程的标记-整理算法;
    • CMS收集器:以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,包括初始标记、并发标记、重新标记、并发清除,会产生空间碎片,作用于老年代;
    • G1收集器:支持并行与并发,特点:分代收集、空间整合、可预测的停顿,实现步骤:初始标记、并发标记、最终标记、筛选回收,不会产生空间碎片,可精准控制停顿,作用于新生代和老年代;
    • ZGC:JDK11中推送出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收
  4. 内存分配与回收策略?

    • 对象优先在Eden分配;
    • 大对象直接进入老年代;
    • 长期存活的对象将进入老年代;
    • 动态对象年龄判定
    • 空间分配担保
  5. 虚拟机类加载机制?

    • 加载:在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据入口;
    • **验证:**确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求
    • **准备:**在方法区中分配这些变量所使用的内存空间;
    • **解析:**虚拟机将常量池中的符号引用替换为直接引用的过程;
    • 初始化:
  6. 虚拟机类加载器?

    • **启动类加载器(Bootstrap ClassLoader):**负责将JAVA_HOME/lib目录中,或者被-Xbootclasspath参数所指引的路径中,并且是虚拟机识别的类库加载到虚拟机内存中,不能直接引用,需要委派给引导类加载器;
    • **拓展类加载器(Extension ClassLoader)
      声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/72199
推荐阅读
相关标签