面试问题整理-自用

3. Java 8 的新特性有什么?工作中用了哪些?

https://zhuanlan.zhihu.com/p/439176814
java8新增了非常多的新特性,这里主要讨论如下几个:

3.1 Lambda表达式

Lambda表达式,也称闭包,是Java8最大的语言改变。
java曾经只能以匿名内部类的方式实现Lambda表达式的功能。
Lambda表达式的语法格式如下所示:

(parameters) -> expression
或
(parameters) ->{ statements; }

Lambda编程风格总结:

  • 可选类型声明:不需要声明参数类型,编译器可以同一识别参数值(当然也是可以声明的)
  • 可选的参数圆括号:如果只有一个参数,可以不加圆括号
  • 可选的大括号:如果主体就一个语句,可以不用大括号
  • 可选的返回关键字:如果主体只有一个表达式则编译器自动返回该值

3.2 方法引用

方法引用使用::通过方法的名字来指向一个方法。

  • 构造器引用:它的语法是Class::new,或者更一般的Class< T >::new实例如下:
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
  • 静态方法引用:它的语法是Class::static_method,实例如下:
cars.forEach( Car::collide );
  • 特定类的任意对象的方法引用:它的语法是Class::method实例如下:
cars.forEach( Car::repair );
  • 特定对象的方法引用:它的语法是instance::method实例如下:
final Car police = Car.create( Car::new );
cars.forEach( police::follow );

在这里插入图片描述

3.3 函数式接口

函数式接口指有且仅有一个抽象方法,但可以有多个非抽象方法的接口。这样的接口可以隐式转换为Lambda表达式。
目前 Java 库中的所有相关接口都已经带有这个注解了,实践上java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子!
但是在实践中,函数式接口非常脆弱,只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface
举个例子:

@FunctionalInterface
public interface GreetingService {

    void sayMessage(String message);
}

Java8中可以采用Lambda表达式进行编程

GreetingService greetService = message -> System.out.println("Hello " + message);
greetService.sayMessage("world");

3.4 默认方法

默认方法是指接口的默认方法,它是java8的新特性之一。顾名思义,默认方法就是接口提供一个默认实现,且不强制实现类去覆写的方法。默认方法用default关键字来修饰。
在java8之前,修改接口功能通常会给接口添加新的方法,这时对已经实现该接口的所有实现类,都要一一添加对新方法的实现,换言之,在给接口定义新方法的同时无法不影响到已有的实现类,这时,java8的默认方法特性就可以解决这种接口修改与已有实现类不兼容的问题

public interface Vehicle {
    //默认方法
   default void print(){
      System.out.println("我是一辆车!");
   }
}

3.5 Stream API

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
举个例子:

List<Integer> transactionsIds = 
widgets.stream()
             .filter(b -> b.getColor() == RED)
             .sorted((x,y) -> x.getWeight() - y.getWeight())
             .mapToInt(Widget::getWeight)
             .sum();

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

3.6 Optional类

3.7 新的日期时间API

3.8 Nashorn JS引擎

4. HashMap的数据结构?有什么优化?源码看一下?

https://zhuanlan.zhihu.com/p/79219960

4.1 HashMap介绍

之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树
另外,HashMap是非线程安全的,也就是说在多个线程同时对HashMap中的某个元素进行增删改操作的时候,是不能保证数据的一致性的。
java 1.7 使用数组 + 链表的方式来存储HashMap
下图是java 1.8 后HashMap的存储方式!
在这里插入图片描述
只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树
我们会发现优化的部分就是把链表结构变成了红黑树。原来jdk1.7的优点是增删效率高,于是在jdk1.8的时候,不仅仅增删效率高,而且查找效率也提升了。

红黑树:自平衡的二叉查找树,查询效率非常高,能从O(n)转变为O(logn)
HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

4.2 源码解析

  1. 储存元素put
    在这里插入图片描述
  2. 扩容
    在这里插入图片描述

4.3 ConcurrentHashMap如何实现线程安全的?

