Java总结2


IO,NIO,AIO

IO介绍

​ IO是基于流模型出现的输入输出流,比如操作文件时用输入输出流读取文件和写入文件。

​ 传统IO是BIO(Block-IO)传统阻塞IO。

  • 字节流:InputStream,OutputStream
  • 字符流:Reader,Writer

java流

  1. Writer的使用
Writer writer = new FileWriter("D:\\培训\\test.txt", true);    //true表示追加,false表示覆盖
writer.append("方芳芳");
writer.close();
  1. Reader的使用
Reader reader = new FileReader("D:\\\\培训\\\\test.txt");
BufferedReader bufferedReader = new BufferedReader(reader);
String str = null;

while((str = bufferedReader.readLine()) != null)
{
    System.out.println(str);
}
bufferedReader.close();
reader.close();
  1. InputStream的使用
InputStream inputStream = new FileInputStream(new File("D:\\培训\\test.txt"));
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String str = new String(bytes);
System.out.println(str);
inputStream.close();
  1. OutputStream的使用
OutputStream outputStream = new FileOutputStream("D:\\培训\\test.txt",true);
outputStream.write("老王".getBytes());
outputStream.close();

NIO介绍

​ Java1.4出现了java.nio包,NIO(Non-Blocking IO)同步非阻塞IO。它提供了Channel、Selector、Buffer等概念,可以实现多路复用和同步非阻塞IO操作,提高了IO操作的性能。

组合方 式 性能分析
同步阻塞 最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态
同步非阻塞 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上
异步阻塞 这种方式在分布式数据库中经常用到。例如,在往一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其他机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O;异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况
异步非阻塞 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。例如,Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高

AIO介绍

​ AIO是NIO的升级,称为异步非阻塞IO,异步IO的操作基于事件和回调机制。


IO,NIO,AIO的区别?

  • BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,因此人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

​ 简单来说 BIO 就是传统 IO 包,产生的最早;NIO 是对 BIO 的改进提供了多路复用的同步非阻塞 IO,而 AIO 是 NIO 的升级,提供了异步非阻塞 IO。

文件的读写

// 读取文件
byte[] bytes = Files.readAllBytes(Paths.get("d:\\io.txt"));
// 写入文件
Files.write(Paths.get("d:\\io.txt"), "追加内容".getBytes(), StandardOpenOption.APPEND);

Files的常见操作?

​ Files是Java1.7提供的,使得文件和文件夹的操作更加方便。常用方法如下:

  • Files. exists():检测文件路径是否存在
  • Files. createFile():创建文件
  • Files. createDirectory():创建文件夹
  • Files. delete():删除一个文件或目录
  • Files. copy():复制文件
  • Files. move():移动文件
  • Files. size():查看文件个数
  • Files. read():读取文件
  • Files. write():写入文件

不定项选择:为了提高读写性能,可以采用什么流?

A:InputStream
B:DataInputStream
C:BufferedReader
D:BufferedInputStream
E:OutputStream
F:BufferedOutputStream

答:D、F

​ 题目解析:BufferedInputStream 是一种带缓存区的输入流,在读取字节数据时可以从底层流中一次性读取多个字节到缓存区,而不必每次都调用系统底层;同理,BufferedOutputStream 也是一种带缓冲区的输出流,通过缓冲区输出流,应用程序先把字节写入缓冲区,缓存区满后再调用操作系统底层,从而提高系统性能,而不必每次都去调用系统底层方法。

FileInputStream和BufferedInputStream的区别?

​ FileInputStream读小文件性能更好,BufferedInputStream进行大文件操作更有优势。

反射和JDK动态代理技术

反射

​ 通过反射,可以在程序运行期间获取,检测,调用对象的属性和方法。

反射的使用场景:

  1. 框架
  2. 数据库连接池

