当前位置: 首页 > news >正文

哈尔滨网站专业制作电视台网站模版

哈尔滨网站专业制作,电视台网站模版,公司网站功能性建设有哪些,网站建设神州互动Java多线程面试题 友情提示#xff0c;看完此文#xff0c;在Java多线程这块#xff0c;基本上可以吊打面试官了 线程和进程的区别 进程是资源分配的最小单位#xff0c;线程是CPU调度的最小单位 线程是进程的子集#xff0c;一个进程可以有很多线程#xff0c;每条线…Java多线程面试题 友情提示看完此文在Java多线程这块基本上可以吊打面试官了 线程和进程的区别 进程是资源分配的最小单位线程是CPU调度的最小单位 线程是进程的子集一个进程可以有很多线程每条线程并行执行不同的任务。 不同的进程使用不同的内存空间而所有的线程共享一片相同的内存空间。 每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈 创作不易你的关注分享就是博主更新的最大动力 每周持续更新 扫描【企鹅君】公众号二维码免费领取最新独家面试资料还可以第一时间阅读(比博客早两到三篇) 求关注❤️ 求点赞❤️ 求分享❤️ 对博主真的非常重要 该篇已经被GitHub项目收录github.com/JavaDance 欢迎Star和完善 线程状态 线程状态 Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态 NEW: 初始状态线程被创建出来但没有被调用 start() 。RUNNABLE: 运行状态线程被调用了 start()等待运行的状态。BLOCKED阻塞状态需要等待锁释放。WAITING等待状态表示该线程需要等待其他线程做出一些特定动作通知或中断。TIME_WAITING超时等待状态可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。TERMINATED终止状态表示该线程已经运行完毕。 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。 2、线程状态切换 方法作用区别start启动线程由虚拟机自动调度执行run()方法线程处于就绪状态run线程逻辑代码块处理JVM调度执行线程处于运行状态sleep让当前正在执行的线程休眠暂停执行不释放锁wait使得当前线程等待释放同步锁notify唤醒在此对象监视器上等待的单个线程唤醒单个线程notifyAll唤醒在此对象监视器上等待的所有线程唤醒多个线程yiled停止当前线程让同等优先权的线程运行用Thread类调用join使当前线程停下来等待直至另一个调用join方法的线程终止用线程对象调用 sleep() 方法和 wait() 方法对比 共同点两者都可以暂停线程的执行。 区别 sleep() 方法没有释放锁而 wait() 方法释放了锁 。wait() 通常被用于线程间交互/通信sleep()通常被用于暂停执行。wait() 方法被调用后线程不会自动苏醒需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后线程会自动苏醒或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。sleep() 是 Thread 类的静态本地方法wait() 则是 Object 类的本地方法。为什么这样设计呢下一个问题就会聊到。 为什么 wait() 方法不定义在 Thread 中 wait() 是让获得对象锁的线程实现等待会自动释放当前线程占有的对象锁。每个对象Object都拥有对象锁既然要释放当前线程占有的对象锁并让其进入 WAITING 状态自然是要操作对应的对象Object而非当前的线程Thread。 类似的问题为什么 sleep() 方法定义在 Thread 中 因为 sleep() 是让当前线程暂停执行不涉及到对象类也不需要获得对象锁。 如何在Java中实现线程 1继承Thread类实现多线程 继承Thread类,然后重写run方法.由于Java单继承的特性这种方式用的比较少 public class MyThread extends Thread {public MyThread() {}public void run() {for(int i0;i10;i) {System.out.println(Thread.currentThread():i);}}public static void main(String[] args) {MyThread mThread1new MyThread();MyThread mThread2new MyThread();MyThread myThread3new MyThread();mThread1.start();mThread2.start();myThread3.start();} }2实现Runnable()接口实现其run()方法 推荐此方式。两个特点 a.覆写Runnable接口实现多线程可以避免单继承局限b.实现Runnable()可以更好的体现共享的概念c.当执行目标类实现Runnable接口此时执行目标target类和Thread是代理模式子类负责真是业务的操作thread负责资源调度与线程创建辅助真实业务。 public class MyTarget implements Runnable{public static int count20;public void run() {while(count0) {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()-当前剩余票数:count--);}}public static void main(String[] args) {MyThread targetnew MyTarget();Thread mThread1new Thread(target,线程1);Thread mThread2new Thread(target,线程2);Thread mThread3new Thread(target,线程3);mThread1.start();mThread2.start();myThread3.start();} }3实现Callable接口创建多线程 a.执行目标核心方法叫call()方法 ​ b.有返回值 import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;public class MyTarget implements CallableString {private int count 20;Overridepublic String call() throws Exception {for (int i count; i 0; i--) { // Thread.yield();System.out.println(Thread.currentThread().getName()当前票数 i);}return sale out;} public static void main(String[] args) throws InterruptedException, ExecutionException {CallableString callable new MyTarget();FutureTask StringfutureTasknew FutureTask(callable);Thread mThreadnew Thread(futureTask);Thread mThread2new Thread(futureTask);Thread mThread3new Thread(futureTask); // mThread.setName(hhh);mThread.start();mThread2.start();mThread3.start();System.out.println(futureTask.get());} }4通过线程池创建多线程, 后面讲线程池会讲到 Thread 类中的start() 和 run() 方法有什么区别 start()方法被用来启动新创建的线程使该被创建的线程状态变为可运行状态。 当你直接调用run()方法的时候只会是在原来的线程中调用没有新的线程启动。只有调用start()方法才会启动新线程。 如果我们调用了Thread的run()方法它的行为就会和普通的方法一样直接运行run方法。为了在新的线程中执行我们的代码必须使用Thread.start()方法。 什么是线程上下文切换? 线程在执行过程中会有自己的运行条件和状态也称上下文比如上文所说到过的程序计数器栈信息等。当出现如下情况的时候线程会从占用 CPU 状态中退出。 主动让出 CPU比如调用了 sleep(), wait() 等。时间片用完因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。调用了阻塞类型的系统中断比如请求 IO线程被阻塞。被终止或结束运行 这其中前三种都会发生线程切换线程切换意味着需要保存当前线程的上下文留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。 上下文切换是现代操作系统的基本功能因其每次需要保存信息恢复信息这将会占用 CPU内存等系统资源进行处理也就意味着效率会有一定损耗如果频繁切换就会造成整体效率低下。 什么是线程死锁?如何避免死锁? 认识线程死锁 线程死锁描述的是这样一种情况多个线程同时被阻塞它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞因此程序不可能正常终止。 如下图所示线程 A 持有资源 2线程 B 持有资源 1他们同时都想申请对方的资源所以这两个线程就会互相等待而进入死锁状态。 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》) public class DeadLockDemo {private static Object resource1 new Object();//资源 1private static Object resource2 new Object();//资源 2public 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();} }Output Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源然后这两个线程就会陷入互相等待的状态这也就产生了死锁。 上面的例子符合产生死锁的四个必要条件 互斥条件该资源任意一个时刻只由一个线程占用。请求与保持条件一个线程因请求资源而阻塞时对已获得的资源保持不放。不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源。循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 如何预防和避免线程死锁? 如何预防死锁 破坏死锁的产生的必要条件即可 破坏请求与保持条件一次性申请所有的资源。破坏不剥夺条件占用部分资源的线程进一步申请其他资源时如果申请不到可以主动释放它占有的资源。破坏循环等待条件靠按序申请资源来预防。按某一顺序申请资源释放资源则反序释放。破坏循环等待条件。 如何避免死锁 避免死锁就是在资源分配时借助于算法比如银行家算法对资源分配进行计算评估使其进入安全状态。 安全状态 指的是系统能够按照某种线程推进顺序P1、P2、P3……Pn来为每个线程分配所需资源直到满足每个线程对资源的最大需求使每个线程都可顺利完成。称 P1、P2、P3.....Pn 序列为安全序列。 我们对线程 2 的代码修改成下面这样就不会产生死锁了。 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);}}}, 线程 2).start();输出 Thread[线程 1,5,main]get resource1 Thread[线程 1,5,main]waiting get resource2 Thread[线程 1,5,main]get resource2 Thread[线程 2,5,main]get resource1 Thread[线程 2,5,main]waiting get resource2 Thread[线程 2,5,main]get resource2Process finished with exit code 0我们分析一下上面的代码为什么避免了死锁的发生? 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件因此避免了死锁。 可以直接调用 Thread 类的 run 方法吗 这是另一个非常经典的 Java 多线程面试问题而且在面试中会经常被问到。很简单但是很多人都会答不上来 new 一个 Thread线程进入了新建状态。调用 start()方法会启动一个线程并使线程进入了就绪状态当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作然后自动执行 run() 方法的内容这是真正的多线程工作。 但是直接执行 run() 方法会把 run() 方法当成一个 main 线程下的普通方法去执行并不会在某个线程中执行它所以这并不是多线程工作。 总结调用 start() 方法方可启动线程并使线程进入就绪状态直接执行 run() 方法的话不会以多线程的方式执行。 JMM(Java 内存模型) JMMJava 内存模型相关的问题比较多也比较重要于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题JMMJava 内存模型详解 。 volatile 关键字 如何保证变量的可见性 在 Java 中volatile 关键字可以保证变量的可见性如果我们将变量声明为 volatile 这就指示 JVM这个变量是共享且不稳定的每次使用它都到主存中进行读取。 volatile 关键字其实并非是 Java 语言特有的在 C 语言里也有它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰这就指示 编译器这个变量是共享且不稳定的每次使用它都到主存中进行读取。 volatile 关键字能保证数据的可见性但不能保证数据的原子性。synchronized 关键字两者都能保证。 如何禁止指令重排序 在 Java 中volatile 关键字除了可以保证变量的可见性还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile 在对这个变量进行读写操作的时候会通过插入特定的 内存屏障 的方式来禁止指令重排序。 在 Java 中Unsafe 类提供了三个开箱即用的内存屏障相关的方法屏蔽了操作系统底层的差异 public native void loadFence(); public native void storeFence(); public native void fullFence();理论上来说你通过这个三个方法也可以实现和volatile禁止重排序一样的效果只是会麻烦一些。 下面我以一个常见的面试题为例讲解一下 volatile 关键字禁止指令重排序的效果。 面试中面试官经常会说“单例模式了解吗来给我手写一下给我解释一下双重检验锁方式实现单例模式的原理呗” 双重校验锁实现对象单例线程安全 public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getUniqueInstance() {//先判断对象是否已经实例过没有实例化过才进入加锁代码if (uniqueInstance null) {//类对象加锁synchronized (Singleton.class) {if (uniqueInstance null) {uniqueInstance new Singleton();}}}return uniqueInstance;} }uniqueInstance 采用 volatile 关键字修饰也是很有必要的 uniqueInstance new Singleton(); 这段代码其实是分为三步执行 为 uniqueInstance 分配内存空间初始化 uniqueInstance将 uniqueInstance 指向分配的内存地址 但是由于 JVM 具有指令重排的特性执行顺序有可能变成 1-3-2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空因此返回 uniqueInstance但此时 uniqueInstance 还未被初始化。 volatile 可以保证原子性么 volatile 关键字能保证变量的可见性但不能保证对变量的操作是原子性的。 我们通过下面的代码即可证明 /*** 微信搜 JavaGuide 回复面试突击即可免费领取个人原创的 Java 面试手册** author Guide哥* date 2022/08/03 13:40**/ public class VolatoleAtomicityDemo {public volatile static int inc 0;public void increase() {inc;}public static void main(String[] args) throws InterruptedException {ExecutorService threadPool Executors.newFixedThreadPool(5);VolatoleAtomicityDemo volatoleAtomicityDemo new VolatoleAtomicityDemo();for (int i 0; i 5; i) {threadPool.execute(() - {for (int j 0; j 500; j) {volatoleAtomicityDemo.increase();}});}// 等待1.5秒保证上面程序执行完成Thread.sleep(1500);System.out.println(inc);threadPool.shutdown();} }正常情况下运行上面的代码理应输出 2500。但你真正运行了上面的代码之后你会发现每次输出结果都小于 2500。 为什么会出现这种情况呢不是说好了volatile 可以保证变量的可见性嘛 也就是说如果 volatile 能保证 inc 操作的原子性的话。每个线程中对 inc 变量自增完之后其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作那么最终 inc 的值应该是 5*5002500。 很多人会误认为自增操作 inc 是原子性的实际上inc 其实是一个复合操作包括三步 读取 inc 的值。对 inc 加 1。将 inc 的值写回内存。 volatile 是无法保证这三个操作是具有原子性的有可能导致下面这种情况出现 线程 1 对 inc 进行读取操作之后还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改1再将inc 的值写回内存。线程 2 操作完毕后线程 1 对 inc的值进行修改1再将inc 的值写回内存。 这也就导致两个线程分别对 inc 进行了一次自增操作后inc 实际上只增加了 1。 其实如果想要保证上面的代码运行正确也非常简单利用 synchronized、Lock或者AtomicInteger都可以。 使用 synchronized 改进 public synchronized void increase() {inc; }使用 AtomicInteger 改进 public AtomicInteger inc new AtomicInteger();public void increase() {inc.getAndIncrement(); }使用 ReentrantLock 改进 Lock lock new ReentrantLock(); public void increase() {lock.lock();try {inc;} finally {lock.unlock();} }乐观锁和悲观锁 什么是悲观锁 悲观锁总是假设最坏的情况认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改)所以每次在获取资源操作的时候都会上锁这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说共享资源每次只给一个线程使用其它线程阻塞用完后再把资源转让给其它线程。 像 Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。 public void performSynchronisedTask() {synchronized (this) {// 需要同步的操作} }private Lock lock new ReentrantLock(); lock.lock(); try {// 需要同步的操作 } finally {lock.unlock(); }高并发的场景下激烈的锁竞争会造成线程阻塞大量阻塞线程会导致系统的上下文切换增加系统的性能开销。并且悲观锁还可能会存在死锁问题影响代码的正常运行。 什么是乐观锁 乐观锁总是假设最好的情况认为共享资源每次被访问的时候不会出现问题线程可以不停地执行无需加锁也无需等待只是在提交修改的时候去验证对应的资源也就是数据是否被其它线程修改了具体方法可以使用版本号机制或 CAS 算法。 在 Java 中java.util.concurrent.atomic包下面的原子变量类比如AtomicInteger、LongAdder就是使用了乐观锁的一种实现方式 CAS 实现的。 // LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 // 代价就是会消耗更多的内存空间空间换时间 LongAdder sum new LongAdder(); sum.increment();高并发的场景下乐观锁相比悲观锁来说不存在锁竞争造成线程阻塞也不会有死锁的问题在性能上往往会更胜一筹。但是如果冲突频繁发生写占比非常多的情况会频繁失败和重试这样同样会非常影响性能导致 CPU 飙升。 不过大量失败重试的问题也是可以解决的像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。 理论上来说 悲观锁通常多用于写比较多的情况多写场景竞争激烈这样可以避免频繁失败和重试影响性能悲观锁的开销是固定的。不过如果乐观锁解决了频繁失败和重试这个问题的话比如LongAdder也是可以考虑使用乐观锁的要视实际情况而定。乐观锁通常多用于写比较少的情况多读场景竞争较少这样可以避免频繁加锁影响性能。不过乐观锁主要针对的对象是单个共享变量参考java.util.concurrent.atomic包下面的原子变量类。 如何实现乐观锁 乐观锁一般会使用版本号机制或 CAS 算法实现CAS 算法相对来说更多一些这里需要格外注意。 版本号机制 一般是在数据表中加上一个数据版本号 version 字段表示数据被修改的次数。当数据被修改时version 值会加一。当线程 A 要更新数据值时在读取数据的同时也会读取 version 值在提交更新时若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新否则重试更新操作直到更新成功。 举一个简单的例子假设数据库中帐户信息表中有一个 version 字段当前值为 1 而当前帐户余额字段 balance 为 $100 。 操作员 A 此时将其读出 version1 并从其帐户余额中扣除 $50 $100-$50 。在操作员 A 操作的过程中操作员 B 也读入此用户信息 version1 并从其帐户余额中扣除 $20 $100-$20 。操作员 A 完成了修改工作将数据版本号 version1 连同帐户扣除后余额 balance$50 提交至数据库更新此时由于提交数据版本等于数据库记录当前版本数据被更新数据库记录 version 更新为 2 。操作员 B 完成了操作也将版本号 version1 试图向数据库提交数据 balance$80 但此时比对数据库记录版本时发现操作员 B 提交的数据版本号为 1 数据库记录当前版本也为 2 不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略因此操作员 B 的提交被驳回。 这样就避免了操作员 B 用基于 version1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 CAS 算法 CAS 的全称是 Compare And Swap比较与交换 用于实现乐观锁被广泛应用于各大框架中。CAS 的思想很简单就是用一个预期值和要更新的变量值进行比较两值相等才会进行更新。 CAS 是一个原子操作底层依赖于一条 CPU 的原子指令。 原子操作 即最小不可拆分的操作也就是说操作一旦开始就不能被打断直到操作完成。 CAS 涉及到三个操作数 V要更新的变量值(Var)E预期值(Expected)N拟写入的新值(New) 当且仅当 V 的值等于 E 时CAS 通过原子方式用新值 N 来更新 V 的值。如果不等说明已经有其它线程更新了 V则当前线程放弃更新。 举一个简单的例子线程 A 要修改变量 i 的值为 6i 原值为 1V 1E1N6假设不存在 ABA 问题。 i 与 1 进行比较如果相等 则说明没被其他线程修改可以被设置为 6 。i 与 1 进行比较如果不相等则说明被其他线程修改当前线程放弃更新CAS 操作失败。 当多个线程同时使用 CAS 操作一个变量时只有一个会胜出并成功更新其余均会失败但失败的线程并不会被挂起仅是被告知失败并且允许再次尝试当然也允许失败的线程放弃操作。 Java 语言并没有直接实现 CASCAS 相关的实现是通过 C 内联汇编的形式实现的JNI 调用。因此 CAS 的具体实现和操作系统以及 CPU 都有关系。 sun.misc包下的Unsafe类提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的 CAS 操作 /*** CAS* param o 包含要修改field的对象* param offset 对象中某field的偏移量* param expected 期望值* param update 更新值* return true | false*/ public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);关于 Unsafe 类的详细介绍可以看这篇文章Java 魔法类 Unsafe 详解 - JavaGuide - 2022 。 乐观锁存在哪些问题 ABA 问题是乐观锁最常见的问题。 ABA 问题 如果一个变量 V 初次读取的时候是 A 值并且在准备赋值的时候检查到它仍然是 A 值那我们就能说明它的值没有被其他线程修改过了吗很明显是不能的因为在这段时间它的值可能被改为其他值然后又改回 A那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 ABA问题。 ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用并且当前标志是否等于预期标志如果全部相等则以原子方式将该引用和该标志的值设置为给定的更新值。 public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {PairV current pair;returnexpectedReference current.reference expectedStamp current.stamp ((newReference current.reference newStamp current.stamp) ||casPair(current, Pair.of(newReference, newStamp))); }循环时间长开销大 CAS 经常会用到自旋操作来进行重试也就是不成功就一直循环执行直到成功。如果长时间不成功会给 CPU 带来非常大的执行开销。 如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升pause 指令有两个作用 可以延迟流水线执行指令使 CPU 不会消耗过多的执行资源延迟的时间取决于具体实现的版本在一些处理器上延迟时间是零。可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空从而提高 CPU 的执行效率。 只能保证一个共享变量的原子操作 CAS 只对单个共享变量有效当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始提供了AtomicReference类来保证引用对象之间的原子性你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。 synchronized 关键字 synchronized 是什么有什么用 synchronized 是 Java 中的一个关键字翻译成中文是同步的意思主要解决的是多个线程之间访问资源的同步性可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 在 Java 早期版本中synchronized 属于 重量级锁效率低下。这是因为监视器锁monitor是依赖于底层的操作系统的 Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程都需要操作系统帮忙完成而操作系统实现线程之间的切换时需要从用户态转换到内核态这个状态之间的转换需要相对比较长的时间时间成本相对较高。 不过在 Java 6 之后 synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销这些优化让 synchronized 锁的效率提升了很多。因此 synchronized 还是可以在实际项目中使用的像 JDK 源码、很多开源框架都大量使用了 synchronized 。 如何使用 synchronized synchronized 关键字的使用方式主要有下面 3 种 修饰实例方法修饰静态方法修饰代码块 1、修饰实例方法 锁当前对象实例 给当前对象实例加锁进入同步代码前要获得 当前对象实例的锁 。 synchronized void method() {//业务代码 }2、修饰静态方法 锁当前类 给当前类加锁会作用于类的所有对象实例 进入同步代码前要获得 当前 class 的锁。 这是因为静态成员不属于任何一个实例对象归整个类所有不依赖于类的特定实例被类的所有实例共享。 synchronized static void method() {//业务代码 }静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么不互斥如果一个线程 A 调用一个实例对象的非静态 synchronized 方法而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法是允许的不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的锁而访问非静态 synchronized 方法占用的锁是当前实例对象锁。 3、修饰代码块 锁指定对象/类 对括号里指定的对象/类加锁 synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁 synchronized(this) {//业务代码 }总结 synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁synchronized 关键字加到实例方法上是给对象实例上锁尽量不要使用 synchronized(String a) 因为 JVM 中字符串常量池具有缓存功能。 构造方法可以用 synchronized 修饰么 先说结论构造方法不能使用 synchronized 关键字修饰。 构造方法本身就属于线程安全的不存在同步的构造方法一说。 synchronized 底层原理了解吗 synchronized 关键字底层原理属于 JVM 层面的东西。 synchronized 同步语句块的情况 public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println(synchronized 代码块);}} }通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件然后执行javap -c -s -v -l SynchronizedDemo.class。 从上面我们可以看出synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置。 上面的字节码中包含一个 monitorenter 指令以及两个 monitorexit 指令这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。 当执行 monitorenter 指令时线程试图获取锁也就是获取 对象监视器 monitor 的持有权。 在 Java 虚拟机(HotSpot)中Monitor 是基于 C实现的由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。 另外wait/notify等方法也依赖于monitor对象这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法否则会抛出java.lang.IllegalMonitorStateException的异常的原因。 在执行monitorenter时会尝试获取对象的锁如果锁的计数器为 0 则表示锁可以被获取获取后将锁计数器设为 1 也就是加 1。 对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后将锁计数器设为 0表明锁被释放其他线程可以尝试获取锁。 如果获取对象锁失败那当前线程就要阻塞等待直到锁被另外一个线程释放为止。 synchronized 修饰方法的的情况 public class SynchronizedDemo2 {public synchronized void method() {System.out.println(synchronized 方法);} } synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令取得代之的确实是 ACC_SYNCHRONIZED 标识该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法从而执行相应的同步调用。 如果是实例方法JVM 会尝试获取实例对象的锁。如果是静态方法JVM 会尝试获取当前 class 的锁。 总结 synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置。 synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令取得代之的确实是 ACC_SYNCHRONIZED 标识该标识指明了该方法是一个同步方法。 不过两者的本质都是对对象监视器 monitor 的获取。 相关推荐Java 锁与线程的那些事 - 有赞技术团队 。 进阶一下学有余力的小伙伴可以抽时间详细研究一下对象监视器 monitor。 JDK1.6 之后的 synchronized 底层做了哪些优化 JDK1.6 对锁的实现引入了大量的优化如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 锁主要存在四种状态依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级这种策略是为了提高获得锁和释放锁的效率。 关于这几种优化的详细信息可以查看下面这篇文章Java6 及以上版本对 synchronized 的优化 。 synchronized 和 volatile 有什么区别 synchronized 关键字和 volatile 关键字是两个互补的存在而不是对立的存在 volatile 关键字是线程同步的轻量级实现所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。volatile 关键字能保证数据的可见性但不能保证数据的原子性。synchronized 关键字两者都能保证。volatile关键字主要用于解决变量在多个线程之间的可见性而 synchronized 关键字解决的是多个线程之间访问资源的同步性。 ReentrantLock ReentrantLock 是什么 ReentrantLock 实现了 Lock 接口是一个可重入且独占式的锁和 synchronized 关键字类似。不过ReentrantLock 更灵活、更强大增加了轮询、超时、中断、公平锁和非公平锁等高级功能。 public class ReentrantLock implements Lock, java.io.Serializable {}ReentrantLock 里面有一个内部类 SyncSync 继承 AQSAbstractQueuedSynchronizer添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。 ReentrantLock 默认使用非公平锁也可以通过构造器来显式的指定使用公平锁。 // 传入一个 boolean 值true 时为公平锁false 时为非公平锁 public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync(); }从上面的内容可以看出 ReentrantLock 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 AQS 详解 这篇文章。 公平锁和非公平锁有什么区别 公平锁 : 锁被释放之后先申请的线程先得到锁。性能较差一些因为公平锁为了保证时间上的绝对顺序上下文切换更频繁。非公平锁锁被释放之后后申请的线程可能会先获取到锁是随机或者按照其他优先级排序的。性能更好但可能会导致某些线程永远无法获取到锁。 synchronized 和 ReentrantLock 有什么区别 两者都是可重入锁 可重入锁 也叫递归锁指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果是不可重入锁的话就会造成死锁。 JDK 提供的所有现成的 Lock 实现类包括 synchronized 关键字锁都是可重入的。 在下面的代码中method1() 和 method2()都被 synchronized 关键字修饰method1()调用了method2()。 public class SynchronizedDemo {public synchronized void method1() {System.out.println(方法1);method2();}public synchronized void method2() {System.out.println(方法2);} }由于 synchronized锁是可重入的同一个线程在调用method1() 时可以直接获得当前对象的锁执行 method2() 的时候可以再次获取这个对象的锁不会产生死锁问题。假如synchronized是不可重入锁的话由于该对象的锁已被当前线程所持有且无法释放这就导致线程在执行 method2()时获取锁失败会出现死锁问题。 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API synchronized 是依赖于 JVM 实现的前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化但是这些优化都是在虚拟机层面实现的并没有直接暴露给我们。 ReentrantLock 是 JDK 层面实现的也就是 API 层面需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成所以我们可以通过查看它的源代码来看它是如何实现的。 ReentrantLock 比 synchronized 增加了一些高级功能 相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待改为处理其他事情。可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。可实现选择性通知锁可以绑定多个条件: synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现但是需要借助于Condition接口与newCondition()方法。 如果你想使用上述功能那么选择 ReentrantLock 是一个不错的选择。 关于 Condition接口的补充 Condition是 JDK1.5 之后才有的它具有很好的灵活性比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例即对象监视器线程对象可以注册在指定的Condition中从而可以有选择性的进行线程通知在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时被通知的线程是由 JVM 选择的用ReentrantLock类结合Condition实例可以实现“选择性通知” 这个功能非常重要而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题。而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。 可中断锁和不可中断锁有什么区别 可中断锁获取锁的过程中可以被中断不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。不可中断锁一旦线程申请了锁就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。 ReentrantReadWriteLock ReentrantReadWriteLock 在实际项目中使用的并不多面试中也问的比较少简单了解即可。JDK 1.8 引入了性能更好的读写锁 StampedLock 。 ReentrantReadWriteLock 是什么 ReentrantReadWriteLock 实现了 ReadWriteLock 是一个可重入的读写锁既可以保证多个线程同时读的效率同时又可以保证有写入操作时的线程安全。 public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable{ } public interface ReadWriteLock {Lock readLock();Lock writeLock(); }一般锁进行并发控制的规则读读互斥、读写互斥、写写互斥。读写锁进行并发控制的规则读读不互斥、读写互斥、写写互斥只有读读不互斥。 ReentrantReadWriteLock 其实是两把锁一把是 WriteLock (写锁)一把是 ReadLock读锁 。读锁是共享锁写锁是独占锁。读锁可以被同时读可以同时被多个线程持有而写锁最多只能同时被一个线程持有。 和 ReentrantLock 一样ReentrantReadWriteLock 底层也是基于 AQS 实现的。 ReentrantReadWriteLock 也支持公平锁和非公平锁默认使用非公平锁可以通过构造器来显示的指定。 // 传入一个 boolean 值true 时为公平锁false 时为非公平锁 public ReentrantReadWriteLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync();readerLock new ReadLock(this);writerLock new WriteLock(this); }ReentrantReadWriteLock 适合什么场景 由于 ReentrantReadWriteLock 既可以保证多个线程同时读的效率同时又可以保证有写入操作时的线程安全。因此在读多写少的情况下使用 ReentrantReadWriteLock 能够明显提升系统性能。 共享锁和独占锁有什么区别 共享锁一把锁可以被多个线程同时获得。独占锁一把锁只能被一个线程获得。 线程持有读锁还能获取写锁吗 在线程持有读锁的情况下该线程不能取得写锁(因为获取写锁的时候如果发现当前的读锁被占用就马上获取失败不管读锁是不是被当前线程持有)。在线程持有写锁的情况下该线程可以继续获取读锁获取读锁时如果发现写锁被占用只有写锁没有被当前线程占用的情况才会获取失败。 读写锁的源码分析推荐阅读 聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件 这篇文章写的很不错。 读锁为什么不能升级为写锁 写锁可以降级为读锁但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺毕竟写锁属于是独占锁这样的话会影响性能。 另外还可能会有死锁问题发生。举个例子假设两个线程的读锁都想升级写锁则需要对方都释放自己锁而双方都不释放就会产生死锁。 StampedLock StampedLock 面试中问的比较少不是很重要简单了解即可。 StampedLock 是什么 StampedLock 是 JDK 1.8 引入的性能更好的读写锁不可重入且不支持条件变量 Conditon。 不同于一般的 Lock 类StampedLock 并不是直接实现 Lock或 ReadWriteLock接口而是基于 CLH 锁 独立实现的AQS 也是基于这玩意。 public class StampedLock implements java.io.Serializable { }StampedLock 提供了三种模式的读写控制模式读锁、写锁和乐观读。 写锁独占锁一把锁只能被一个线程获得。当一个线程获取写锁后其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁不过这里的写锁是不可重入的。读锁 悲观读共享锁没有线程获取写锁的情况下多个线程可以同时持有读锁。如果己经有线程持有写锁则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁不过这里的读锁是不可重入的。乐观读允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。 另外StampedLock 还支持这三种锁在一定条件下进行相互转换 。 long tryConvertToWriteLock(long stamp){} long tryConvertToReadLock(long stamp){} long tryConvertToOptimisticRead(long stamp){}StampedLock 在获取锁的时候会返回一个 long 型的数据戳该数据戳用于稍后的锁释放参数如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳这也是StampedLock不可重入的原因。 // 写锁 public long writeLock() {long s, next; // bypass acquireWrite in fully unlocked case onlyreturn ((((s state) ABITS) 0L U.compareAndSwapLong(this, STATE, s, next s WBIT)) ?next : acquireWrite(false, 0L)); } // 读锁 public long readLock() {long s state, next; // bypass acquireRead on common uncontended casereturn ((whead wtail (s ABITS) RFULL U.compareAndSwapLong(this, STATE, s, next s RUNIT)) ?next : acquireRead(false, 0L)); } // 乐观读 public long tryOptimisticRead() {long s;return (((s state) WBIT) 0L) ? (s SBITS) : 0L; }StampedLock 的性能为什么更好 相比于传统读写锁多出来的乐观读是StampedLock比 ReadWriteLock 性能更好的关键原因。StampedLock 的乐观读允许一个写线程获取写锁所以不会导致所有写线程阻塞也就是当读多写少的时候写线程有机会获取写锁减少了线程饥饿的问题吞吐量大大提高。 StampedLock 适合什么场景 和 ReentrantReadWriteLock 一样StampedLock 同样适合读多写少的业务场景可以作为 ReentrantReadWriteLock的替代品性能更好。 不过需要注意的是StampedLock不可重入不支持条件变量 Conditon对中断操作支持也不友好使用不当容易导致 CPU 飙升。如果你需要用到 ReentrantLock 的一些高级性能就不太建议使用 StampedLock 了。 另外StampedLock 性能虽好但使用起来相对比较麻烦一旦使用不当就会出现生产问题。强烈建议你在使用StampedLock 之前看看 StampedLock 官方文档中的案例。 StampedLock 的底层原理了解吗 StampedLock 不是直接实现 Lock或 ReadWriteLock接口而是基于 CLH 锁 实现的AQS 也是基于这玩意CLH 锁是对自旋锁的一种改良是一种隐式的链表队列。StampedLock 通过 CLH 队列进行线程的管理通过同步状态值 state 来表示锁的状态和类型。 StampedLock 的原理和 AQS 原理比较类似这里就不详细介绍了感兴趣的可以看看下面这两篇文章 AQS 详解StampedLock 底层原理分析 如果你只是准备面试的话建议多花点精力搞懂 AQS 原理即可StampedLock 底层原理在面试中遇到的概率非常小。 Atomic 原子类 Atomic 原子类部分的内容我单独写了一篇文章来总结Atomic 原子类总结 。 参考 《深入理解 Java 虚拟机》《实战 Java 高并发程序设计》Guide to the Volatile Keyword in Java - Baeldunghttps://www.baeldung.com/java-volatile不可不说的 Java“锁”事 - 美团技术团队https://tech.meituan.com/2018/11/15/java-lock.html在 ReadWriteLock 类中读锁为什么不能升级为写锁https://cloud.tencent.com/developer/article/1176230高性能解决线程饥饿的利器 StampedLockhttps://mp.weixin.qq.com/s/2Acujjr4BHIhlFsCLGwYSg理解 Java 中的 ThreadLocal - 技术小黑屋https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/ThreadLocal (Java Platform SE 8 ) - Oracle Help Centerhttps://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html sleep() 方法和 wait() 方法对比 共同点两者都可以暂停线程的执行。 区别 sleep() 方法没有释放锁而 wait() 方法释放了锁 。wait() 通常被用于线程间交互/通信sleep()通常被用于暂停执行。wait() 方法被调用后线程不会自动苏醒需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后线程会自动苏醒或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。sleep() 是 Thread 类的静态本地方法wait() 则是 Object 类的本地方法。为什么这样设计呢下一个问题就会聊到。 为什么 wait() 方法不定义在 Thread 中 wait() 是让获得对象锁的线程实现等待会自动释放当前线程占有的对象锁。每个对象Object都拥有对象锁既然要释放当前线程占有的对象锁并让其进入 WAITING 状态自然是要操作对应的对象Object而非当前的线程Thread。 类似的问题为什么 sleep() 方法定义在 Thread 中 因为 sleep() 是让当前线程暂停执行不涉及到对象类也不需要获得对象锁。 创作不易你的关注分享就是博主更新的最大动力 每周持续更新 微信搜索【企鹅君】关注公众号还能领取独家面试资料喔并且可以第一时间阅读(比博客早两到三篇) 求关注❤️ 求点赞❤️ 求分享❤️ 对博主真的非常重要 该篇已经被GitHub项目收录github.com/JavaDance 欢迎Star和完善
http://www.eeditor.cn/news/121837/