简单地说,ConcurrentHashMap通过CAS和Synchronized实现比HashTable效果更好的线程安全!
https://blog.csdn.net/qq_41737716/article/details/90549847

5. IO模型

  1. 阻塞式I/O模型(Blocking I/O Model):进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。
    在这里插入图片描述

非阻塞式I/O模型(Non-blocking I/O Model):进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞,但需要不断地轮询I/O操作的状态。进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。
在这里插入图片描述

I/O复用模型(I/O Multiplexing Model):多个的进程的IO可以注册到一个复用器(select)上,然后用一个进程调用该select, select会监听所有注册进来的IO;
如果select没有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞;而当任一IO在内核缓冲区中有可数据时,select调用就会返回;
而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO,读取内核中准备好的数据。
可以看到,多个进程注册IO后,只有一个select调用进程被阻塞。
在这里插入图片描述

信号驱动式I/O模型(Signal-driven I/O Model):在进行I/O操作时,应用程序不会被阻塞,但需要注册信号处理函数,在I/O操作完成时,系统会向应用程序发送一个信号,通知其进行处理。
在这里插入图片描述

异步I/O模型(Asynchronous I/O Model):在进行I/O操作时,应用程序不会被阻塞,I/O操作的完成会通过回调函数的方式通知应用程序,从而实现异步处理。
在这里插入图片描述
五个IO模型的比较
在这里插入图片描述

1. 线程池的参数是什么?怎么用?

下面是ThreadPoolExecutor类的构造方法源码,其他创建线程池的方法最终都会导向这个构造方法,共有7个参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。

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

  • corePoolSize:核心线程数
    线程池维护的最小线程数量。核心线程创建后不会被回收。
    大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收
  • maximumPoolSize:最大线程数
    线程池允许创建的最大线程数量
  • keepAliveTime:空闲线程存活时间
    当一个可回收线程的空闲时间大于这个参数,就会被回收
    可被回收的线程:非核心线程,设置allowCoreThreadTimeout=true的核心线程
  • unit:时间单位
  • workQueue:工作队列
    存放待执行任务的队列。当提交的任务超过核心线程数后,再提交的任务会放在工作队列,任务调度时再从队列中取出任务。 它仅仅用来存放被execute()方法提交的Runnable任务。
    JDK默认的工作队列有五种:
    ArrayBlockingQueue
    LinkedBlockingQueue
    SynchronousQueue
    PriorityBlockingQueue
    DelayQueue
  • threadFactory:线程工厂
    创建线程的工厂。可以设定线程名,线程编号等
  • handle:拒绝策略
    当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。
    JDK默认的拒绝策略有四种:
    AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
    DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
    CallerRunsPolicy:由调用线程处理该任务。
    在这里插入图片描述
    自己写一个线程池:
package threadpool;

