多线程的创建

​ 在 Java 中,创建多线程有四种方式,分别是 继承Thread类实现Runnable接口实现Callable接口, 使用线程池.

  1. 继承Thread类: 创建一个继承了Thread类的子类,并重写其run方法来定义线程执行的任务。
  2. 实现Runnable接口: 创建一个实现了Runnable接口的类,并实现其run方法。然后在主类中创建该类的实例,并将其作为参数传递给Thread 对象。
  3. 实现Callable接口:创建一个实现了Callable接口的类,并实现其call方法。在主类中创建该类的对象,然后将其作为参数传递给FutureTask对象(FutureTask类管理多线程运行的结果),最后将 ft 作为参数传递给Thread 对象。未来可以通过ft.get()获得该线程运行的结果。
  4. 使用线程池:通过使用Executors类创建线程池,并通过线程池来管理线程的创建和复用。

继承Thread类

实现Runnable接口

使用Callable接口

输出结果是5050

对比

线程 start 和 run 的区别

在Java多线程中,run 方法和 start 方法的区别在于:

  1. run 方法包含线程要执行的代码,当直接调用 run 方法时,它会在当前线程的上下文中执行,而不会创建新的线程。
  2. start 方法用于启动一个新的线程,调度器会在新线程中自动执行 run 方法的代码。

​ 所以如果需要实现多线程执行,则应该调用 start 方法来启动新线程。

Java中有哪些锁

口诀:克功悲独户,自请分

可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。

  1. 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。 Java ReentrantLockSynchronized 是非公平锁。
  2. 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 Java ReentrantLockSynchronized 是可重入锁。
  3. 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。 Java ReentrantLockSynchronized 是独享锁。但是对于Lock的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
  4. 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是 ReentrantLock。读写锁在Java中的具体实现就是 ReadWriteLock
  5. 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。
  6. 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
  7. 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对 Synchronized 。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  8. 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。

synchronized

synchronized是Java中的一个关键字,用于实现同步和线程安全。

  • 当一个方法或代码块被 synchronized 修饰时,它将成为一个临界区,同一时刻只能由一个线程访问。确保多个线程在访问共享资源时不会产生冲突。
  • 当线程通过 synchronized 等待锁时是不能被 Thread.interrupt() 中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁。
  • synchronized 实现的机理依赖于软件层面上的JVM,所以实现方便,可靠性高;其性能会随着Java版本的不断升级而提高,比如说 java5 引入偏向锁/轻量级锁/重量级锁等锁机制。

synchronized和lock的区别是什么

可以结合起来说

synchronizedLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而Lock是一个接口,是Java提供的显式锁机制,需要手动获取和释放锁,通过实现类(如ReentrantLock)来创建锁对象,然后主动调用锁的获取和释放方法。

  1. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断,一旦获取不到锁就会一直等待;也没有公平性的概念,线程调度由JVM控制。
    • lock:提供了更多的灵活性,例如可以尝试获取锁,如果锁已被其他线程持有,可以选择等待或者中断等待。提供了超时获取锁的能力,可以在指定时间内尝试获取锁,也可以设置为公平锁,按照请求锁的顺序来获取锁。
  2. 等待与通知:

    • synchronized:与 wait()notify()/notifyAll() 方法一起使用,用于线程的等待和通知。
    • lock:可以与 Condition 接口结合,实现更细粒度的线程等待和通知机制。

synchronized和ReentrantLock的区别是什么

  1. synchronizedReentrantLock都是Java中用于实现线程同步的手段,synchronized是Java的关键字,基于JVM的内置锁实现,可以用于修饰方法或代码块,使用起来比较简单直接。而ReentrantLockjava.util.concurrent.locks包中的一个锁实现,需要显式创建,并通过调用lock()unlock()方法来管理锁的获取和释放。

  2. 特性

    • synchronized:灵活性相对较低,只能用于方法或代码块。而且synchronized方法一旦开始执行,即使线程被阻塞,也不能中断。没有超时机制,一旦获取不到锁就会一直等待,也没有公平性的概念,线程调度由JVM控制。
    • ReentrantLock:支持中断操作,可以在等待锁的过程中响应中断, 提供了尝试获取锁的超时机制,可以通过tryLock()方法设置超时时间。可以设置为公平锁,按照请求的顺序来获取锁,提供了isLocked()isFair()等方法,可以检查锁的状态。
  3. 条件变量:

    • synchronized可以通过wait()notify()notifyAll()与对象的监视器方法配合使用来实现条件变量。
    • ReentrantLock可以通过Condition新API实现更灵活的条件变量控制。