​ 反射获取调用类可以通过 Class.forName(),反射获取类实例要通过 newInstance(),相当于 new 一个新对象,反射获取方法要通过 getMethod(),获取到类方法之后使用 invoke() 对类方法进行调用。如果是类方法为私有方法的话,则需要通过 setAccessible(true) 来修改方法的访问限制,以上的这些操作就是反射的基本使用。

动态代理技术

​ 动态代理技术可以理解为本可以自己做的事情,交给别人去做。

动态代理的使用场景:

  1. 面向切面编程AOP
  2. 封装一些AOP调用,也可以通过代理实现一个全局拦截器。

JDK动态代理技术

interface Animal {
    void eat();
}
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("The dog is eating");
    }
}
class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("The cat is eating");
    }
}

// JDK 代理类
class AnimalProxy implements InvocationHandler {
    private Object target; // 代理对象
    public Object getInstance(Object target) {
        this.target = target;
        // 取得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("调用前");
        Object result = method.invoke(target, args); // 方法调用
        System.out.println("调用后");
        return result;
    }
}

public static void main(String[] args) {
    // JDK 动态代理调用
    AnimalProxy proxy = new AnimalProxy();
    Animal dogProxy = (Animal) proxy.getInstance(new Dog());
    dogProxy.eat();
}

JKD动态代理技术只能代理实现接口的类。

cglib字节码增强技术

class Panda {
    public void eat() {
        System.out.println("The panda is eating");
    }
}
class CglibProxy implements MethodInterceptor {
    private Object target; // 代理对象
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        // 设置父类为实例类
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("调用前");
        Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
        System.out.println("调用后");
        return result;
    }
}
public static void main(String[] args) {
    // cglib 动态代理调用
    CglibProxy proxy = new CglibProxy();
    Panda panda = (Panda)proxy.getInstance(new Panda());
    panda.eat();
}

​ 由以上代码可以知道,cglib 的调用通过实现 MethodInterceptor 接口的 intercept 方法,调用 invokeSuper 进行动态代理的。它可以直接对普通类进行动态代理,并不需要像 JDK 代理那样,需要通过接口来完成,值得一提的是 Spring 的动态代理也是通过 cglib 实现的。

注意:cglib 底层是通过子类继承被代理对象的方式实现动态代理的,因此代理类不能是最终类(final),否则就会报错 java.lang.IllegalArgumentException: Cannot subclass final class xxx。


动态代理解决了什么问题?

​ 实现代码之间的解耦。

动态代理技术和反射机制的联系?

​ 反射机制可以实现动态代理技术,但是动态代理技术不仅可以通过反射机制实现,也可以通过cglib字节码增强技术,ASM(一个短小精悍的字节码操作框架)等实现。

以下描述错误的是?

A:cglib 的性能更高
B:Spring 中有使用 cglib 来实现动态代理
C:Spring 中有使用 JDK 原生的动态代理
D:JDK 原生动态代理性能更高

选D。 JDK动态代理技术的性能比字节码增强技术的性能低,Spring动态代理的实现方式有两种:JDK动态代理技术和字节码增强技术。

cglib可以代理所有类吗?

​ cglib不能代理final修饰的类,因为cglib需要继承被代理类。

JDK动态代理技术和cglib的区别?

​ JDK动态代理技术:被代理类必须实现接口,不需要任何依赖,可以平滑的支持JDK版本的升级。

​ cglib:被代理类不需要实现接口,需要添加依赖,性能更高。

为什么JDK动态代理技术必须实现接口?

​ 因为JDK动态代理技术的实现方法newProxyInstance()里有两个参数,一个是类加载器,另一个是实现的接口列表,所以必须实现接口才可以。

线程

线程

​ 线程是程序运行的执行单元,依托于进程存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源,更加轻量化。

进程

​ 进程是程序的一次动态执行,是系统进行资源分配和调度的基本单位,是操作系统运行的基础,通常每一个进程都拥有自己独立的内存空间和系统资源。简单来说,进程可以被当做是一个正在运行的程序。

为什么需要线程?

​ 因为程序的运行必须依靠进程,线程又是进程的基本执行单元。