import java.util.concurrent.*;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        System.out.println("可用的系统线程数:" + Runtime.getRuntime().availableProcessors());

        ExecutorService threadPool = new ThreadPoolExecutor(
                5,           //核心线程数
                10,                      //最大线程数
                1L,                     //存活时间
                TimeUnit.SECONDS,       //时间单位
                new LinkedBlockingDeque<>(3),   //工作队列
                Executors.defaultThreadFactory(),       //线程工厂
                new ThreadPoolExecutor.AbortPolicy()    //拒绝策略
        );

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " 办理业务");
                });
            }
        } catch (Exception E) {
            E.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

2. Java并发模型有哪些?

https://blog.csdn.net/songguangfan/article/details/118580922
https://www.cnblogs.com/cxuanBlog/p/13494343.html
并发系统可以采用多种并发编程模型来实现。并发模型一般都指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同。

2.1 并行工作者模式

在这里插入图片描述

在并行工作者模型中,委派者将传入的作业分配给不同的工作者。每个工作者完成整个任务。工作者们并行运行在不同的线程上,甚至可能在不同的CPU上运行。
并行 Worker 模型是 Java 并发模型中非常常见的一种模型。许多 java.util.concurrent 包下的并发工具都使用了这种模型。

  • 优点
    很容易理解,为了提高系统的并行度你可以增加多个 Worker 完成任务。
    整个 Worker -> Delegator -> Client 的过程是异步的。
  • 缺点
    共享状态会变得很复杂,线程需要确保共享状态是否能够让其他线程共享,需要避免 竞态条件,死锁 和许多其他共享状态造成的并发问题。
    无状态的工作者:共享状态能够被系统中的其它线程修改。所以工作者在每次需要的时候必须重读状态以确保每次都能访问到最新的副本,称为无状态的。每次都重读需要的数据,将会导致速度变慢。
    作业顺序是不确定的:无法保证首先执行或最后执行哪些作业

2.2 流水线并发模型

基本的流水线并发图如下所示:
在这里插入图片描述
实际情况中,通常不会按照一条装配线流动,甚至各个worker中还要共同参与完成
在这里插入图片描述
流水线又可以分为Actor模型和Channels模型

  • Actor模型
    在 Actor 模型中,每一个 Actor 其实就是一个 Worker, 每一个 Actor 都能够处理任务。
    在这里插入图片描述
  • Channels模型
    在 Channel 模型中,worker 通常不会直接通信,与此相对的,他们通常将事件发送到不同的 通道(Channel)上,然后其他 worker 可以在这些通道上获取消息。这种方式降低了 worker 和 worker 之间的耦合性。
    在这里插入图片描述
  • 优点
    不存在共享状态
    有状态worker
    更好的硬件整合
    任务进行更加高效
  • 缺点
    难编写,难回调

2.3 函数式并行模式

第三种并发模型是函数式并行模型。函数式并行的基本思想是采用函数调用实现程序。函数可以看作是”代理人(agents)“或者”actor“,函数之间可以像流水线模型(AKA 反应器或者事件驱动系统)那样互相发送消息。某个函数调用另一个函数,这个过程类似于消息发送。
一旦每个函数调用都可以独立的执行,它们就可以分散在不同的CPU上执行了。这也就意味着能够在多处理器上并行的执行使用函数式实现的算法。

3. Future,FutureTask与CompletableFuture

https://blog.csdn.net/sermonlizhi/article/details/123356877

3.1 Future与FutureTask

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future源码:

public interface Future<V> {
	// 取消任务的执行,参数表示是否立即中断任务执行,或者等任务结束
    boolean cancel(boolean mayInterruptIfRunning);
	// 任务是否已经取消,任务完成前将其取消,则返回true
    boolean isCancelled();
	// 任务是否已经完成
    boolean isDone();
    // 等待任务执行结束,返回泛型结果.中断或任务执行异常都会抛出异常
    V get() throws InterruptedException, ExecutionException;
	// 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future归根结底只是一个接口,而FutureTask实现了这个接口,同时还实现了Runnable接口,这样FutureTask就相当于是消费者和生产者的桥梁了,消费者可以通过FutureTask存储任务的执行结果,跟新任务的状态:未开始、处理中、已完成、已取消等等。而任务的生产者可以拿到FutureTask被转型为Future接口,可以阻塞式的获取处理结果,非阻塞式获取任务处理状态
总结:FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取Callable的返回结果。


FutureTask的使用:构建一个FutureTask对象,其构造方法的入参为Callable的实例对象,然后将FutureTask对象作为Thread构造方法的入参。

		FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println("通过Callable执行任务");
                Thread.sleep(3000);
                return "返回任务结果";
            }
        });
        //在Thread中启动
        new Thread(task).start();

        //查询这个task是否完成(非阻塞)
        System.out.println(task.isDone());
        //获得task的结果(阻塞)
        System.out.println(task.get());

Future的注意事项:

  • 当 for 循环批量获取Future的结果时容易 block,get 方法调用时应使用 timeout 限制
  • Future的生命周期不能后退。一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来

Future的局限性:
从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:

  • 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;
  • 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
  • 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
  • 没有异常处理:Future接口中没有关于异常处理的方法;