相关文章:

  • 高校网站建设要点做承兑 汇票一般会用哪些网站
  • wordpress cms管理站内seo和站外seo区别
  • 设计网站logowordpress 上传至
  • 淄博张店做网站的公司wordpress添加图片不显示
  • 找个网站看看长沙建网站联系电话
  • 苏州企业网站推广网站怎样制作流程
  • 宁夏水利建设工程网站食品电子商务网站建设论文
  • 国外设计网站怎么登陆wordpress标签logo
  • 国家住房和城乡建设部网站wordpress怎么pjax
  • 淘宝 客要推广网站怎么做北京企业网站开发多少钱
  • 对建设网站未来发展的建议wordpress安装没反应
  • 开封建设网站电脑做微信推送的网站
  • 四川建设数字证书网站免费的企业名录
  • 英德市住房和城乡建设局网站怎么做房产网站
  • 长沙网站设计建设北京设计公司名称
  • seo网站建站网络营销10大平台
  • 龙岩做网站的地方广告设计公司营业执照
  • 一个阿里云服务器可以放几个网站做网站的属于什么行业
  • wordpress教程下载网站主题粉色做网站背景图片
  • 响应式网站怎么写管理软件是什么
  • 平利县城乡建设局网站阳萎早谢吃什么药最好
  • 温州网站排名优化公司哪家好网站里的做菠菜
  • 常见的网络营销方式有哪些电子商务沙盘seo关键词
  • 网站终端制作金湖县网站建设
  • 做网站练手买软件网站建设
  • 网站和微信网站设计论文经济可行性分析
  • 亿唐网不做网站做品牌兴国建设局网站
  • 网站备案帐号是什么意思法制网站建设问卷调查
  • 免费网站模一级消防工程师考试内容
  • 东莞住房和城乡建设厅网站discuz做淘客网站