面试问题【线程】

news2024/12/23 15:54:35

线程

  • 什么是进程
  • 什么是线程
  • 进程和线程的关系
  • 什么是并发和并行
  • 如何使用线程
  • Thread 和 Runnable 两种开发线程的区别
  • 线程的生命周期
  • 什么是上下文切换
  • 什么是线程死锁
  • 如何避免死锁
  • 说说 sleep() 方法和 wait() 方法区别和共同点
  • 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
  • 讲一下 synchronized 关键字的底层原理
  • 构造方法可以使用 synchronized 关键字修饰么
  • 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
  • 谈谈 synchronized 和 ReentrantLock 的区别
  • 公平锁和非公平锁有什么区别
  • Volatile 关键字的作用
  • Volatile 与 Synchronized 比较
  • ThreadLocal 说一下
  • 讲一讲ThreadLocal 原理
  • ThreadLocal 与 Synchronized 的区别
  • 为什么要用线程池
  • 执行 execute()方法和 submit()方法的区别是什么呢
  • JDK中自带的线程池
  • ThreadPoolExecutor 构造函数重要参数分析
  • JUC 中常用的并发工具类
  • 多线程 Thread.yield 方法到底有什么用
  • 什么是锁消除和锁粗化

什么是进程

是包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元。电脑中时会有很多单独运行的程序,每个程序有一个独立的进程。例如微信,IDEA,GOOGLE等等。

在这里插入图片描述

什么是线程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

在这里插入图片描述

进程和线程的关系

  • 一个进程由一个或多个线程组成。
  • 线程的划分尺度小于进程。
  • 多个进程在执行过程中拥有独立的内存单元,而多个线程共享内存。

什么是并发和并行

  • 并发

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

翻译成人话就是:

我们在打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。(就像前面提到的操作系统的时间片分时调度)

  • 并行

并行,当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行。

如何使用线程

(1)通过继承Thread类,重写run方法,线程的任务定义在run()方法中。

class TicketThread extends Thread {
    private int tickets = 30;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }
}

创建线程对象和启动线程

// 创建一个线程对象
TicketThread t = new TicketThread();
// 启动线程
t.start();

(2)实现Runnable接口,实现run方法,线程的任务,定义在run()方法中。

class TicketThread implements Runnable {
    private int tickets = 30;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                break;
            }
        }
    }
}

创建线程对象和启动线程

// 创建一个任务对象
Runnable runnable = new TicketThread();
// 创建一个线程对象
Thread t = new Thread(runnable);
// 启动线程
t.start();

(3)实现Callable接口。

class TicketThread implements Callable<List<String>> {
    private int tickets = 30;
    List<String> list = new ArrayList<String>();

    @Override
    public List<String> call() throws Exception {

        while (true) {
            if (tickets > 0) {
                list.add(Thread.currentThread().getName() + "正在买票" + tickets--);
            } else {
                list.add(Thread.currentThread().getName() + "票卖完了");
                return list;
            }
        }
    }
}

创建线程对象和启动线程

Callable callable = new TicketThread();
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
// 获取返回值
List<String> list = (List<String>)futureTask.get();
for (String s : list) {
    System.out.println(s);
}

Thread 和 Runnable 两种开发线程的区别

  1. 继承Thread类,不能实现多个线程共享同一个实例资源,实现Runnable接口,任务模块化,多个线程可以共享同一个资源。
  2. 继承Thread类,不能继承其他类,有单继承局限性,实现Runnable接,还可以继承其他的父类。

结论:开发中通常使用实现Runnable接口,开发多线程应用。

线程的生命周期

  1. 新建状态(new):创建一个线程对象。
  2. 就绪状态(Runnable):线程对象创建之后,调用start()方法,就绪状态的线程只处于等待CPU的使用权,变为可运行。
  3. 运行状态(Running): 就绪状态的线程,获取到了CPU资源,执行程序代码。
  4. 阻塞状态(Blocked): 等待阻塞(线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒)、线程阻塞(线程获取synchronized同步锁失败(因为锁被其它线程锁占用),它会进入同步阻塞状态)、其它阻塞(通过调用线程的sleep()或join()或发出了IO请求时,线程就会进入阻塞状态。当sleep()超时、join()等待线程终止或超时、或者IO处理完毕时,线程重新转入就绪状态)。
  5. 死亡状态(Dead):线程任务执行结束,即run()方法结束,该线程对象就会被垃圾回收,线程对象即为死亡状态。

什么是上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

什么是线程死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

在这里插入图片描述

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

如何避免死锁

加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

说说 sleep() 方法和 wait() 方法区别和共同点

  • 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。

  • 两者都可以暂停线程的执行。

  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法,sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