而这些局限性CompletionService和CompletableFuture都解决了,这边文章重点介绍CompletableFuture的使用

3.2 CompletableFuture对Future的改进

Future接口的功能无法满足更加复杂的异步处理流程,所以由CompletableFuture进行异步功能的改进和扩张
CompletableFuture实现了Future接口和CompletionStage接口

3.2.1 CompletableFuture四大静态方法

在这里插入图片描述

  • runAsync() 以Runnable函数式接口类型为参数,没有返回结果,supplyAsync() 以Supplier函数式接口类型为参数,返回结果类型为U;Supplier接口的 get()是有返回值的(会阻塞)
  • 使用没有指定Executor的方法时,内部使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。
  • 默认情况下CompletableFuture会使用公共的ForkJoinPool线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置ForkJoinPool线程池的线程数)。如果所有CompletableFuture共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要根据不同的业务类型创建不同的线程池,以避免互相干扰

静态初始化代码:CompletableFuture/CompletableFutureBuildDemo.java

3.2.2 CompletableFuture通用异步编程

代码:CompletableFuture/CompletableFutureDemo.java

3.2.3 函数式接口,链式语法

https://blog.csdn.net/chuhe163/article/details/124237803
在这里插入图片描述

  • 链式语法:加入@Accessors(chain = true)注解的方法,可以使用链式调用:

在这里插入图片描述

  • CompletableFuture中join()和get()的区别:
    join不报异常,get报异常
3.2.4 CompletableFuture实战:电商并行查询

代码:CompletableFuture/CompletableFutureMallDemo.java

3.2.5 CompletableFuture常用方法
  1. 获得结果和触发计算
public T get()								获取结果,有抛出异常
public T get(long timeout, TimeUnit unit)	设置一个超时时间,超时就直接抛出异常
public T join()								获取结果,不抛出异常
public T getNow(T valueIfAbsent)			计算完了就回正常值,没算完就返回valueIfAbsent

public boolean complete(T value)			调用即打断get方法并立即返回value
  1. 对计算结果进行处理
thenApply(f->{})							计算结果存在依赖关系,对这两个线程串行化,异常就停止在这一步
handle((f,e)->{})							同上,但有异常的话会继续向下走
  1. 对计算结果进行消费
thenAccept(f->{})								接收任务的处理结果并消费处理,无返回结果
thenRun(()->{})										任务A执行完执行任务B,且B不需要A的结果
  1. 对计算速度进行选用
A.applyToEither(B, f->{})						AB谁快,谁进f
  1. 对结果进行合并
A.thenCombine(B, (x,y)->{})						合并AB的结果

在这里插入图片描述

3.2.6 run 和 runAsync的区别

没有传入自定义线程池时,都使用默认线程池ForkJoinPool
如果执行第一个任务时,传入了一个自定义线程池:
调用thenRun方法执行第二个任务时,则第二个和第一个任务共用一个线程池
调用thenRunAsync执行第二个任务时,则第一个任务使用的是自己传入的线程池,第二个则会用默认线程池

4. synchronized和volatile的区别

https://zhuanlan.zhihu.com/p/61966479

可见性

要实现共享变量的可见性,必须保证两点:

  • 线程修改后的共享变量值能够及时从工作内存刷新到主内存中
  • 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中

可见性的实现方式:

  • synchronized
  • volatile

synchronized实现可见性

synchronized可以同时实现可见性原子性

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时,需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)

volatile实现可见性

volatile只能实现可见性,不能实现原子性
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
深入来说:通过加入内存屏障禁止重排序优化来实现的

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

线程写volatile变量的过程:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

线程读volatile变量的过程:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

5. 锁的了解

5.1 锁升级

https://www.jianshu.com/p/d61f294ac1a6
在这里插入图片描述

5.1.1 偏向锁

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

5.1.2 轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。

轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机将首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后将对象头中的 Mark Word 复制到锁记录中。

拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。

另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。

5.1.3 重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。

5.2 乐观锁与悲观锁

乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。
Java中的乐观锁CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作

悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读写数据都会认为其他线程会修改,所以每次读写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程block,直到这个线程释放锁然后其他线程获取到锁。
Java中的悲观锁synchronized修饰的方法和方法块、ReentrantLock

5.3 自旋锁

自旋锁是一种技术: 为了让线程等待,我们只须让线程执行一个忙循环(自旋)。

自旋锁的优点: 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给Java虚拟机的并发性能带来了很大的压力。

自旋锁的缺点: 占用处理器的时间,如果占用的时间很长,会白白消耗处理器资源,而不会做任何有价值的工作,带来性能的浪费。因此自旋等待的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。

自旋次数默认值:10次,可以使用参数-XX:PreBlockSpin来自行更改。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。

5.4 递归锁

任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。

再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。获取锁后,进行计数自增

释放锁:释放锁时,进行计数自减。

Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段。

可重入锁的作用: 避免死锁

5.5 读写锁

读写锁是一种技术: 通过ReentrantReadWriteLock类来实现。为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的。

读锁: 允许多个线程获取读锁,同时访问同一个资源。

写锁: 只允许一个线程获取写锁,不允许同时访问同一个资源。

Java中的读写锁:ReentrantReadWriteLock

5.6 公平锁与非公平锁

公平锁是一种思想: 多个线程按照申请锁的顺序来获取锁。申请的多个线程按队列来获取锁。
非公平锁是一种思想: 多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。

优点: 非公平锁的性能高于公平锁。

缺点: 有可能造成线程饥饿(某个线程很长一段时间获取不到锁)

Java中的非公平锁:synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的

5.7 分段锁

分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。ConcurrentHashMap原理:它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。

线程安全:ConcurrentHashMap 是一个 Segment 数组, Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全

5.8 锁粗化

锁粗化是一种优化技术: 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作都是出现在循环体体之中,就算真的没有线程竞争,频繁地进行互斥同步操作将会导致不必要的性能损耗,所以就采取了一种方案:把加锁的范围扩展(粗化)到整个操作序列的外部,这样加锁解锁的频率就会大大降低,从而减少了性能损耗。

5.9 synchronized

synchronized是Java中的关键字:用来修饰方法、对象实例,代码块。属于独占锁、悲观锁、可重入锁、非公平锁。

  1. 作用于实例方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是 Class类,相当于类的一个全局锁, 会锁所有调用该方法的线程;
  3. synchronized 作用于一个非 NULL的对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在代码块前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。

5.9.1 Lock和synchronized的区别

Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

  1. Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别

  2. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。

  3. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  4. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

  5. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  6. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

synchronized的优势:

足够清晰简单,只需要基础的同步功能时,用synchronized。
Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。

5.9.2 ReentrantLock 和synchronized的区别

ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

划重点

相同点:

1.主要解决共享变量如何安全访问的问题

2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,

3.保证了线程安全的两大特性:可见性、原子性。

不同点:

1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。

2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性

3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的

4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。

5.ReentrantLock 通过 Condition 可以绑定多个条件

1. Spring @Async 注解的使用以及原理

https://blog.csdn.net/qq_22076345/article/details/82194482
https://blog.csdn.net/qq_22076345/article/details/82229447
Spring中使用@Async注解标记的方法称为异步方法,会在调用方法线程之外开一个独立线程来运行这个方法。
@Async注解使用条件:

  1. 本注解一般用在类的方法上。如果用在类上,那么这个类的所有方法都会异步执行
  2. 调用异步方法的类上需要配置注解@EnableAsync
  3. 任意参数类型都是支持的,但是方法返回值必须是void或者Future类型。当使用Future时,你可以使用 实现了Future接口的ListenableFuture接口或者CompletableFuture类与异步任务做更好的交互。如果异步方法有返回值,没有使用Future类型的话,调用方获取不到返回值。

原理:(看不太懂)
@Async和@EnableAsync注解实现方法异步调用底层是通过AOP和线程池实现的。

2. Spring如何实现事务?

Spring框架提供了两种事务管理方式:声明式事务和编程式事务。