多线程?

​ 提高程序的执行性能。

线程的创建?

  1. 继承Thread,重写run()方法
  2. 实现Runnable,实现run()方法
  3. 实现Callable,实现run()方法
Callable:
        MyTaskByCallable task = new MyTaskByCallable();
        FutureTask<String> futureTask = new FutureTask<String>(task);
        FutureTask<String> futureTask1 = new FutureTask<String>(task);
        Thread thread = new Thread(futureTask);
        Thread thread1 = new Thread(futureTask1);
        thread.start();
        thread1.start();
        System.out.println(futureTask.get());

wait()

​ 使用wait()实现线程等待。

​ 当使用wait()时,必须持有当前对象的锁,否则会返回异常。

join()

​ 当前线程执行完成之后才能继续执行下面的语句。

yiled()

​ 线程让步,交出CPU的执行权。yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。

interrupted()

​ 中断线程

线程优先级

​ setPriority()设置线程优先级,1—10。

死锁

死锁

​ 死锁是指两个或两个以上的线程,由于竞争资源或者由于彼此通信造成的一种阻塞现象,如果无外力介入,它们都无法推进下去。

​ 线程A独占锁1,线程B独占锁2,当线程A尝试去获取锁2,线程B尝试去获取锁1时,由于两个线程各自占有对方的锁,就会一直阻塞下去。


线程和进程的区别?

​ 从本质上来说,线程是进程的实际执行单元,一个程序至少有一个进程,一个进程至少有一个线程,它们的区别主要体现在以下几个方面:

  • 进程间是独立的,不能共享内存空间和上下文,而线程可以;
  • 进程是程序的一次执行,线程是进程中执行的一段程序片段;
  • 线程占用的资源比进程少。

线程的常用方法?

  • currentThread():返回当前正在执行的线程引用
  • getName():返回此线程的名称
  • setPriority()/getPriority():设置和返回此线程的优先级
  • isAlive():检测此线程是否处于活动状态,活动状态指的是程序处于正在运行或准备运行的状态
  • sleep():使线程休眠
  • join():等待线程执行完成
  • yield():让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态
  • interrupted():是线程处于中断的状态,但不能真正中断线程

wait()和sleep()的区别?

​ wait() 和 sleep() 的区别主要体现在以下三个方面。

  • 存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
  • 释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll() 直接唤醒。

守护线程:

​ 守护线程的优先级是最低的,它通常为其他线程提供服务,通过setDaemon方法设置。例如JVM中的垃圾回线程。

线程的状态?

  • NEW:尚未启动
  • RUNNABLE:正在执行中
  • BLOCKED:阻塞(被同步锁或者 IO 锁阻塞)
  • WAITING:永久等待状态
  • TIMED_WAITING:等待指定的时间重新被唤醒的状态
  • TERMINATED:执行完成

start()和run()的区别?

​ start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

产生死锁的条件?

  • 互斥条件:一个资源每次只能被一个线程使用;
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺;
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系;