讲一下 synchronized 关键字的底层原理

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized 关键字最主要的三种使用方式:

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
  //业务代码
}
  1. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized void staic method() {
  //业务代码
}
  1. 修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
  //业务代码
}

同步代码块:编译后,将monitorenter指令插入到代码块的开始处,然后将monitorexit指令插入到代码块结束处或者异常处。每个对象都有一个关联的monitor,当遇到monitorenter时,就需要去获取monitor,获取不到则进入阻塞队列,等待这个monitor被持有者释放后的唤醒通知。

同步方法:在method_info的结构里,有ACC_Sychronized标记,线程执行时如果识别到这个标记,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。

这两者的细节不一样,但是本质上都是尝试获取对象的monitor,如果获取不到,则线程会被阻塞(状态为BLOCKED),进入同步队列。当持有monitor的线程释放了锁后,就会唤醒阻塞在同步队列的线程,然后大家重新尝试获取monitor
在这里插入图片描述

构造方法可以使用 synchronized 关键字修饰么

构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说

说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,但是也有人说锁能进行降级。这种策略是为了提高获得锁和释放锁的效率。

谈谈 synchronized 和 ReentrantLock 的区别

  1. synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
  2. synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁,而 ReentrantLock 需要手动加锁和释放锁。
  3. synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是通过 AQS 程序级别的 API 实现。
  4. synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。默认情况下 ReentrantLock 为非公平锁。

公平锁和非公平锁有什么区别

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。举个例子,公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,如下图所示:

在这里插入图片描述
通过收费站的顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。

在这里插入图片描述
在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。

Volatile 关键字的作用

  1. 线程可见性
public class VolatileTest {
    boolean flag = true;

    public void updateFlag() {
        this.flag = false;
        System.out.println("修改flag值为:" + this.flag);
    }

    public static void main(String[] args) {
        VolatileTest test = new VolatileTest();
        new Thread(() -> {
            while (test.flag) {
            }
            System.out.println(Thread.currentThread().getName() + "结束");
        }, "Thread1").start();

        new Thread(() -> {
            try {
                Thread.sleep(2000);
                test.updateFlag();
            } catch (InterruptedException e) {
            }
        }, "Thread2").start();

    }
}

打印结果如下,我们可以看到虽然线程Thread2已经把flag 修改为false了,但是线程Thread1没有读取到flag修改后的值,线程一直在运行

修改flag值为:false

我们把flag 变量加上volatile: volatile boolean flag = true;

重新运行程序,打印结果如下。Thread1结束,说明Thread1读取到了flage修改后的值

修改flag值为:false
Thread1结束

说到可见性,我们需要先了解一下Java内存模型,Java内存模型如下所示:

在这里插入图片描述
线程之间的共享变量存储在主内存中(Main Memory)中,每个线程都一个都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。所以当一个线程把主内存中的共享变量读取到自己的本地内存中,然后做了更新。在还没有把共享变量刷新的主内存的时候,另外一个线程是看不到的。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

Volatile 与 Synchronized 比较

  1. Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
  2. Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
  3. 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  4. volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

ThreadLocal 说一下

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

举个简单的例子:

孙悟空大闹天宫,那孙悟空就是Main线程同理那些猴子分身就是创建的线程。但是金箍棒就是只有一个,每一个猴子共用一个金箍棒坑定会出问题,那孙悟空的解决方式就是使用ThreadLocal,给每一个分身创建一个金箍棒的副本,各个分身将金箍棒放到自己的耳朵中。

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    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);
            Thread.sleep(new Random().nextInt(1000));
            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 原理

其实从上面的孙悟空大闹天宫的例子我们已经讲了ThreadLocal的原理了,但是这里我们从 Thread类源代码入手。

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

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

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 ......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

在这里插入图片描述

ThreadLocal 与 Synchronized 的区别

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  2. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

为什么要用线程池

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

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

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

JDK中自带的线程池

  • FixedThreadPool :该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

ThreadPoolExecutor 构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁。
  • TimeUnit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。
    在这里插入图片描述

JUC 中常用的并发工具类

CountDownLatch:CountDownLatch是我目前使用比较多的类,CountDownLatch初始化时会给定一个计数,然后每次调用countDown() 计数减1,当计数未到达0之前调用await() 方法会阻塞直到计数减到0

多线程 Thread.yield 方法到底有什么用

yield 即 “谦让”,也是 Thread 类的方法。它让出当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。

public static native void yield();

yield() 和 sleep() 的异同:

  1. 都能暂停当前线程,sleep() 可以指定具体休眠的时间,而 yield() 则依赖 CPU 的时间片划分。
  2. 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。

