ok

java线程和linux的C++线程有何区别

Java线程:Java中的异常处理机制可以捕获和处理线程中的异常。

C++线程:C++线程中的异常会导致程序终止,除非显式地进行了异常处理。

java线程

多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。

  • 程序计数器PC,需要按顺序实行机器指令;多线程情况下,相当于一个指针指向执行位置,恢复上下文
  • 如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

虚拟机栈和本地方法栈为什么私有

  • 每个方法的栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程
  • 本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

    java线程的生命周期

    Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程死锁

sleep() 方法和 wait() 方法对比

  1. wait释放了锁,sleep没释放锁,所以wait会被用于线程间的同步,sleep常用于暂停程序
  2. wait的线程不自动苏醒,需要同一对象的其他线程使用notify去唤醒,sleep的时间过完会自动往下执行
  • wait执行完释放了锁
  • sleep没有释放锁,可能导致死锁
  • wait() 通常被用于线程间交互/通信
  • sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。
  • sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中

  • wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。
  • 每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

    为什么sleep方法定义在Thread中

  • sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

【面试题】可以直接调用 Thread 类的 run 方法吗

【回答】可以直接调用,但是直接执行 run() 方法,不经过start()时,jvm会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它

  • 调用 start() 方法方可启动线程并使线程进入就绪状态
  • 直接执行 run() 方法的话不会以多线程的方式执行

Java内存模型

共享内存:volatile声明变量共享,防止jvm指令重排

  • 该变量可能会被多个线程同时访问
  • 每次读区都从内存读,不会被本地线程缓存
  • 可确保可见性和有序性,但不能保证原子性

这个变量在堆上还是在栈上

  • 至于变量在哪里存储,volatile 关键字主要影响了变量的可见性,在堆还是栈上取决于它是成员变量还是局部变量

    禁止指令重排序:volatile

    一个jvm调优的办法,jvm会自动重排,有可能导致性能下降。unsafe类提供了直接操作内存的方法,volatile的变量在读写时,插入读写屏障来禁止指令重排
    1
    2
    3
    public native void loadFence();
    public native void storeFence();
    public native void fullFence();

乐观锁悲观锁

  • 乐观锁是线程无需等待,只在提交修改的时候验证资源是否被修改
  • 悲观锁是显式的synchronized或者reentrantlock独占锁

CAS算法java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS Compare And Swap(比较与交换)实现的。

区别:高并发场景下,乐观锁不会造成死锁或阻塞问题。

  • 高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。

  • 但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

    两者使用方式

  • 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。

  • 不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。

CAS算法流程

  • 是一个原子操作
  • 涉及到 var变量,expected预期值,new预期写入的新值
  • 当v = e时,通过new更新v,如果v != e,就说明其他进程要写,当前线程放弃更新
  • 当多个线程同时使用 CAS 操作一个变量时,只有一个会成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
  1. java语言中的cas类是通过unsafe类里的compareAndSwapObject实现的
  2. 它是C++内联汇编的产物

乐观锁的ABA问题

检查变量的时候,原值为A,要赋值为B时检测为A,但是不能保证这之前没有被其他线程修改过

解决思路:在变量前追加版本号/时间戳:AtomicStampedReference

乐观锁的循环时间长的问题

CAS使用自旋锁来进行重试,降低消耗使用pause指令

synchronized关键字

  • 修饰实例方法:锁当前对象实例
  • 修饰静态方法:锁当前类
  • 修饰代码块:对括号里指定的对象/类加锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
    synchronized void method() {
    //业务代码
    }
    // 给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁
    synchronized static void method() {
    //业务代码
    }
    // synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
    // synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
    synchronized(this) {
    //业务代码
    }
  1. synchronized(A.class) 和修饰static方法都是要锁的class
  2. synchronized+方法是给实例对象加锁

[问题]静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?

不互斥!锁的是两种不同的方法,静态用的是当前类的锁,非静态锁的是单个实例对象

构造方法能不能使用 synchronized 关键字修饰【不能】

  • 因为构造方法本身属于线程安全,构造一个对象是原子操作
    多线程可能有些特殊情况造成不安全。
  • 比如说逃逸问题:
    如果在构造方法中将未完全初始化的对象引用传递给其他线程,其他线程可能会在对象完全初始化之前访问它,这也可能导致线程不安全
    1
    2
    3
    4
    5
    6
    public class EscapingExample {
    private static SomeObject sharedObject;
    public EscapingExample() {
    sharedObject = new SomeObject(); // 逃逸
    }
    }

构造时,可以使用静态工厂方法构造对象

底层原理了解吗,很了解,通过对象监视器和访问标识来实现

  • synchronized 同步语句块:使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
  • 修饰方法:JVM通过一个访问标识ACC_SYNCHRONIZED 指明这一个同步方法

synchronized 和 volatile关键字,互补

  • volatile 修饰变量,synchronized修饰方法和代码块
  • volatile 保证顺序性可见性,synchronized保证可见性和原子性
  • volatile 解决变量在多个线程之间的可见性,synchronized解决多线程访问资源的同步性

ReentrantLock是一个可重入的独占锁

  • 底层用AbstractQueuedSynchronizer 实现,内部的Sync类继承了AQS类,
  • 实现公平锁和非公平锁 UnFairSync和FairSync,都是继承的内部类Sync实现的,默认使用公平锁

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

synchronized 和 reentrantlock

  • sychronized 依赖于 jvm的对象监视器monitor、访问标识,reentrantlock依赖于jdk层面
  • reentrantlock需要配合trycatch,抛出的异常是InterruptedException

threadLocal(变量)

可以让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据

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
// 常用方法
import java.text.SimpleDateFormat;
public class ThreadLocalExample implements Runnable{

// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本withInitial(obj)作为本地线程的一个对象
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

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

@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
formatter.set(new SimpleDateFormat());

System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}

threadLocal原理

  • 通过维护threadlocalmap类型的两个变量threadLocals,inheritableThreadLocals可继承threadlocals
  • 初始化为null,当当前线程调用set get时,进行创建
    1
    2
    3
    4
    5
    6
    7
    8
    public class Thread implements Runnable {

    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    }

threadlocal内存泄漏问题

  • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。

  • 所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉,导致了内存泄漏

  • ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

  • ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

  • 弱引用WeakReference

  • 强引用指可以直接访问对象的引用,一般不会被gc,弱引用的话,弱引用不会阻止被引用对象的垃圾回收,也就是说,当只有弱引用引用一个对象时,垃圾回收器可以随时回收该对象,而不考虑当前内存是否足够。这使得弱引用非常适合用于缓存等场景,当内存资源不足时,缓存中的对象可以被及时释放。

线程池

使用ThreadPoolExecutor构造方法去创建

  • FixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。初始大小为 0。当有新任务提交时,如果当前线程池中没有线程可用,它会创建一个新的线程来处理该任务。如果在一段时间内(默认为 60 秒)没有新任务提交,核心线程会超时并被销毁,从而缩小线程池的大小。
  • ScheduledThreadPool:该方法返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

内置线程池Executor是什么,为什么不建议使用【容易导致内存泄漏】

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

使用executors返回的线程池可能导致的问题如下:

  1. FixedThreadPool 和 SingleThreadExecutor:使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM
  2. CachedThreadPool:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM
  3. ScheduledThreadPool 和 SingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM

实现一个根据任务的优先级来执行的线程池【使用阻塞队列】

通过构造函数,传入一个ProorityBlockingQueue<Runnable>