预防死锁?

  • 尽量使用 tryLock(long timeout, TimeUnit unit) 的方法 (ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁;
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁;
  • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁;
  • 尽量减少同步的代码块。

thread.wait()和thread.wait(0)

​ 两个方法效果相同,但是wait()方法内部是调用wait(0)方法。wait()方法表示进入等待状态,让出当前的锁和CPU资源,并且只有其他线程执行notify()和notifyall()方法才能唤醒该线程。

线程的调度策略?

​ 线程调度器选择优先级最高的线程运行,但是如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了 yield() 方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep() 方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

总结

​ 程序的运行依靠的是进程,而进程的执行依靠的是多个线程,多线程之间可以共享一块内存和一组系统资源,而多进程间通常是相互独立的。线程的创建有三种方式:继承 Thread 重写 run 方法,实现 Runnable 或 Callable 接口,其中 Callable 可以允许线程的执行有返回值,JDK 8 中也可以使用 Lambda 来更加方便的使用线程,线程是有优先级的,优先级从 1-10 ,数字越大优先级越高,也越早被执行。如果两个线程各自拥有一把锁的同时,又同时等待获取对方的锁,就会造成死锁。可以降低锁的粒度或减少同步代码块的范围或使用 Java 提供的安全类,来防止死锁的产生。

线程池之ThreadPoolExecutor

​ 线程池:把一个或多个线程通过统一的方式调度和重复使用的技术,减少性能的损耗。

为什么使用线程池?

  • 可重复使用已有线程,避免对象创建、消亡和过度切换的性能开销。

  • 避免创建大量同类线程所导致的资源过度竞争和内存溢出的问题。

  • 支持更多功能,比如延迟任务线程池(newScheduledThreadPool)和缓存线程池(newCachedThreadPool)等。

    ​ 在Java中,线程池的概念是Executor这个接口,具体实现类为ThreadPoolExcutor。

ThreadPoolExecutor的参数?

  1. int corePoolSize:该线程池中核心线程数的最大值

    核心线程:线程池新建线程的时候,如果当前线程数小于核心线程数,那么会创建核心线程。如果超过corePoolSize,那么会创建非核心线程。核心线程默认情况下会一直存活在线程池中,即使它什么也不做。

    ​ 如果指定allowCoreThreadTimeOut的属性为true,那么核心线程如果闲置状态的话,超过一定时间就会被销毁。

  2. int maximamPoolSize:该线程池中线程总数的最大值。

    ​ 线程总数=核心线程数+非核心线程数

  3. long keepAliveTime:非核心线程的闲置存活时间

    ​ 一个非核心线程,如果闲置超过该时间,该线程就会被销毁。如果allowCoreThreadTimeOut设置为true,那么该时间也会作用在核心线程上。

  4. TimeUnit unit:闲置存活时间的单位

    ​ TimeUnit是一个枚举类型。微毫秒,微秒,毫秒,s,min,hour,days

  5. BlockingQueue workQueueu:该线程池中的任务队列:维护着等待执行的Runnable对象

    ​ 当所有的核心线程都在运作时,新添加的任务会添加到这个队列,如果队列满了,则新建非核心线程去执行任务。常见的workQueue类型:

    SynchronousQueue:该队列会直接提交收到的任务,所以为了保证线程数不能达到maximumPoolSize,一般会将该属性设置为Integer.MAX_VALUE。

    LinkedBlockingQueue:接受到任务时,先判断核心线程数,创建或者把任务放到队列中,因为该队列是无限大的,所以会导致maximumPoolSize失效。

    ArrayBlockingQueue:可以限定队列的长度,当接受到任务时,判断核心线程数,创建,或者把任务放入队列,如果队列满了,创建非核心线程执行任务,如果队列满了,线程总数也满了,则会发生错误。

    DelayQueue:队列内的元素必须实现Delay接口,所以当一个任务传入队列时,只有达到了指定的时间,才会被执行。

  6. ThreadFactory:为线程池创建一个线程工厂。

  7. rejectedExecutorHandler

    ​ 线程池中任务队列超过最大值的拒绝策略,rejectedExecutorHandler是一个接口,它里面只有一个方法,可以在这个方法中添加超过队列最大值的处理事件。

    ​ ThreadPoolExecutor中提供了四种默认的拒绝策略:

  • new ThreadPoolExecutor.DiscardPolicy():丢弃掉该任务,不进行处理
  • new ThreadPoolExecutor.DiscardOldestPolicy():丢弃队列里最近的一个任务,并执行当前任务
  • new ThreadPoolExecutor.AbortPolicy():直接抛出 RejectedExecutionException 异常
  • new ThreadPoolExecutor.CallerRunsPolicy():既不抛弃任务也不抛出异常,直接使用主线程来执行此任务

向线程池添加任务

​ ThreadPoolExecutor.execute(Runnable task)提交任务

ThreadPoolExecutor的策略

​ 当一个任务被添加进线程池时:

  1. 线程数量未达到核心线程数,创建核心线程执行任务。
  2. 达到核心线程数,添加进任务队列。
  3. 队列已满,新建线程(非核心线程)执行任务。
  4. 队列已满,线程数也满,根据拒绝策略调用逻辑。

常见的四种线程池?

​ Java通过Executors提供了四种核心线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的。

  • 可缓存线程池CachedThreadPool()
public static ExecutorService newCachedThreadPool() &#123;
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    &#125;
  1. 没有核心线程,线程数无限制。
  2. 在创建任务时,若有空闲线程,直接复用,若无,则创建线程。
  3. 闲置状态的线程超过60秒,就会被移除。
  • 定长线程池FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) &#123;
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
&#125;
  1. 核心线程数等于总线程数,所以默认情况下,线程不会销毁。
  2. 提交了任务,如果有闲置线程,也不会复用,会创建一个新线程。之后会创建线程,添加任务到队列中。
  • 单线程池SingleThreadPool