什么是锁消除和锁粗化

  1. 锁消除

所消除就是虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。

比如StringBuffer的append方法,因为append方法需要判断对象是否被占用,而如果代码不存在锁竞争,那么这部分的性能消耗是无意义的。于是虚拟机在即时编译的时候就会将上面的代码进行优化,也就是锁消除。

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

从源码可以看出,append方法用了 synchronized关键字,它是线程安全的。但我们可能仅在线程内部把StringBuffer当做局部变量使用;StringBuffer仅在方法内作用域有效,不存在线程安全的问题,这时我们可以通过编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
 
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
public static String createStringBuffer(String str1, String str2) {
    StringBuffer sBuf = new StringBuffer();
    sBuf.append(str1);// append方法是同步操作
    sBuf.append(str2);
    return sBuf.toString();
}

逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?如果将sBuf作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说sBuf这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。

  1. 锁粗化

锁的请求、同步、释放都会消耗一定的系统资源,如果高频的锁请求反而不利于系统性能的优化,锁粗化就是把多次的锁请求合并成一个请求,扩大锁的范围,降低锁请求、同步、释放带来的性能损耗。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/384407.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Transformer学习

原论文&#xff1a;Attention Is All You Need。论文地址&#xff1a;https://arxiv.org/abs/1706.03762. Transformer是针对自然语言处理的&#xff0c;Google在2017年发表在Computation and Language&#xff0c;RNN模型记忆长度有限且无法并行化但是Tranformer解决了上述问…

解析几何北大第五版复习提纲

第一章 两向量向量积 向量积定义&#xff1a;a x b |a||b|sin几何意义&#xff1a;平行四边形面积性质&#xff1a; 两向量共线的充分必要条件是 a x b 0 数乘&#xff1a; 分配律&#xff1a; 求法&#xff1a;行列式 三向量混合积 混合积定义&#xff1a;对于一个六面体,…

快鲸SCRM发布口腔企业私域运营解决方案

口腔企业普遍面临着以下几方面运营痛点问题 1、获客成本居高不下&#xff0c;恶性竞争严重 2、管理系统落后&#xff0c;人员流失严重 3、客户顾虑多、决策时间长 4、老客户易流失&#xff0c;粘性差 以上这些痛点&#xff0c;不得不倒逼口腔企业向精细化运营客户迈进。 …

【LeetCode】剑指 Offer 21. 调整数组顺序使奇数位于偶数前面 p131 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/ 1. 题目介绍&#xff08;21. 调整数组顺序使奇数位于偶数前面&#xff09; 输入一个整数数组&#xff0c;实现一个函数来调整该数组中数字的顺序&…

4自由度串联机械臂按颜色分拣物品功能的实现

1. 功能说明 本实验要实现的功能是&#xff1a;将黑、白两种颜色的工件分别放置在传感器上时&#xff0c;机械臂会根据检测到的颜色&#xff0c;将工件搬运至写有相应颜色字样区域。 2. 使用样机 本实验使用的样机为4自由度串联机械臂。 3. 运动功能实现 3.1 电子硬件 在这个…

快速吃透π型滤波电路-LC-RC滤波器

π型滤波器简介 π型滤波器包括两个电容器和一个电感器&#xff0c;它的输入和输出都呈低阻抗。π型滤波有RC和LC两种&#xff0c; 在输出电流不大的情况下用RC&#xff0c;R的取值不能太大&#xff0c;一般几个至几十欧姆&#xff0c;其优点是成本低。其缺点是电阻要消耗一些…

亚马逊二审来袭,跨境电商传统验证算法真的靠谱吗?

多个大卖突遭二审 已有卖家账号被封 近期有不少卖家在论坛上反映称自己收到了亚马逊的二次视频验证邮件。 邮件上称&#xff1a; 卖家必须要完成额外的身份审查&#xff0c;才有资格在亚马逊继续销售商品&#xff1b;亚马逊要求卖家出示注册时提交的身份证原件和营业执照原件…

聊聊混沌工程

这是鼎叔的第五十四篇原创文章。行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。欢迎关注本专栏和微信公众号《敏捷测试转型》&#xff0c;大量原创思考文章陆续推出。混沌工程是一门新兴学科&#xff0c;它不仅仅只是个技术活动&#xff0c;还包含如何设计能够持续协作的…

xgboost:算法数学原理

xgboost算法数学原理 1、求预测值 y^iϕ(xi)∑k1Kfk(xi),fk∈F,(1)\hat{y}_i\phi\left(\mathbf{x}_i\right)\sum_{k1}^K f_k\left(\mathbf{x}_i\right), \quad f_k \in \mathcal{F},\tag{1} y^​i​ϕ(xi​)k1∑K​fk​(xi​),fk​∈F,(1) F{f(x)wq(x)}(q:Rm→T,w∈RT)\mathca…