volatile 关键字的作用有那些?

  1. volatile 通常被比喻成”轻量级的 synchronized “,它不需要获取和释放锁,是Java并发编程中比较重要的一个关键字。 和 synchronized 不同, volatile 是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
  2. volatile关键字在Java中主要用于保证变量的内存可见性和禁止指令重排。
  • 保证可见性: 确保当一个线程修改了一个volatile变量时,其他线程能够立即看到这个改变。

    • 当对非 volatile 变量进行读写的时候,每个线程先从主内存拷贝变量到 CPU 缓存中,如果计算机有多个 CPU,每个线程可能在不同的 CPU 上 被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
    • volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过 CPU cache 这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
  • 禁止指令重排:volatile变量的写操作在JVM执行时不会发生指令重排,确保写入操作在读取操作之前完成。

    • 指令重排序是 JVM 为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度, 包括编译器重排序和运行时重排序;
  • volatile 变量禁止指令重排序。针对 volatile 修饰的变量,在读写操作指令前后会插入内存屏障,指令重排序时不能把后面的指令重排序到内存屏障。

  1. 虽然volatile可以确保可见性,但它不保证复合操作的原子性。

volatile 与synchronized 的对比

volatilesynchronized都是Java中用于多线程同步的工具,在用途、原子性、互斥性、性能和使用场景上有一定的区别。

  • 机制和用途
  1. synchronized:用于提供线程间的同步机制。当一个线程进入一个由 synchronized 修饰的代码块或方法时,它会获取一个监视器锁,这保证了同一时间只有一个线程可以执行这段代码。其主要用途是确保数据的一致性和线程安全性。

  2. volatile:用于修饰变量。volatile 的主要作用是确保变量的可见性,即当一个线程修改了一个 volatile 变量的值,其他线程能够立即看到这个修改。此外,它还可以防止指令重排序。但是,volatile 并不能保证复合操作的原子性。

    ​ 总结: volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

  • 原子性
  1. synchronized:可以保证被其修饰的代码块的原子性,即这段代码在执行过程中不会被其他线程打断。
  2. volatile:只能保证单个读写操作的原子性,对于复合操作(如自增、自减等)则无法保证原子性。
  • 互斥性:
  1. synchronized:提供了互斥性,即同一时间只有一个线程可以执行被其修饰的代码块或方法。
  2. volatile:不提供互斥性,只是确保变量的可见性。

为什么要有线程池?

  • 资源管理: 在多线程应用中,每个线程都需要占用内存和CPU资源,如果不加限制地创建线程,会导致系统资源耗尽,可能引发系统崩溃。线程池通过限制并控制线程的数量,帮助避免这个问题。
  • 提高性能:通过重用已存在的线程,线程池可以减少创建和销毁线程的开销。
  • 任务排队:线程池通过任务队列和工作线程的配合,合理分配任务,确保任务按照一定的顺序执行,避免线程竞争和冲突。
  • 统一管理:线程池提供了统一的线程管理方式,可以对线程进行监控、调度和管理。

​ 背诵:采用多线程编程的时候如果线程过多会造成系统资源的大量占用,降低系统效率。线程池的作用就是创造并且管理一部分线程,当系统需要处理任务时直接将任务添加到线程池的任务队列中,由线程池决定由哪个空闲且存活线程来处理,当线程池中线程不够时会适当创建一部分线程,线程冗余时会销毁一部分线程。这样提高线程的利用率,降低系统资源的消耗。

线程池有哪些常用参数

线程

  • corePoolSize核心线程数:线程池中长期存活的线程数。
  • maximumPoolSize最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
  • keepAliveTime空闲线程存活时间:当线程数大于corePoolSize时,多余的空闲线程能等待新任务的最长时间。
  • TimeUnit:与keepAliveTime一起使用,指定keepAliveTime的时间单位,如秒、分钟等。

线程池

  • workQueue线程池任务队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
  • ThreadFactory:创建线程的工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
  • RejectedExecutionHandler拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。

线程的生命周期

BIO、NIO、AIO 的区别

BIO、NIO和AIO是Java中不同的I/O模型,它们在处理输入输出操作时有不同的特点。

  • BIO: 阻塞式的I/O模型。当一个线程执行I/O操作时,如果数据还没准备好,这个线程会被阻塞,直到数据到达。适合连接数较少且固定的场景,但扩展性较差。
  • NIO: 非阻塞的I/O模型。NIO使用缓冲区和通道来处理数据,提高了I/O操作的效率。支持面向缓冲区的读写操作,可以处理大量并发的连接。
  • AIO: 异步I/O模型,从Java 7开始引入。在AIO中,I/O操作被发起后,线程可以继续执行其他任务,一旦I/O操作完成,操作系统会通知线程。适合需要处理大量并发I/O操作,且希望避免I/O操作阻塞线程的场景。
  • 使用场景:
    • BIO适合低并发、连接数较少的应用。
    • NIO适合高并发、需要处理大量连接的应用。
    • AIO适合需要高性能、异步处理I/O操作的场景。