public static ExecutorService newSingleThreadExecutor() &#123;
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
&#125;
  1. 有且只有一个线程执行任务。
  2. 所有任务按照顺序执行,遵循队列的入队出队规则。
  • 延时线程池ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) &#123;
    return new ScheduledThreadPoolExecutor(corePoolSize);
&#125;
//ScheduledThreadPoolExecutor():public ScheduledThreadPoolExecutor(int corePoolSize) &#123;
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
&#125;

​ DEFAULT_KEEPALIVE_MILLIS默认为10秒。

  1. 不仅设置了核心线程数,还设置了最大线程数为Integer.MAX_VALUE。
  2. 唯一一个可以延时执行和周期执行任务的线程池。
        /*
         * 延时3s后执行任务
         */
        pool4.schedule(new Runnable() &#123;

            @Override
            public void run() &#123;
                System.out.println(LocalDateTime.now());
            &#125;
        &#125;, 3, TimeUnit.SECONDS);
        /*
         * 延时3s后执行任务,当任务开始执行的时候过7s再次执行
         */
        pool4.scheduleAtFixedRate(new Runnable() &#123;

            @Override
            public void run() &#123;
                System.out.println(LocalDateTime.now());

            &#125;
        &#125;, 3, 7, TimeUnit.SECONDS);
        /*
         * 延时3s后执行任务,当任务结束后,过7s再次执行
         */
        pool4.scheduleWithFixedDelay(new Runnable() &#123;

            @Override
            public void run() &#123;
                System.out.println(LocalDateTime.now());
            &#125;
        &#125;, 3, 7, TimeUnit.SECONDS);
  • 单线程延时线程池SingleThreadScheduledExecutor

    ​ 其实就是一个单线程的延时线程池

  • 并行线程池WorkStealingPool

    ​ Java8新增的线程池,如果不指定任

    何参数,则以当前机器处理器个数作为线程个数,此线程池并行处理任务,不保证执行顺序。

ThreadFactory就是一个接口,用来创建线程。


submit()和execute()

  1. execute()传入的是Runnable,submit()传入的是Callable
  2. execute不能接受返回值,submit可以接收返回值。

线程池关闭

  • shutdown():不会立刻中止线程池,需要等所有任务队列中的任务都执行完才会中止。
  • shutdownNow():立刻中止线程池,线程池的状态变为STOP状态,并停止所有正在执行的任务,返回未执行的任务。

ThreadPoolExecutor的方法

  • submit()/execute():执行线程池
  • shutdown()/shutdownNow():终止线程池
  • isShutdown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取核心线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的任务队列
  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

ThreadPoolExecutor VS Executors

​ ThreadPoolExecutor是传统的创建线程池的方式 ,Executors提供了更多类型的线程池类型。但是不推荐使用Executors创建线程池。

​ 使用传统的方式可以让读者更加明确线程池的运行规则,规避资源耗尽的风险。

​ Executors的弊端如下:

  • 定长线程池和单线程线程池,它俩的队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
  • 可缓存线程池和延时线程池,它俩运行创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,造成OOM

单线程池的意义?

​ 单线程线程池提供了队列功能,如果有多个任务会排队执行,可以保证任务执行的顺序性。单线程线程池也可以重复利用已有线程,减低系统创建和销毁线程的性能开销。

总结

​ Executors 可以创建 6 种不同类型的线程池,其中 newFixedThreadPool() 适合执行单位时间内固定的任务数,newCachedThreadPool() 适合短时间内处理大量任务,newSingleThreadExecutor() 和 newSingleThreadScheduledExecutor() 为单线程线程池,而 newSingleThreadScheduledExecutor() 可以执行周期性的任务,是 newScheduledThreadPool(n) 的单线程版本,而 newWorkStealingPool() 为 JDK 8 新增的并发线程池,可以根据当前电脑的 CPU 处理数量生成对比数量的线程池,但它的执行为并发执行不能保证任务的执行顺序。

ThreadLocal

​ ThreadLocal用于解决多线程之间的数据隔离问题。也就是说ThreadLocal会为每一个线程创建一个单独的变量副本。

ThreadLocal的应用场景

  • 用来管理Session,因为每个人的信息是不一样的。

  • 数据库连接,为每一个进程分配一个独立的资源。

  • 还被用于Spring的事务管理器中

ThreadLocal的内存溢出原理

​ ThreadLocal并不存储数据,而是依靠ThreadLocalMap存储数据,ThreadLocalMap中有一个Entry数组,数组通过KV的形式对数据进行存储,其中K就是ThreadLocal本身,V就是要存储的值。

​ 如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于 ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 key 为 null 的 Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。

​ 每次使用完ThreadLocal之后,调用remove方法移除无用的数据。


ThreadLocal为什么是线程安全的?

​ ThreadLocal为每一个维护变量的副本,把共享数据的可见范围限制在同一个线程之内,因此ThreadLocal是线程安全的,每个线程都有属于自己的变量。

如何共享数据?

​ 使用ThreadLocal的子类InheritableThreadLocal可以天然的支持多线程间的信息共享。

ThreadLocal和Synchonized的区别?

​ ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突,但是 ThreadLocal 与 Synchronized 有本质的区别,Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时刻只能被一个线程访问,是一种 “以时间换空间” 的方式;而 ThreadLocal 为每一个线程提供了独立的变量副本,这样每个线程的(变量)操作都是相互隔离的,这是一种 “以空间换时间” 的方式。

线程安全

synchronized

​ synchronized是Java提供的同步机制,当一个线程正在执行synchronized修饰的代码块时,其他线程只能阻塞等待该线程执行完才能继续执行。

​ synchronized的使用:

// 修饰代码块
synchronized (this) &#123;
    // do something
&#125;
// 修饰方法
synchronized void method() &#123;
    // do something
&#125;

​ 实现原理:

​ 本质是通过进入和退出的Monitor对象来实现线程安全的。

​ JVM采用monitorenter和monitorexit两个指令实现同步,一个加锁,一个释放锁。

ReentrantLock使用

Lock lock = new ReentrantLock();
lock.lock();
lock.unlock();

尝试获取锁

lock.tryLock();  //返回Boolean
lock.tryLock(long timeout,TimeUnit unit);  //尝试一段时间内获取锁

ReentrantLock的注意事项

​ 使用Lock必须记得释放锁,不然该锁会被永久占用。


ReentrantLock的常用方法

  • lock():用于获取锁
  • unlock():用于释放锁
  • tryLock():尝试获取锁
  • getHoldCount():查询当前线程执行 lock() 方法的次数
  • getQueueLength():返回正在排队等待获取此锁的线程数
  • isFair():该锁是否为公平锁

ReentrantLock的优势

​ ReentrantLock具备非阻塞方式获取锁的特性,使用tryLock()方法,使用tryLock(Time,Unit)方法还可以获取一段时间内的锁。ReentrantLock还可以中断获得的锁,使用lockInterruptibly()方法,当获取锁之后,如果线程被中断,则会抛出异常并释放当前获得的锁。

创建公平锁

​ new ReentrantLock(true)

公平锁和非公平锁

​ 公平锁指线程获取锁是按照加锁顺序来的,非公平锁指强锁机制,先lock()的线程不一定先获取锁。

ReentrantLock 中 lock() 和 lockInterruptibly() 有什么区别?

​ lock() 和 lockInterruptibly() 的区别在于获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

synchronized和ReentrantLock的区别

  • ReentrantLock使用起来比较灵活,但是必须有释放锁的配合动作。

  • ReentrantLock必须手动加锁,释放锁。synchronized不需要手动开启和释放锁。

  • ReentrantLock只适用于代码块锁,而synchronized可用于修饰方法,代码块等;

  • ReentrantLock的性能略高于synchronized。

synchornized如何实现锁升级?

​ 在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

并发包中的高级同步工具

java.utils.concurrent (JUC)

  • 提供了线程池的创建类ThreadPoolExecutor,Executors等;
  • 提供了各种锁,如Lock,ReentrantLock等;
  • 提供了各种线程安全的数据结构,如ConcurrentHashMap,LinkedBlockingQueue等;
  • 提供了更加高级的线程同步结构,如CountDownLatch、CyclicBarrier、Semaphore 等。

CountDownLatch

​ CountDownLatch(闭锁)可以看作一个只能做减法的计数器,可以让一个或多个线程等待执行。

​ 两个重要的方法:

  • countDown():使计数器减1;
  • await():当计数器不为0时,则调用该方法的线程阻塞,当计数器为0时,唤醒等待的一个或多个线程。

CyclicBarrier

​ CyclicBarrier(循环屏障)通过它可以实现一组线程等待满足某个条件后同时进行。

​ 它的构造方法为 CyclicBarrier(int parties,Runnable barrierAction) 其中,parties 表示有几个线程来参与等待,barrierAction 表示满足条件之后触发的方法。CyclicBarrier 使用 await() 方法来标识当前线程已到达屏障点,然后被阻塞。

**Semaphore **

​ Semaphore (信号量)用于管理多线程中控制资源的访问与使用。

​ Semaphore 就好比停车场的门卫,可以控制车位的使用资源。比如来了 5 辆车,只有 2 个车位,门卫可以先放两辆车进去,等有车出来之后,再让后面的车进入。

Phaser

​ Phaser(移加器)是JDK 7 提供的,它的功能是等待所有线程到达之后,才继续或者开始进行新的一组任务。

锁和CAS

乐观锁和悲观锁

​ 乐观锁和悲观锁不是具体的“锁”,而是一种并发编程的基本概念。最早出现在数据库的设计中,后来逐渐倍Java的并发包所引入。

  • 悲观锁

    ​ 悲观锁认为对于同一个数据的并发操作,一定会发生修改,哪怕没有被修改,也会认为被修改了。因此对于同一个数据的并发操作,悲观锁采取加锁的形式,悲观的认为,不加锁的并发操作一定会出问题。

  • 乐观锁

    ​ 乐观锁在获取数据时,并不担心数据被修改,每次获取数据时也不会加锁,只是在更新时,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改,则不进行数据更新,如果被其他线程修改则进行数据更新。

公平锁和非公平锁

​ 根据线程获取锁的抢占机制,分为公平锁和非公平锁。

  • 公平锁

    ​ 公平锁是指多个线程按照申请锁的顺序来获取锁。

  • 非公平锁

    ​ 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,而是通过抢占的方式去获取锁。

    ​ ReentrantLock提供了创建两种锁的方式:

    new ReentrantLock(true/false); // 公平锁/非公平锁

    ​ 如果构造函数不传入参数,默认为非公平锁。

独占锁和共享锁

​ 根据锁能否被多个线程持有,可以分为独占锁和共享锁。

  • 独占锁

    ​ 独占锁是指任何时候都只能有一个线程能执行资源操作。

  • 共享锁

    ​ 共享锁是指可以同时被多个线程读取,但只能被一个线程修改。比如ReentrantReadWriteLock就是共享锁的实现方式,它允许一个线程进行写操作,其他线程进行读操作。

可重入锁

​ 可重入锁指的是该线程获取了该锁之后,可以无限次的进入该锁锁住的代码。

自旋锁

​ 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少线程上下文切换的损耗,缺点是会消耗CPU。

偏向锁

​ 偏向锁会偏向于第一个获得它的线程。为了让线程获得锁的代价更低。

MySQL间隙锁

​ 间隙锁是一个在索引记录之间的间隙上的锁。是innodb在可重复度提交下为了解决幻读问题引入的锁机制。它是为了保证在某个间隙下的数据在锁定情况下不会发生任何变化。

CAS和ABA

​ CAS(Compare and Swap)比较并交换,是一种乐观锁的实现,是用非阻塞算法来代替锁定,java.utils.concurrent包下的AtomicInteger 就是借助CAS来实现的。

​ 著名的ABA问题就是CAS引起的。

​ 线程A查询过一个数据为100,线程B在线程A下一次查询之前+50 -50,线程A查询结果还是100,觉得数据没有改变。

​ ABA问题描述:

  • 线程一:取款,获取原值 200 元,与 200 元比对成功,减去 100 元,修改结果为 100 元。

  • 线程二:取款,获取原值 200 元,阻塞等待修改。

  • 线程三:转账,获取原值 100 元,与 100 元比对成功,加上 100 元,修改结果为 200 元。

  • 线程二:取款,恢复执行,原值为 200 元,与 200 元对比成功,减去 100 元,修改结果为 100 元。

    ​ 常见解决ABA的方式:加版本号,来区分值是否有变动。

    ​ Java 1.5 提供了AtomicStampedReference原子引用变量,通过添加版本号解决ABA问题。


synchronized是哪种锁的实现?为什么?

​ synchronized是悲观锁的实现。因为synchronized修饰的代码,每次执行时都会进行加锁操作,同时只允许一个线程进行操作。

synchronized是公平锁还是非公平锁?

​ synchronized使用的是非公平锁,并且不可设置。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择。

为什么非公平锁的吞吐量大于公平锁?

​ 比如A占用锁,B等待被唤醒,这时C来申请锁,如果是公平锁C就要排在B后面,但是非公平锁可以让C先用,当B被唤醒之前C就已近使用完成了,从而减少了C等待和唤醒之间的消耗。

volatile的作用?

​ volatile是JVM提供的最轻量级同步机制。

​ 当变量被定义成volatile之后,具备两种特性:

  • 保证此变量对所有线程的可见性,当一条线程修改了这个变量的值,改的新值对于其他线程是可见的。(可以立即得知)

  • 禁止指令重排序优化,普通变量仅仅能保证在该方法执行过程中正确结果,但是不保证程序代码的执行顺序。

volatile和synchronized的区别

​ synchronized既能保证可见性,又能保证原子性。而volatile只能保证可见性,无法保证原子性。比如i++是volatile修饰会有线程安全的问题。

CAS如何实现?

​ 通过Java Native Interface的代码实现,比如在windows系统CAS就是借助C语言来调用CPU底层指令实现的。


文章作者: kilig
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kilig !
 上一篇
Java总结3 Java总结3
Spring 核心功能​ 发展过程: Spring1.x ​ 此版本为了解决企业应用程序复杂性而创建的,当时J2EE应用的经典架构为分层架构:表现层,业务层,持久层。最流行的组合是SSH(Structs,Spr
下一篇 
Java总结 Java总结
Java程序是怎么运行的?Q/A: JDK和Java的区别? ​ JDK是Java开发工具包,里面包括:JRE(Java运行环境),JVM(Java虚拟机)等。 ​ Java是一种开发语言。 Q/A: Ja
  目录