即时通讯开发常用加解密算法与通讯安全

平时开发工作中&#xff0c;我们会经常接触加密、解密的技术。尤其在今天移动互联网时代&#xff0c;越来越多的用户会将数据存储在云端&#xff0c;或使用在线的服务处理信息。这些数据有些涉及用户的隐私&#xff0c;有些涉及用户的财产&#xff0c;要是没有一套的方案来解决…

玩转结构体---【C语言】

⛩️博主主页&#xff1a;威化小餅干&#x1f4dd;系列专栏&#xff1a;【C语言】藏宝图&#x1f38f; ✨绳锯⽊断&#xff0c;⽔滴⽯穿&#xff01;一个编程爱好者的学习记录!✨目录结构体类型的声明结构体成员访问结构体传参前言我们是否有想过&#xff0c;为什么会有结构体呢…

stylelint执行插件的全过程

stylelint可以用来扩展插件去实现各种规则&#xff0c;接下来带大家看看stylelint是如何执行插件的 首先遍历absoluteFilePaths路径&#xff08;该路径是我们执行lint命令配置的文件类型eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix&…

Spark RDD

RDD RDD 是构建 Spark 分布式内存计算引擎的基石&#xff0c;如 &#xff1a;DAG/调度系统都衍生自 RDD RDD 是对分布式数据集的抽象&#xff0c;囊括所有内存/磁盘的分布式数据实体 RDD/数组差异 对比项数组RDD概念数据结构实体数据模型抽象数据跨度单机进程内跨进程&…

GC简介和监控调优

GC简介&#xff1a; GC(Garbage Collection)是java中的垃圾回收机制&#xff0c;是Java与C/C的主要区别之一&#xff0c;在使用JAVA的时候&#xff0c;一般不需要专门编写内存回收和垃圾清理代 码。这是因为在Java虚拟机中&#xff0c;存在自动内存管理和垃圾清扫机制。 什么…

ThreadLoca基本使用以及与synchronized的区别

文章目录1. ThreadLocal介绍1.1 官方介绍1.2 基本使用1.2.1 常用方法1.2.2 使用案例1.3 ThreadLocal类与synchronized关键字1.3.1 synchronized同步方式1.3.2 ThreadLocal与synchronized的区别2. 运用场景_事务案例2.1 转账案例2.1.1 场景构建2.1.2 引入事务2.2 常规解决方案2.…

k8s servelList(服务列表) 卡死不同步问题分析

提要容器集群版本情况&#xff1a;k8s 1.20客户端k8s client版本&#xff1a; 0.21事情是这样的&#xff0c;运行了一年的服务&#xff0c;突然有一天业务反馈服务使用异常&#xff0c;然后初步调查结果如下以下截图是网关异常以下截图是客户端zull&#xff08;feign&#xff0…

依赖倒置DIP在系统架构中的应用

最近在对项目中的某一模块进行重构和功能的拓展。一直没想到好方法。 简单理解为&#xff1a; R项目 调用了 E项目的打印接口&#xff0c;但是E项目需要对R传来对数据传输对象DTO进行二次处理&#xff0c;甚至夹杂很多R项目的业务逻辑&#xff08;去调用R项目的接口&#xff0…

代码规范书写说明

目录 一&#xff0c;命名风格 二、常量定义 三、代码格式 一&#xff0c;命名风格 &#xff08;1&#xff09;、不能够以下划线或者美元符号开始&#xff0c;也不能以下划线或者美元符号结束 反例&#xff1a;_name / __name / $name / name_ &#xff08;2&#xff09;、所…

春招进行时:“211文科硕士吐槽工资5500” HR:行情和能力决定价值

学历重要&#xff0c;还是能力重要&#xff1f; 春招进行时&#xff0c;不少学生求职遇冷&#xff0c;会把原因归结为学历水平不够高、毕业院校不够档次、专业不够热门、非一线城市就业机会少等等。 直到上海一位211大学的文科男硕士&#xff0c;吐槽招聘会提供的岗位薪资待遇…

10个实用技巧:如何让你的外贸独立站排名直线上升

在当今竞争激烈的互联网市场中&#xff0c;谷歌SEO已经成为了外贸独立站排名提升的必修课程。为了使得自己的网站能够在谷歌上排名更高&#xff0c;网站优化的工作显得尤为重要。 在这篇文章中&#xff0c;我们将分享10个实用技巧&#xff0c;帮助你的外贸独立站排名直线上升。…