声明式事务
声明式事务管理只需要用到@Transactional 注解和@EnableTransactionManagement。它是基于 Spring AOP 实现的,并且通过注解实现,实现起来简单,对原有代码没有入侵性。
在声明式事务中,我们只需要在配置文件中指定事务的类型、传播行为、超时时间等属性,Spring框架会在运行时自动为我们创建和管理事务。这种方式的优点是简单、易用,可以避免代码中出现大量的事务管理代码,但缺点是不够灵活,只能在配置文件中进行事务的配置。

编程式事务
编程式事务是通过在代码中编写事务管理代码来实现的。在编程式事务中,我们需要在代码中显式地开启、提交或回滚事务,并且需要手动处理事务的异常。这种方式的优点是灵活、可控,可以根据具体的业务需求进行事务管理,但缺点是代码复杂度高,容易出错,并且不易维护。
通过PlatformTransactionManager控制事务
通过TransactionTemplate控制事务

总的来说,声明式事务适合简单的事务场景,而编程式事务适合复杂的事务场景。在实际应用中,我们可以根据具体的业务需求选择合适的事务管理方式。

1. JVM调优有什么方法?

这里有一篇讲JVM调优很不错的文章:
https://www.cnblogs.com/three-fighter/p/14644152.html

1.1 调优步骤

JVM调优应该是性能优化的最后一颗子弹!性能问题一般第一选择是优化应用层。
调优步骤:

  • 分析系统运行状况,GC日志及dump文件,确定问题瓶颈
  • 根据调优内存,吞吐量等指标来确定JVM调优量化目标
  • 确定JVM调优参数
  • 对比调优前后差异,不断分析调整,找到最合适的参数配置(迭代完成)

1.2 调优工具

  • JDK工具
    在这里插入图片描述
  • Linux工具
    在这里插入图片描述

1.3 常用策略

  1. 选择合适的垃圾收集器
    CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
    CPU多核,关注吞吐量 ,那么选择PS+PO组合。
    CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。
    CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
 //设置Serial垃圾收集器(新生代)
 开启:-XX:+UseSerialGC
 ​
 //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
 开启 -XX:+UseParallelOldGC
 ​
 //CMS垃圾收集器(老年代)
 开启 -XX:+UseConcMarkSweepGC
 ​
 //设置G1垃圾收集器
 开启 -XX:+UseG1GC
  1. 调整内存大小
    垃圾收集非常频繁。如果内存太小,就需要频繁垃圾收集来清空堆。
 //设置堆初始值
 指令1:-Xms2g
 指令2:-XX:InitialHeapSize=2048m
 ​
 //设置堆区最大值
 指令1:`-Xmx2g` 
 指令2: -XX:MaxHeapSize=2048m
 ​
 //新生代内存配置
 指令1:-Xmn512m
 指令2:-XX:MaxNewSize=512m
  1. 设置符合预期的停顿时间
    程序间歇性卡顿:如果没有设置确切的停顿时间,GC会以吞吐量为主,收集时间点就不稳定
 //GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis 
  1. 调整内存区域大小比例
    如果JVM某一区域的GC频繁,其他区域正常的话,就考虑调整对应区域的大小比例
 //survivor区和Eden区大小比率
 指令:-XX:SurvivorRatio=6  //S区和Eden区占新生代比率为1:6,两个S区2:6
 ​
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
  1. 调整对象升老年代的年龄
    如果老年代频繁GC,可能是因为升代年龄小,老年代对象变多导致GC频繁
//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7
 -XX:InitialTenuringThreshol=7 
  1. 调整GC触发时机
    若CMS,G1经常Full GC,卡顿严重。G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction
 ​
 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65 
  1. 调整JVM本地内存大小
    GC时间,次数和堆内存空间都充足,但报OOM?可能是因为本地内存溢出。本地内存溢出不会报OOM,这块内存会随着堆来GC回收。这时可以调整本地内存大小来解决问题
 XX:MaxDirectMemorySize

1.4 具体案例

请见链接,这里不详细写了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值