Java并发面试题

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

基础知识

并发编程的优缺点

为什么要使用并发编程并发编程的优点

  • 充分利用多核CPU的计算能力通过并发编程的形式可以将多核CPU的计算能力发挥到极致性能得到提升
  • 方便进行业务拆分提升系统并发能力和性能在特殊的业务场景下先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量而多线程并发编程正是开发高并发系统的基础利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型并行程序会比串行程序更适应业务需求而并发编程更能吻合这种业务拆分 。

并发编程有什么缺点

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度但是并发编程并不总是能提高程序运行速度的而且并发编程可能会遇到很多问题比如**内存泄漏、上下文切换、线程安全、死锁**等问题。

并发编程三要素是什么在 Java 程序中怎么保证多线程的运行安全

并发编程三要素线程的安全性问题体现在

原子性原子即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

可见性一个线程对共享变量的修改,另一个线程能够立刻看到。synchronized,volatile

有序性程序执行的顺序按照代码的先后顺序执行。处理器可能会对指令进行重排序

出现线程安全问题的原因

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决办法

  • JDK Atomic开头的原子类、synchronized、LOCK可以解决原子性问题
  • synchronized、volatile、LOCK可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

并行和并发有什么区别

  • 并发多个任务在同一个 CPU 核上按细分的时间片轮流(交替)执行从逻辑上来看那些任务是同时执行。
  • 并行单位时间内多个处理器或多核处理器同时处理多个任务是真正意义上的“同时进行”。
  • 串行有n个任务由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况也就不存在临界区的问题。

做一个形象的比喻

并发 = 两个队列和一台咖啡机。

并行 = 两个队列和两台咖啡机。

串行 = 一个队列和一台咖啡机。

什么是多线程多线程的优劣

多线程多线程是指程序中包含多个执行流即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

多线程的好处

可以提高 CPU 的利用率。在多线程程序中一个线程必须等待的时候CPU 可以运行其它的线程而不是等待这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的劣势

  • 线程也是程序所以线程需要占用内存线程越多占用内存也越多
  • 多线程需要协调和管理所以需要 CPU 时间跟踪线程
  • 线程之间对共享资源的访问会相互影响必须解决竞用共享资源的问题。

线程和进程区别

什么是线程和进程?

进程

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间一个进程可以有多个线程比如在Windows系统中一个运行的xx.exe就是一个进程。qq.exe

线程

进程中的一个执行任务控制单元负责当前进程中程序的执行。一个进程至少有一个线程一个进程可以运行多个线程多个线程可共享数据。

进程与线程的区别

线程具有许多传统进程所具有的特征故又称为轻型进程(Light—Weight Process)或进程元而把传统的进程称为重型进程(Heavy—Weight Process)它相当于只有一个线程的任务。在引入了线程的操作系统中通常一个进程都有若干个线程至少包含一个线程。

根本区别进程是操作系统资源分配的基本单位而线程是处理器任务调度和执行的基本单位

资源开销每个进程都有独立的代码和数据空间程序上下文程序之间的切换会有较大的开销线程可以看做轻量级的进程同一类线程共享代码和数据空间每个线程都有自己独立的运行栈和程序计数器PC线程之间切换的开销小。

包含关系如果一个进程内有多个线程则执行过程不是一条线的而是多条线线程共同完成的线程是进程的一部分所以线程也被称为轻权进程或者轻量级进程。

内存分配同一进程的线程**共享本进程的地址空间和资源而**进程之间的地址空间和资源是相互独立的

影响关系一个进程崩溃后在保护模式下不会对其他进程产生影响但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行必须依存在应用程序中由应用程序提供多个线程执行控制两者均可并发执行

调度和切换线程上下文切换比进程上下文切换要快得多。

补充

为何不使用多进程而是使用多线程

线程廉价线程启动比较快退出比较快对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权如果使用多重进程但是不可预期且测试困难。

什么是上下文切换?

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

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

上下文切换通常是计算密集型的。也就是说它需要相当可观的处理器时间在每秒几十上百次的切换中每次切换都需要纳秒量级的时间。所以上下文切换对系统来说意味着消耗大量的 CPU 时间事实上可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统包括其他类 Unix 系统有很多的优点其中有一项就是其上下文切换和模式切换的时间消耗非常少。

守护线程和用户线程有什么区别呢

守护线程和用户线程

  • 用户 (User) 线程运行在前台执行具体的任务如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程运行在后台为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行守护线程会随 JVM 一起结束工作

main 函数所在的线程就是一个用户线程啊main 函数启动的同时在 JVM 内部同时还启动了好多守护线程比如垃圾回收线程。

比较明显的区别之一是用户线程结束JVM 退出不管这个时候有没有守护线程运行。守护线程不会影响 JVM 的退出。

注意事项

  1. setDaemon(true)必须在start()方法前执行否则会抛出 IllegalThreadStateException 异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行比如读写操作或者计算逻辑
  4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行守护线程会随 JVM 一起结束工作所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

1.定义

守护线程又称为“服务线程”。在没有用户线程可服务时会自动离开。

2.优先级
守护线程的优先级比较低用于为系统中的其它对象和线程提供服务。

3.设置

通过setDaemon(true)来设置线程为“守护线程”将一个用户线程设置为

守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。

4.举例说明

垃圾回收线程就是一个经典的守护线程当我们的程序中不再有任何运行的

Thread,程序就不会再产生垃圾垃圾回收器也就无事可做所以当垃圾回收线程是

JVM上仅剩的线程时垃圾回收线程会自动离开。它始终在低级别的状态中运行用于

实时监控和管理系统中的可回收资源。

5.生命周期

守护进程Daemon是运行在后台的一种特殊进程。它独立于控制终端并且

周期性地执行某种任务或等待处理某些发生的事件。也就是

说守护线程不依赖于终端但是依赖于系统与系统“同生共死”。那Java的守护线程是

什么样子的呢。当JVM中所有的线程都是守护线程的时候JVM就可以退出了如果还有一个

或以上的非守护线程则JVM不会退出。

如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高

windows上面用任务管理器看linux下可以用 top 这个工具看。

  1. 找出cpu耗用厉害的进程pid 终端执行top命令然后按下shift+p 查找出cpu利用最厉害的pid号
  2. 根据上面第一步拿到的pid号top -H -p pid 。然后按下shift+p查找出cpu利用率最厉害的线程号比如top -H -p 1328
  3. 将获取到的线程号转换成16进制去百度转换一下就行
  4. 使用jstack工具将进程信息打印输出jstack pid号 > /tmp/t.dat比如jstack 31365 > /tmp/t.dat
  5. 编辑/tmp/t.dat文件查找线程号对应的信息

什么是线程死锁

产生死锁的必要条件:

  1. 线程互斥
  2. 不剥夺条件非抢占
  3. 占有等待不分配
  4. 环路条件循环等待

死锁是指两个或两个以上的进程线程在执行过程中由于竞争资源或者由于彼此通信而造成的一种阻塞的现象若无外力作用它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁这些永远在互相等待的进程线程称为死锁进程线程。

多个线程同时被阻塞它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞因此程序不可能正常终止。

两个或两个以上的并发进程中每个进程都持有某种资源而又等待这其他线程释放它或它或他们保持的资源在未改变这种状态之前都无法向前推进。

两个或两个以上的进程无限期阻塞、相互等待的状态

image-20200806153524720

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();
    }
}

输出结果

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

形成死锁的四个必要条件是什么

  1. 互斥条件线程(进程)对于所分配到的资源具有排它性即一个资源只能被一个线程(进程)占用直到被该线程(进程)释放
  2. 请求与保持条件一个线程(进程)因请求被占用资源而发生阻塞时对已获得的资源保持不放。
  3. 不剥夺条件线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源。
  4. 循环等待条件当发生死锁时所等待的线程(进程)必定会形成一个环路类似于死循环造成永久阻塞

如何避免线程死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏因为我们用锁本来就是想让他们互斥的临界资源需要互斥访问。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时如果申请不到可以主动释放它占有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源释放资源则反序释放。破坏循环等待条件。

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 resource2

防止死锁可以采用以下的方法

  • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock)设置超时时间超时可以退出防止死锁。
  • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
  • 尽量降低锁的使用粒度尽量不要几个功能用同一把锁。
  • 尽量减少同步的代码块。
  • 银行家算法 使用最大的可用资源与当前最大需求数比较有一个进程可以完成让该进程完成完成后释放资源可用资源增多因此我们就可以继续比较看看是否可以全部执行完成全部执行完成就证明该策略可以 不会产生死锁。

创建线程有哪几种方式

创建线程有四种方式

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 使用 Executors 工具类创建线程池

继承 Thread 类

步骤

  1. 定义一个Thread类的子类重写run方法将相关逻辑实现run()方法就是线程要执行的业务逻辑方法
  2. 创建自定义的线程子类对象
  3. 调用子类实例的star()方法来启动线程
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
    }

}
12345678
public class TheadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread(); 	
        myThread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }

}

12345678910

运行结果

main main()方法执行结束
Thread-0 run()方法正在执行...

实现 Runnable 接口

步骤

  1. 定义Runnable接口实现类MyRunnable并重写run()方法
  2. 创建MyRunnable实例myRunnable以myRunnable作为target创建Thead对象该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
12345678
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}
12345678910

执行结果

main main()方法执行完成
Thread-0 run()方法执行中...

实现 Callable 接口

步骤

  1. 创建实现Callable接口的类myCallable
  2. 以myCallable为参数创建FutureTask对象
  3. 将FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }

}
public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

执行结果

Thread-0 call()方法执行中...
返回结果 1
main main()方法执行完成

使用 Executors 工具类创建线程池

Executors提供了一系列工厂方法用于创先线程池返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutornewScheduledThreadPool后续详细介绍这四种线程池

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }


执行结果

线程任务开始执行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...

说一下 runnable 和 callable 有什么区别

相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

主要区别

  • Runnable 接口 run 方法无返回值Callable 接口 call 方法有返回值是个泛型和Future、FutureTask配合可以用来获取异步执行的结果
  • Runnable 接口 run 方法只能抛出运行时异常且无法捕获处理Callable 接口 call 方法允许抛出异常可以获取异常信息

Callalbe接口支持返回执行结果需要调用FutureTask.get()得到此方法会阻塞主进程的继续往下执行如果不调用不会阻塞。

答1是否有返回值
2是否抛异常
3落地方法不一样一个是run一个是call

线程的 run()和 start()有什么区别

每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

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

start()方法来启动一个线程真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕可以直接继续执行其他的代码 此时线程是处于就绪状态并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态 run()方法运行结束 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的只是线程里的一个函数而不是多线程的。 如果直接调用run()其实就相当于是调用了一个普通函数而已直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码所以执行路径还是只有一条根本就没有线程的特征所以在多线程执行时要使用start()方法而不是run()方法。 run()方法是由jvm创建完本地操作系统级线程后回调的方法不可以手动调用否则就是普通方法

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

这是另一个非常经典的 java 多线程面试问题而且在面试中会经常被问到。很简单但是很多人都会答不上来

new 一个 Thread线程进入了新建状态。调用 start() 方法会启动一个线程并使线程进入了就绪状态当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作然后自动执行 run() 方法的内容这是真正的多线程工作。

而直接执行 run() 方法会把 run 方法当成一个 main 线程下的普通方法去执行并不会在某个线程中执行它所以这并不是多线程工作。

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

什么是 Callable 和 Future?

Callable 接口类似于 Runnable从名字就可以看出来了但是 Runnable 不会返回结果并且无法抛出返回结果的异常而 Callable 功能更强大一些被线程执行后可以返回值这个返回值可以被 Future 拿到也就是说Future 可以拿到异步执行任务的返回值。

Future 接口表示异步任务是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果Future 用于获取结果。

什么是 FutureTask

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装由于 FutureTask 也是Runnable 接口的实现类所以 FutureTask 也可以放入线程池中。

线程的状态和基本操作

说说线程的生命周期及五种基本状态

image-20200806160828320

  1. 新建(new)新创建了一个线程对象。

  2. 可运行(runnable)线程对象创建后当调用线程对象的 start()方法该线程处于就绪状态等待被线程调度选中获取cpu的使用权。

  3. 运行(running)可运行状态(runnable)的线程获得了cpu时间片timeslice执行程序代码。注就绪状态是进入到运行状态的唯一入口也就是说线程要想进入运行状态执行首先必须处于就绪状态中

  4. 阻塞(block)处于运行状态中的线程由于某种原因暂时放弃对 CPU的使用权停止执行此时进入阻塞状态直到其进入到就绪状态才 有机会再次被 CPU 调用以进入到运行状态。

    阻塞的情况分三种
    (一). 等待阻塞运行状态中的线程执行 wait()方法JVM会把该线程放入等待队列(waitting queue)中使本线程进入到等待阻塞状态
    (二). 同步阻塞线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用)则JVM会把该线程放入锁池(lock pool)中线程会进入同步阻塞状态
    (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时线程重新转入就绪状态。

  5. 销毁线程run()、main()方法执行结束或者因异常退出了run()方法则该线程结束生命周期。死亡的线程不可再次复生。

image-20200806161442936

image-20200806161509800

image-20200806161520903

Java 中用到的线程调度算法是什么

计算机通常只有一个 CPU在任意时刻只能执行一条机器指令每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行其实是指从宏观上看各个线程轮流获得 CPU 的使用权分别执行各自的任务。在运行池中会有多个处于就绪状态的线程在等待 CPUJAVA 虚拟机的一项任务就是负责线程的调度线程调度是指按照特定机制为多个线程分配 CPU 的使用权。

有两种调度模型分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得 cpu 的使用权并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。

Java虚拟机采用抢占式调度模型是指优先让可运行池中优先级高的线程占用CPU如果可运行池中的线程优先级相同那么就随机选择一个线程使其占用CPU。处于运行状态的线程会一直运行直至它不得不放弃 CPU。

线程的调度策略

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

1线程体中调用了 yield 方法让出了对 cpu 的占用权利

2线程体中调用了 sleep 方法使线程进入睡眠状态

3线程由于 IO 操作受到阻塞

4另外一个更高优先级线程出现

5在支持时间片的系统中该线程的时间片用完

什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )

线程调度器是一个操作系统服务它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它它的执行便依赖于线程调度器的实现。

时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。

线程调度并不受到 Java 虚拟机控制所以由应用程序来控制它是更好的选择也就是说不要让你的程序依赖于线程的优先级。

请说出与线程同步以及线程调度相关的方法。

1 wait()使一个线程处于等待阻塞状态并且释放所持有的对象的锁

2sleep()使一个正在运行的线程处于睡眠状态是一个静态方法调用此方法要处理 InterruptedException 异常不释放锁

3notify()唤醒一个处于等待状态的线程当然在调用此方法的时候并不能确切的唤醒某一个等待状态的线程而是由 JVM 确定唤醒哪个线程而且与优先级无关

4notityAll()唤醒所有处于等待状态的线程该方法并不是将对象的锁给所有线程而是让它们竞争只有获得锁的线程才能进入就绪状态

sleep() 和 wait() 有什么区别

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

  • 类的不同sleep() 是 Thread线程类的静态方法wait() 是 Object类的方法。

  • 是否释放锁sleep() 不释放锁wait() 释放锁。

  • 用途不同Wait 通常被用于线程间交互/通信sleep 通常被用于暂停执行。

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

    补充 waitnotifynotifyAll详解

    https://www.cnblogs.com/moongeek/p/7631447.html

    notify 和 notifyAll的区别

    notify方法只唤醒一个等待对象的线程并使该线程开始执行。所以如果有多个线程等待一个对象这个方法只会唤醒其中一个线程选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒推荐使用notifyAll 方法。比如在生产者-消费者里面的使用每次都需要唤醒所有的消费者或是生产者以判断程序是否可以继续往下执行。

你是如何调用 wait() 方法的使用 if 块还是循环为什么

处于等待状态的线程可能会收到错误警报和伪唤醒如果不在循环中检查等待条件程序就会在没有满足结束条件的情况下退出。

wait() 方法应该在循环调用因为当线程获取到 CPU 开始执行的时候其他条件可能还没有满足所以在处理前循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码

synchronized (monitor) {
    //  判断条件谓词是否得到满足
    while(!locked) {
        //  等待唤醒
        monitor.wait();
    }
    //  处理其他的业务逻辑
}

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里

Java中任何对象都可以作为锁并且 wait()notify()等方法用于等待对象的锁或者唤醒线程在 Java 的线程中并没有可供任何对象使用的锁所以任意对象调用方法一定定义在Object类中。

wait(), notify()和 notifyAll()这些方法在同步代码块中调用

有的人会说既然是线程放弃对象锁那也可以把wait()定义在Thread类里面啊新定义的线程继承于Thread类也不需要重新定义wait()方法的实现。然而这样做有一个非常大的问题一个线程完全可以持有很多锁你一个线程放弃锁的时候到底要放弃哪个锁当然了这种设计并不是不能实现只是管理起来更加复杂。

综上所述wait()、notify()和notifyAll()方法要定义在Object类中。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用

当一个线程需要调用对象的 wait()方法的时候这个线程必须拥有该对象的锁接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的当一个线程需要调用对象的 notify()方法时它会释放这个对象的锁以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁这样就只能通过同步来实现所以他们只能在同步方法或者同步块中被调用。

Thread 类中的 yield 方法有什么作用

使当前线程从执行状态运行状态变为可执行态就绪状态。

当前线程到了就绪状态那么接下来哪个线程会从就绪状态变成执行状态呢可能是当前线程也可能是其他线程看系统的分配了。

线程的 sleep()方法和 yield()方法有什么区别

1 sleep()方法给其他线程运行机会时不考虑线程的优先级因此会给低优先级的线程以运行的机会yield()方法只会给相同优先级或更高优先级的线程以运行的机会

2 线程执行 sleep()方法后转入阻塞blocked状态而执行 yield()方法后转入就绪ready状态

3sleep()方法声明抛出 InterruptedException而 yield()方法没有声明任何异常

4sleep()方法比 yield()方法跟操作系统 CPU 调度相关具有更好的可移植性通常不建议使用yield()方法来控制并发线程的执行。

如何停止一个正在运行的线程

在java中有以下3种方法可以终止正在运行的线程

  1. 使用退出标志使线程正常退出也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止但是不推荐这个方法因为stop和suspend及resume一样都是过期作废的方法。
  3. 使用interrupt方法中断线程。

Java 中 interrupted 和 isInterrupted 方法的区别

interrupt用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

注意线程中断仅仅是置线程的中断状态位不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法也就是线程中断后会抛出interruptedException 的方法就是在监视线程的中断状态一旦线程的中断状态被置为“中断状态”就会抛出中断异常。

interrupted是静态方法查看当前中断信号是true还是false**并且清除中断信号。**如果一个线程被中断了第一次调用 interrupted 则返回 true第二次和后面的就返回 false 了。

isInterrupted查看当前中断信号是true还是false

什么是阻塞式方法

阻塞式方法是指程序会一直等待该方法完成期间不做其他事情ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前当前线程会被挂起直到得到结果之后才会返回。此外还有异步和非阻塞式方法在任务完成前就返回。

Java 中你怎样唤醒一个阻塞的线程

首先 wait()、notify() 方法是针对对象的调用任意对象的 wait()方法都将导致线程阻塞阻塞的同时也将释放该对象的锁相应地调用任意对象的 notify()方法则将随机解除该对象阻塞的线程但它需要重新获取该对象的锁直到获取成功才能往下执行

其次wait、notify 方法必须在 synchronized 块或方法中被调用并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁执行 wait 阻塞后当前线程就将之前获取的对象锁释放。

notify() 和 notifyAll() 有什么区别

如果线程调用了对象的 wait()方法那么线程便会处于该对象的等待池中等待池中的线程不会去竞争该对象的锁。

notifyAll() 会唤醒所有的线程notify() 只会唤醒一个线程。

notifyAll() 调用后会将全部线程由等待池移到锁池然后参与锁的竞争竞争成功则继续执行如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程具体唤醒哪一个线程由虚拟机控制。

如何在两个线程间共享数据

在两个线程间共享变量即可实现共享。

一般来说共享变量要求变量本身是线程安全的然后在线程内使用的时候如果有对共享变量的复合操作那么也得保证复合操作的线程安全性。

Java 如何实现多线程之间的通讯和协作

可以通过中断 和 共享变量的方式实现线程间的通讯和协作

比如说最经典的生产者-消费者模型当队列满时生产者需要等待队列有空间才能继续往里面放入商品而在等待的期间内生产者必须释放对临界资源即队列的占用权。因为生产者如果不释放对临界资源的占用权那么消费者就无法消费队列中的商品就不会让队列有空间那么生产者就会一直无限等待下去。因此一般情况下当队列满时会让生产者交出对临界资源的占用权并进入挂起状态。然后等待消费者消费了商品然后消费者通知生产者队列有空间了。同样地当队列空时消费者也必须等待等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

Java中线程通信协作的最常见的两种方式

一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

线程间直接的数据交换

三.通过管道进行线程间通信1字节流2字符流

同步方法和同步块哪个是更好的选择

同步块是更好的选择因为它不会锁住整个对象当然你也可以让它锁住整个对象。同步方法会锁住整个对象哪怕这个类中有多个不相关联的同步块这通常会导致他们停止执行并需要等待获得这个对象上的锁。

同步块更要符合开放调用的原则只在需要锁住的代码块锁住相应的对象这样从侧面来说也可以避免死锁。

请知道一条原则同步的范围越小越好。

什么是线程同步和线程互斥有哪几种实现方式

当一个线程对共享的数据进行操作时应使之成为一个”原子操作“即在没有完成相关操作之前不允许其他线程打断它否则就会破坏数据的完整性必然会得到错误的处理结果这就是线程的同步。

在多线程应用中考虑不同线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会形成线程之间的死锁。为了防止死锁的发生需要通过同步来实现线程安全。

**线程互斥是指对于共享的进程系统资源在各单个线程访问时的排它性。**当有若干个线程都要使用某一共享资源时任何时刻最多只允许一个线程去使用其它要使用该资源的线程必须等待直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。

线程间的同步方法大体可分为两类用户模式和内核模式。顾名思义内核模式就是指利用系统内核对象的单一性来进行同步使用时需要切换内核态与用户态而用户模式就是不需要切换到内核态只在用户态完成操作。

用户模式下的方法有原子操作例如一个单一的全局变量临界区。内核模式下的方法有事件信号量互斥量。

实现线程同步的方法

  • 同步代码方法sychronized 关键字修饰的方法
  • 同步代码块sychronized 关键字修饰的代码块
  • 使用特殊变量域volatile实现线程同步volatile关键字为域变量的访问提供了一种免锁机制
  • 使用重入锁实现线程同步reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

在监视器(Monitor)内部是如何做线程同步的程序应该做哪种级别的同步

在 java 虚拟机中每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联为了实现监视器的互斥功能每个对象都关联着一把锁。

一旦方法或者代码块被 synchronized 修饰那么这个部分就放入了监视器的监视区域确保一次只能有一个线程执行该部分的代码线程在获取锁之前不允许执行该部分的代码

另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案

如果你提交任务时线程池队列已满这时会发生什么

这里区分一下

1如果使用的是无界队列 LinkedBlockingQueue也就是无界队列的话没关系继续添加任务到阻塞队列中等待执行因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列可以无限存放任务

2如果使用的是有界队列比如 ArrayBlockingQueue任务首先会被添加到ArrayBlockingQueue 中ArrayBlockingQueue 满了会根据maximumPoolSize 的值增加线程数量如果增加了线程数量还是处理不过来ArrayBlockingQueue 继续满那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务默认是 AbortPolicy

什么叫线程安全servlet 是线程安全吗?

线程安全是编程中的术语指某个方法在多线程环境中被调用时能够正确地处理多个线程之间的共享变量使程序功能正确完成。

Servlet 不是线程安全的servlet 是单实例多线程的当多个线程同时访问同一个方法是不能保证共享变量的线程安全性的。

Struts2 的 action 是多实例多线程的是线程安全的每个请求过来都会 new 一个新的 action 分配给这个请求请求完成后销毁。

SpringMVC 的 Controller 是线程安全的吗不是的和 Servlet 类似的处理流程。

Struts2 好处是不用考虑线程安全问题Servlet 和 SpringMVC 需要考虑线程安全问题但是性能可以提升不用处理太多的 gc可以使用 ThreadLocal 来处理多线程的问题。

在 Java 程序中怎么保证多线程的运行安全

  • 方法一使用安全类比如 java.util.concurrent 下的类使用原子类AtomicInteger
  • 方法二使用自动锁 synchronized。
  • 方法三使用手动锁 Lock。
Lock lock = new ReentrantLock();
lock. lock();
try {
    System. out. println("获得锁");
} catch (Exception e) {
    // TODO: handle exception
} finally {
    System. out. println("释放锁");
    lock. unlock();
}

你对线程优先级的理解是什么

每一个线程都是有优先级的一般来说高优先级的线程在运行时会具有优先权但这依赖于线程调度的实现这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10)1 代表最低优先级10 代表最高优先级。

Java 的线程优先级调度会委托给操作系统去处理所以与具体的操作系统优先级有关如非特别需要一般无需设置线程优先级。

线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的而 run 方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑那么我举个例子假设 Thread2 中 new 了Thread1main 函数中 new 了 Thread2那么

1Thread2 的构造方法、静态块是 main 线程调用的Thread2 的 run()方法是Thread2 自己调用的

2Thread1 的构造方法、静态块是 Thread2 调用的Thread1 的 run()方法是Thread1 自己调用的

一个线程运行时发生异常会怎样

如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。

Java 线程数过多会造成什么异常

  • 线程的生命周期开销非常高

  • 消耗过多的 CPU

    资源如果可运行的线程数量多于可用处理器的数量那么有线程将会被闲置。大量空闲的线程会占用许多内存给垃圾回收器带来压力而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。

  • 降低稳定性JVM

    在可创建线程的数量上存在一个限制这个限制值将随着平台的不同而不同并且承受着多个因素制约包括 JVM 的启动参数、Thread 构造函数中请求栈的大小以及底层操作系统对线程的限制等。如果破坏了这些限制那么可能抛出OutOfMemoryError 异常。

并发理论

Java内存模型

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的==私有数据区域,==而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,**但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,**不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

image-20201124163731970

Java中垃圾回收有什么目的什么时候进行垃圾回收

垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的。GCROOT 不可达

垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。

如果对象的引用被置为null垃圾收集器是否会立即释放对象占用的内存

不会在下一个垃圾回调周期中这个对象将是被可回收的。

也就是说并不会立即被垃圾收集器立刻回收而是在下一次垃圾回收时才会释放其占用的内存。

重排序与数据依赖性

为什么代码会重排序

在执行程序时为了提供性能处理器和编译器常常会对指令进行重排序但是不能随意重排序不是你想怎么排序就怎么排序它需要满足以下两个条件

  • 在单线程环境下不能改变程序运行的结果
  • 存在数据依赖关系的不允许重排序

需要注意的是重排序不会影响单线程环境的执行结果但是会破坏多线程的执行语义。

as-if-serial规则和happens-before规则的区别

  • as-if-serial语义保证单线程内程序的执行结果不被改变happens-before关系保证正确同步的多线程程序的执行结果不被改变
  • as-if-serial语义给编写单线程程序的程序员创造了一个幻境单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境正确同步的多线程程序是按happens-before指定的顺序来执行的。
  • as-if-serial语义和happens-before这么做的目的都是为了在不改变程序执行结果的前提下尽可能地提高程序执行的并行度。0

并发关键字

synchronized

synchronized 的作用

在 Java 中synchronized 关键字是用来控制线程同步的就是在多线程的环境下控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。

另外在 Java 早期版本中synchronized属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的 Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程都需要操作系统帮忙完成而操作系统实现线程之间的切换时需要从用户态转换到内核态这个状态之间的转换需要相对比较长的时间时间成本相对较高这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

说说自己是怎么使用 synchronized 关键字在项目中用到了吗

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

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

总结 synchronized 关键字加到 static 静态方法和 synchronized(xxx.class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中字符串常量池具有缓存功能

双重校验锁实现对象单例线程安全

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(); 这段代码其实是分为三步执行

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空因此返回 uniqueInstance但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排保证在多线程环境下也能正常运行。

synchronized是Java中的一个关键字在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令查看相应的字节码文件。

synchronized 同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过JDK 反汇编指令 javap -c -v SynchronizedDemo

image-20200806170638656

可以看出在执行同步代码块之前之后都有一个monitor字样其中前面的是monitorenter后面的是离开monitorexit不难想象一个线程也执行同步代码块首先要获取锁而获取锁的过程就是monitorenter 在执行完代码块之后要释放锁释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢

这个主要是防止在同步代码块中线程因异常退出而锁没有得到释放这必然会造成死锁等待的线程永远获取不到锁。因此最后一个monitorexit是保证在异常情况下锁也可以得到释放避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志该标记表明线程进入该方法时需要monitorenter退出该方法时需要monitorexit。

synchronized可重入的原理

重入锁是指一个线程获取到该锁之后该线程可以继续获得该锁。

底层原理维护一个计数器当线程获取该锁时计数器加一再次获得该锁时继续加一释放锁时计数器减一当计数器值为0时表明该锁未被任何线程所持有其它线程可以竞争获取锁。

什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码执行时间非常快此时等待的线程都加锁可能是一种不太值得的操作因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快不妨让等待锁的线程不要被阻塞而是在 synchronized 的边界做忙循环这就是自旋。如果做了多次循环发现还没有获得锁再阻塞这样可能是一种更好的策略。

多线程中 synchronized 锁升级的原理是什么

synchronized 锁升级原理在锁对象的对象头里面有一个 threadid 字段在第一次访问的时候 threadid 为空jvm 让其持有偏向锁并将 threadid 设置为其线程 id再次进入的时候会先判断 threadid 是否与其线程 id 一致如果一致则可以直接使用此对象如果不一致则升级偏向锁为轻量级锁通过自旋循环一定次数来获取锁执行一定次数之后如果还没有正常获取到要使用的对象此时就会把锁从轻量级升级为重量级锁此过程就构成了 synchronized 锁的升级。

锁的升级的目的锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式使用了偏向锁升级为轻量级锁再升级到重量级锁的方式从而减低了锁带来的性能消耗。

线程 B 怎么知道线程 A 修改了变量

1volatile 修饰变量

2synchronized 修饰修改变量的方法

3wait/notify

4while 轮询

当一个线程进入一个对象的 synchronized 方法 A 之后其它线程是否可进入此对象的 synchronized 方法 B

不能。其它线程只能访问该对象的非同步方法同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁如果已经进入A 方法说明对象锁已经被取走那么试图进入 B 方法的线程就只能在等锁池注意不是等待池哦中等待对象的锁。

synchronized、volatile、CAS 比较

1synchronized 是悲观锁属于抢占式会引起其他线程阻塞。

2volatile 提供多线程共享变量可见性和禁止指令重排序优化。

3CAS 是基于冲突检测的乐观锁非阻塞

synchronized 和 Lock 有什么区别

  • 首先synchronized是Java内置关键字在JVM层面Lock是个Java类
  • synchronized 可以给类、方法、代码块加锁而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁使用简单发生异常会自动释放锁不会造成死锁而 lock 需要自己加锁和释放锁如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁而 synchronized 却无法办到。

相同点两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果不可锁重入的话就会造成死锁。同一个线程每次获取锁锁的计数器都自增1所以要等到锁的计数器下降为0时才能释放锁。

主要区别如下

  • ReentrantLock 使用起来比较灵活但是必须有释放锁的配合动作
  • ReentrantLock 必须手动获取与释放锁而 synchronized 不需要手动释放和开启锁
  • ReentrantLock 只适用于代码块锁而 synchronized 可以修饰类、方法、变量等。
  • 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁synchronized 操作的应该是对象头中 mark word

Java中每一个对象都可以作为锁这是synchronized实现同步的基础

  • 普通同步方法锁是当前实例对象
  • 静态同步方法锁是当前类的class对象
  • 同步方法块锁是括号里面的对象

两者区别

  1. 首先synchronized是java内置关键字在jvm层面Lock是个java类
  2. synchronized无法判断是否获取锁的状态Lock可以判断是否获取到锁
  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 b 线程执行过程中发生异常会释放锁)Lock需在finally中手工释放锁unlock()方法释放锁否则容易造成线程死锁
  4. 用synchronized关键字的两个线程1和线程2如果当前线程1获得锁线程2线程等待。如果线程1阻塞线程2则会一直等待下去而Lock锁就不一定会等待下去如果尝试获取不到锁线程可以不用一直等待就结束了
  5. synchronized的锁可重入、不可中断、非公平而Lock锁可重入、可判断、可公平两者皆可
  6. Lock锁适合大量同步的代码的同步问题synchronized锁适合代码少量的同步问题。

volatile

volatile 关键字的作用

对于可见性Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时它会保证修改的值会立即被更新到主存当有其他线程需要读取时它会去内存中读取新值。

为了解决缓存一致性问题有两种解决办法

1通过总线加LOCK锁的方式

2通过缓存一致协议

早期的cpu中是通过在总线加锁来解决缓存不一致的因为计算机cpu通信是通过总线来执行的如果在总线上面加锁的话就阻塞来其他cpu对该变量的访问如上面的代码在程序执行时总线发出lock指令那么只有在这段代码执行完毕后其他cpu才能读取变量执行相应的指令这样就解决了缓存一致性问题。

从实践角度而言volatile 的一个重要作用就是和 CAS 结合保证了原子性详细的可以参见 java.util.concurrent.atomic 包下的类比如 AtomicInteger。

volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

Java 中能创建 volatile 数组吗

能Java 中可以创建 volatile 类型数组不过只是一个指向数组的引用而不是整个数组。意思是如果改变引用指向的数组将会受到 volatile 的保护但是如果多个线程同时改变数组的元素volatile 标示符就不能起到之前的保护作用了。

volatile 变量和 atomic 变量有什么不同

volatile 变量可以确保先行关系即写操作会发生在后续的读操作之前, 但它并不能保证原子性。

例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。

而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一其它数据类型和引用变量也可以进行相似操作。

volatile 能使得一个非原子操作变成原子操作吗

关键字volatile的主要作用是使变量在多个线程间可见但无法保证原子性对于多个线程访问同一个实例变量需要加锁进行同步。

虽然volatile只能保证可见性不能保证原子性但用volatile修饰long和double可以保证其操作原子性。

所以从Oracle Java Spec里面可以看到

  • 对于64位的long和double如果没有被volatile修饰那么对其操作可以不是原子的。在操作的时候可以分成两步每次对32位操作。
  • 如果使用volatile修饰long和double那么其读写都是原子操作
  • 对于64位的引用地址的读写都是原子操作
  • 在实现JVM时可以自由选择是否把读写long和double作为原子操作
  • 推荐JVM实现为原子操作

volatile 修饰符的有过什么实践

单例模式

是否 Lazy 初始化是

是否多线程安全是

实现难度较复杂

描述对于Double-Check这种可能出现的问题当然这种概率已经非常小了但毕竟还是有的嘛~解决方案是只需要给instance的声明加上volatile关键字即可volatile关键字的一个作用是禁止指令重排把instance声明为volatile之后对它的写操作就会有一个内存屏障什么是内存屏障这样在它的赋值完成之前就不用会调用读操作。注意volatile阻止的不是singleton = newSingleton()这句话内部[1-2-3]的指令重排而是保证了在一个写操作[1-2-3]完成之前不会调用读操作if (instance == null)。

public class Singleton7 {

    private static volatile Singleton7 instance = null;

    private Singleton7() {}

    public static Singleton7 getInstance() {
        if (instance == null) {
            synchronized (Singleton7.class) {
                if (instance == null) {
                    instance = new Singleton7();
                }
            }
        }

        return instance;
    }

}

synchronized 和 volatile 的区别是什么

synchronized 表示只有一个线程可以获取作用对象的锁执行代码阻塞其他线程。

volatile 表示变量在 CPU 的寄存器中是不确定的必须从主存中读取。保证多线程环境下变量的可见性禁止指令重排序。

区别

  • volatile 是变量修饰符synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性不能保证原子性而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化synchronized标记的变量可以被编译器优化。
  • volatile关键字是线程同步的轻量级实现所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升实际开发中使用 synchronized 关键字的场景还是更多一些

final

什么是不可变对象它对写并发应用有什么帮助

不可变对象(Immutable Objects)即对象一旦被创建它的状态对象的数据也即对象属性值就不能改变反之即为可变对象(Mutable Objects)。

不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。

只有满足如下状态一个对象才是不可变的

  • 它的状态不能在创建后再被修改
  • 所有域都是 final 类型并且它被正确创建创建期间没有发生 this 引用的逸出。

不可变对象保证了对象的内存可见性对不可变对象的读取不需要进行额外的同步手段提升了代码执行效率。

Lock体系

Lock简介与初识AQS

Java Concurrency API 中的 Lock 接口(Lock interface)是什么对比同步它有什么优势

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构可以具有完全不同的性质并且可以支持多个相关类的条件对象。

它的优势有

1可以使锁更公平

2可以使线程在等待锁的时候响应中断

3可以让线程尝试获取锁并在无法获取锁的时候立即返回或者等待一段时间

4可以在不同的范围以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁synchronized 只支持非公平锁当然在大部分情况下非公平锁是高效的选择。

  1. Lock是显式锁手动开启和关闭锁别忘记关闭锁synchronized是隐式锁出了作用域自动释放

  2. Lock只有代码块锁synchronized有代码块锁和方法锁

  3. 使用Lock锁JVM将花费较少的时间来调度线程性能更好。并且具有更好的扩展性提供更多的子类

乐观锁和悲观锁的理解及如何实现有哪些实现方式

悲观锁总是假设最坏的情况每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上锁这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制比如**行锁表锁等读锁写锁等都是在做操作之前先上锁。**再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁

乐观锁顾名思义就是很乐观每次去拿数据的时候都认为别人不会修改所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据可以使用版本号等机制。乐观锁适用于多读的应用类型这样可以提高吞吐量像数据库提供的类似于 write_condition 机制其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

乐观锁的实现方式

1、**使用版本标识来确定读到的数据与提交时的数据是否一致。**提交后修改版本标识不一致时可以采取丢弃和再次尝试的策略。

2、java 中的 Compare and Swap 即 比较并交换 CAS 当多个线程尝试使用 CAS 同时更新同一个变量时只有其中一个线程能更新变量的值而其它线程都失败失败的线程并不会被挂起而是被告知这次竞争中失败并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置V、进行比较的预期原值A和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。主存 与希望值相同 则改变

什么是 CAS

CAS 是 compare and swap 的缩写即我们所说的比较交换。

cas 是一种基于锁的操作而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住等一个之前获得锁的线程释放锁之后下一个线程才可以访问。

而乐观锁采取了一种宽泛的态度通过某种方式不加锁来处理资源比如通过给记录加 version 来获取数据性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置V、预期原值A和新值(B)。如果内存地址里面的值和 A 的值是一样的那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的若果在第一轮循环中a 线程获取地址里面的值被b 线程修改了那么 a 线程需要自旋到下次循环才有可能机会执行。

java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

CAS 的会产生什么问题

1、ABA 问题

比如说一个线程 one 从内存位置 V 中取出 A这时候另一个线程 two 也从内存中取出 A并且 two 进行了一些操作变成了 B然后 two 又将 V 位置的数据变成 A这时候线程 one 进行 CAS 操作发现内存中仍然是 A然后 one 操作成功。尽管线程 one 的 CAS 操作成功但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

2、循环时间长开销大

对于资源竞争严重线程冲突严重的情况CAS 自旋的概率会比较大从而浪费更多的 CPU 资源效率低于 synchronized。

3、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时我们可以使用循环 CAS 的方式来保证原子操作但是对多个共享变量操作时循环 CAS 就无法保证操作的原子性这个时候就可以用锁。

什么是死锁

当线程 A 持有独占锁a并尝试去获取独占锁 b 的同时线程 B 持有独占锁 b并尝试获取独占锁 a 的情况下就会发生 AB 两个线程由于互相持有对方需要的锁而发生的阻塞现象我们称为死锁。

死锁与活锁的区别死锁与饥饿的区别

死锁是指两个或两个以上的进程或线程在执行过程中因争夺资源而造成的一种互相等待的现象若无外力作用它们都将无法推进下去。

活锁任务或者执行者没有被阻塞由于某些条件没有满足导致一直重复尝试失败尝试失败。

活锁和死锁的区别在于

处于活锁的实体是在不断的改变状态这就是所谓的“活” 而处于死锁的实体表现为等待

活锁有可能自行解开死锁则不能。

饥饿一个或者多个线程因为种种原因无法获得所需要的资源导致一直无法执行的状态。

Java 中导致饥饿的原因

1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。

2、线程被永久堵塞在一个等待进入同步块的状态因为其他线程总是能在它之前持续地对该同步块进行访问。

3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法)因为其他线程总是被持续地获得唤醒。

多线程锁的升级原理是什么

在Java中锁共有4种状态级别从低到高依次为无状态锁偏向锁轻量级锁和重量级锁状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

AQS(AbstractQueuedSynchronizer)详解与源码分析

AQS 介绍

AQS的全称为AbstractQueuedSynchronizer这个类在java.util.concurrent.locks包下面。

AQS类

AQS是一个用来构建锁和同步器的框架使用AQS能简单且高效地构造出应用广泛的大量的同步器比如我们提到的ReentrantLockSemaphore其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。当然我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

所谓AQS指的是AbstractQueuedSynchronizer它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的具体用法是通过继承AQS实现其模板方法然后将子类作为同步组件的内部类。

AQS 原理概览

AQS核心思想是如果被请求的共享资源空闲则将当前请求资源的线程设置为有效的工作线程并且将共享资源设置为锁定状态。如果被请求的共享资源被占用那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的即将暂时获取不到锁的线程加入到队列中

image-20200806184506042

AQS使用一个int成员变量来表示同步状态通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量使用volatile修饰保证线程可见性

状态信息通过protected类型的getStatesetStatecompareAndSetState进行操作

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地CAS操作将同步状态值设置为给定值update如果当前同步状态的值等于expect期望值
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive独占只有一个线程能执行如ReentrantLock。又可分为公平锁和非公平锁
  • 公平锁按照线程在队列中的排队顺序先到者先拿到锁

  • 非公平锁当线程要获取锁时无视队列顺序直接去抢锁谁抢到就是谁的

  • Share共享多个线程可同时执行如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可至于具体线程等待队列的维护如获取资源失败入队/唤醒出队等AQS已经在顶层实现好了。

AQS底层使用了模板方法模式

同步器的设计是基于模板方法模式的如果需要自定义同步器一般的方式是这样模板方法模式很经典的一个应用

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。这些重写方法很简单无非是对于共享资源state的获取和释放
  2. 将AQS组合在自定义同步组件的实现中并调用其模板方法而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别这是模板方法模式很经典的一个运用。

AQS使用了模板方法模式自定义同步器时需要重写下面几个AQS提供的模板方法

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源成功则返回true失败则返回false。
tryRelease(int)//独占方式。尝试释放资源成功则返回true失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败0表示成功但没有剩余可用资源正数表示成功且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源成功则返回true失败则返回false。

默认情况下每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的并且通常应该简短而不是阻塞。AQS类中的其他方法都是final 所以无法被其他类使用只有这几个方法可以被其他类使用。

以ReentrantLock为例state初始化为0表示未锁定状态。A线程lock()时会调用tryAcquire()独占该锁并将state+1。此后其他线程再tryAcquire()时就会失败直到A线程unlock()到state=0即释放锁为止其它线程才有机会获取该锁。当然释放锁之前A线程自己是可以重复获取此锁的state会累加这就是可重入的概念。但要注意获取多少次就要释放多么次这样才能保证state是能回到零态的。

再以CountDownLatch以例任务分为N个子线程去执行state也初始化为N注意N要与线程个数一致。这N个子线程是并行执行的每个子线程执行完后countDown()一次state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0)会unpark()主调用线程然后主调用线程就会从await()函数返回继续后余动作。

一般来说自定义同步器要么是独占方法要么是共享方式他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式如ReentrantReadWriteLock

ReentrantLock(重入锁)实现原理与公平锁非公平锁区别

什么是可重入锁ReentrantLock

ReentrantLock重入锁是实现Lock接口的一个类也是在实际编程中使用频率很高的一个锁支持重入性表示能够对共享资源能够重复加锁即当前线程获取该锁再次获取不会被阻塞。

在java关键字synchronized隐式支持重入性synchronized通过获取自增释放自减的方式实现重入。与此同时ReentrantLock还支持公平锁和非公平锁两种方式。那么要想完完全全的弄懂ReentrantLock的话主要也就是ReentrantLock同步语义的学习1. 重入性的实现原理2. 公平锁和非公平锁。

重入性的实现原理

要想支持重入性就要解决两个问题

1. 在线程获取锁的时候如果已经获取锁的线程是当前线程的话则直接再次获取成功

2. 由于锁会被获取n次那么只有锁在被释放同样的n次之后该锁才算是完全释放成功

ReentrantLock支持两种锁公平锁非公平锁

何谓公平性是针对获取锁而言的如果一个锁是公平的那么锁的获取顺序就应该符合请求上的绝对时间顺序满足FIFO

读写锁ReentrantReadWriteLock源码分析

ReadWriteLock 是什么

首先明确一下不是说 ReentrantLock 不好只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致但这样如果线程 C 在读数据、线程 D 也在读数据读数据是不会改变数据的没有必要加锁但是还是加锁了降低了程序的性能。因为这个才诞生了读写锁 ReadWriteLock。

ReadWriteLock 是一个读写锁接口读写锁是用来提升并发程序性能的锁分离技术ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现实现了读写的分离读锁是共享的写锁是独占的读和读之间不会互斥读和写、写和读、写和写之间才会互斥提升了读写的性能。

而读写锁有以下三个重要的特性

1公平选择性支持非公平默认和公平的锁获取方式吞吐量还是非公平优于公平。

2重进入读锁和写锁都支持线程重进入。

3锁降级遵循获取写锁、获取读锁再释放写锁的次序写锁能够降级成为读锁。

Condition源码分析与等待通知机制

LockSupport详解

并发容器

并发容器之ConcurrentHashMap详解(JDK1.8版本)与源码分析

什么是ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构那第一时间想到的就是它。相对于hashmap来说ConcurrentHashMap就是线程安全的map其中利用了锁分段的思想提高了并发度。

那么它到底是如何实现线程安全的

JDK 1.6版本关键要素

  • segment继承了ReentrantLock充当锁的角色为每一个segment提供了线程安全的保障
  • segment维护了哈希散列表的若干个桶每个桶由HashEntry构成的链表。

JDK1.8后ConcurrentHashMap抛弃了原有的Segment 分段锁而采用了 CAS + synchronized 来保证并发安全性

Java 中 ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的它是 ConcurrentHashMap 类构造函数的一个可选参数默认值为 16这样在多线程情况下就能避免争用。

在 JDK8 后它摒弃了 Segment锁段的概念而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度具体内容还是查看源码吧。

什么是并发容器的实现

何为同步容器可以简单地理解为通过 synchronized 来实现同步的容器如果有多个线程调用同步容器的方法它们将会串行执行。比如 VectorHashtable以及 Collections.synchronizedSetsynchronizedList 等方法返回的容器。可以通过查看 VectorHashtable 等这些同步容器的实现代码可以看到这些容器实现线程安全的方式就是将它们的状态封装起来并在需要同步的方法上加上关键字 synchronized。

并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制可以称为分段锁在这种锁机制下允许任意数量的读线程并发地访问 map并且执行读操作的线程和写操作的线程也可以并发的访问 map同时允许一定数量的写操作线程并发地修改 map所以它可以在并发环境下实现更高的吞吐量。

SynchronizedMap 和 ConcurrentHashMap 有什么区别

SynchronizedMap 一次锁住整张表来保证线程安全所以每次只能有一个线程来访为 map。

ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶诸如 getputremove 等常用操作只锁当前需要用到的桶。

这样原来只能一个线程进入现在却能同时有 16 个写线程执行并发性能的提升是显而易见的。

另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException取而代之的是在改变时 new 新的数据从而不影响原有的数据iterator 完成后再将头指针替换为新的数据 这样 iterator线程可以使用原来老的数据而写线程也可以并发的完成改变。

并发容器之CopyOnWriteArrayList详解

CopyOnWriteArrayList 是什么可以用于什么应用场景有哪些优缺点

CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的我认为这句话不严谨缺少一个前提条件那就是非复合场景下操作它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中写入将导致创建整个底层数组的副本而源数组将保留在原地使得复制的数组在被修改时读取操作可以安全地执行。

CopyOnWriteArrayList 的使用场景

通过源码分析我们看出它的优缺点比较明显所以使用场景也就比较明显。就是合适读多写少的场景。

CopyOnWriteArrayList 的缺点

  1. 由于写操作的时候需要拷贝数组会消耗内存如果原数组的内容比较多的情况下可能导致 young gc 或者 full gc。
  2. 不能用于实时读的场景像拷贝数组、新增元素都需要时间所以调用一个 set 操作后读取到数据可能还是旧的虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
  3. 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据万一数据稍微有点多每次 add/set 都要重新复制数组这个代价实在太高昂了。在高性能的互联网应用中这种操作分分钟引起故障。

CopyOnWriteArrayList 的设计思想

  1. 读写分离读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路来解决并发冲突

并发容器之ThreadLocal详解

https://www.jianshu.com/p/377bb840802f

ThreadLocal 是什么有哪些使用场景

ThreadLocal 是一个本地线程副本变量工具类在每个线程中都创建了一个 ThreadLocalMap 对象简单说 ThreadLocal 就是一种以空间换时间的做法每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式避免资源在多线程间共享。

我们使用**ThreadLocal解决线程局部变量统一定义问题**多线程数据不能共享。InheritableThreadLocal特例除外不能解决并发问题。解决了基于类级别的变量定义每一个线程单独维护自己线程内的变量值存、取、删的功能

原理线程局部变量是局限于线程内部的变量属于线程自身所有不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量是一种实现线程安全的方式。但是在管理环境下如 web 服务器使用线程局部变量的时候要特别小心在这种情况下工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放Java 应用就存在内存泄露的风险。

image-20200825092651520

首先主线程定义的两个ThreadLocal变量和两个子线程——线程A和线程B。

线程A和线程B分别持有一个ThreadLocalMap用于保存自己独立的副本主线程的ThreadLocal中封装了get()和set()之类的方法。

在线程A和线程B中调用ThreadLocal的set方法会首先通过getMap(Thread.currentThread)获得线程A或者是线程B持有的ThreadLocalMap,在调用map.put()方法并将ThreadLocal作为key。

get()方法和set()方法原理类似也是先获取当前调用线程的ThreadLocalMap,再从map中获取value并将ThreadLocal作为key。

为什么要弱引用

因为如果这里使用普通的key-value形式来定义存储结构实质上就会造成节点的生命周期与线程强绑定只要线程没有销毁那么节点在GC分析中一直处于可达状态没办法被回收而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档比软引用更加弱一些如果一个对象没有强引用链可达那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达则随着它被垃圾回收在ThreadLocalMap里对应的Entry的键值会失效这为ThreadLocalMap本身的垃圾清理提供了便利。

由于ThreadLocalMap使用线性探测法来解决散列冲突所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
关于开放寻址、线性探测等内容可以参考网上资料或者TAOCP《计算机程序设计艺术》第三卷的6.4章节。

至此我们已经可以大致勾勒出ThreadLocalMap的内部存储结构。下面是我绘制的示意图。虚线表示弱引用实线表示强引用。

image-20200825114256122

我们来回顾一下从ThreadLocal读一个值可能遇到的情况
根据入参threadLocal的threadLocalHashCode对表容量取模得到index

  • 如果index对应的slot就是要读的threadLocal则直接返回结果
  • 调用getEntryAfterMiss线性探测过程中每碰到无效slot调用expungeStaleEntry进行段清理如果找到了key则返回结果entry
  • 没有找到key返回null

我们来回顾一下ThreadLocal的set方法可能会有的情况

  • 探测过程中slot都不无效并且顺利找到key所在的slot直接替换即可
  • 探测过程中发现有无效slot调用replaceStaleEntry效果是最终一定会把key和value放在这个slot并且会尽可能清理无效slot
    • 在replaceStaleEntry过程中如果找到了key则做一个swap把它放到那个无效slot中value置为新值
    • 在replaceStaleEntry过程中没有找到key直接在无效slot原地放entry
  • 探测没有发现key则在连续段末尾的后一个空位置放上entry这也是线性探测法的一部分。放完后做一次启发式清理如果没清理出去key并且当前table大小已经超过阈值了则做一次rehashrehash函数会调用一次全量清理slot方法也即expungeStaleEntries如果完了之后table大小超过了threshold - threshold / 4则进行扩容2倍

注意点

1.ThreadLocal类封装了getMap()、Set()、Get()、Remove()4个核心方法。

**2.通过**getMap()获取**每个子线程Thread持有自己的ThreadLocalMap实例, 因此它们是不存在并发竞争的。可以理解为每个线程有自己的变量副本。

**

3.ThreadLocalMap中Entry[]数组存储数据初始化长度16后续每次都是2倍扩容。主线程中定义了几个变量Entry[]才有几个key。

4.Entry的key是对ThreadLocal的弱引用当抛弃掉ThreadLocal对象时垃圾收集器会忽略这个key的引用而清理掉ThreadLocal对象 防止了内存泄漏。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作不会出现 A 线程关了 B线程正在使用的 Connection 还有 Session 管理 等问题。

ThreadLocal 使用例子

public class TestThreadLocal {
    
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
    
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i <5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
    
}

什么是线程局部变量

线程局部变量是局限于线程内部的变量属于线程自身所有不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量是一种实现线程安全的方式。但是在管理环境下如 web 服务器使用线程局部变量的时候要特别小心在这种情况下工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放Java 应用就存在内存泄露的风险。

ThreadLocal内存泄漏分析与解决方案

ThreadLocal造成内存泄漏的原因

ThreadLocalMap 中使用的 key 为 ThreadLocal弱引用,而 value 是强引用。所以如果 ThreadLocal 没有被外部强引用的情况下在垃圾回收的时候key 会被清理掉而 value 不会被清理掉。这样一来ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话value 永远无法被GC 回收这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况在调用 set()get()remove() 方法的时候会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题其实就是要看对内存泄漏的准确定义是什么。
认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了我们往里面放的value对于**【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象->Entry数组->某个entry.value】**这样一条强引用链是可达的因此value不会被回收。
认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。

之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中一个线程的寿命很长大对象长期不被回收影响系统运行效率与安全。如果线程不会复用用完即销毁了也不会有ThreadLocal引发内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为unintentional object retention(无意识的对象保留。

当我们仔细读过ThreadLocalMap的源码我们可以推断如果在使用的ThreadLocal的过程中显式地进行remove是个很好的编码习惯这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象断开value强引用从而大对象被收集器回收。

但无论如何我们应该考虑到何时调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat在代码中对web api作一个切面存放一些如用户名等用户信息在连接点方法结束后再显式调用remove。

ThreadLocal内存泄漏解决方案

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据。
  • 在使用线程池的情况下没有及时清理ThreadLocal不仅是内存泄漏的问题更严重的是可能导致业务逻辑出现问题。所以使用ThreadLocal就跟加锁完要解锁一样用完就清理。

InheritableThreadLocal原理

ThreadLocal本身是线程隔离的InheritableThreadLocal提供了一种父子线程之间的数据共享机制。

它的具体实现是在Thread类中除了threadLocals外还有一个inheritableThreadLocals对象。

还是比较简单的做的事情就是以父线程的inheritableThreadLocalMap为数据源过滤出有效的entry初始化到自己的inheritableThreadLocalMap中。其中childValue可以被重写。

需要注意的地方是InheritableThreadLocal只是在子线程创建的时候会去拷一份父线程的inheritableThreadLocals。如果父线程是在子线程创建后再set某个InheritableThreadLocal对象的值对子线程是不可见的。

并发容器之BlockingQueue详解

什么是阻塞队列阻塞队列的实现原理是什么如何使用阻塞队列来实现生产者-消费者模型

阻塞队列BlockingQueue是一个支持两个附加操作的队列。

这两个附加的操作是在队列为空时获取元素的线程会等待队列变为非空。当队列满时存储元素的线程会等待队列可用

阻塞队列常用于生产者和消费者的场景生产者是往队列里添加元素的线程消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器而消费者也只从容器里拿元素。

JDK7 提供了 7 个阻塞队列。分别是

ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列。

DelayQueue一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue一个不存储元素的阻塞队列。

LinkedTransferQueue一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque一个由链表结构组成的双向阻塞队列。

Java 5 之前实现同步存取时可以使用普通的一个集合然后在使用线程的协作和线程同步可以实现生产者消费者模式主要的技术就是用好wait,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后可以使用阻塞队列来实现此方式大大简少了代码量使得多线程编程更加容易安全方面也有保障。

BlockingQueue 接口是 Queue 的子接口它的主要用途并不是作为容器而是作为线程同步的的工具因此他具有一个很明显的特性当生产者线程试图向 BlockingQueue 放入元素时如果队列已满则线程被阻塞当消费者线程试图从中取出一个元素时如果队列为空则该线程会被阻塞正是因为它所具有这个特性所以在程序中多个线程交替向 BlockingQueue 中放入元素取出元素它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析读取数据的线程不断将数据放入队列然后解析线程不断从队列取数据解析。

线程池

Executors类创建四种常见线程池

java线程池工作过程

https://www.cnblogs.com/gaopengpy/p/12149060.html

线程池的原理

https://blog.csdn.net/weixin_28760063/article/details/81266152

合理配置线程池的大小

如果是CPU密集型任务就需要尽量压榨CPU参考值可以设为 NCPU+1

如果是IO密集型任务参考值可以设置为2*NCPU

通过一次代码校验发现 线程池不建议使用Executors去创建而是通过ThreadPoolExecutor方式的原因 顺便总结线程优缺点

https://blog.csdn.net/qq_31615049/article/details/80756781

Executors工厂创建线程池
newCachedThreadPool

创建一个可缓存线程池

优点很灵活弹性的线程池线程管理用多少线程给多大的线程池不用后及时回收用则新建

缺点一旦线程无限增长会导致内存溢出。

newFixedThreadPool

优点创建一个固定大小线程池超出的线程会在队列中等待。

缺点不支持自定义拒绝策略大小固定难以扩展

newScheduledThreadPool

优点创建一个固定大小线程池可以定时或周期性的执行任务。

缺点任务是单线程方式执行一旦一个任务失败其他任务也受影响

newSingleThreadExecutor

优点创建一个单线程的线程池保证线程的顺序执行

缺点不适合并发。。不懂为什么这种操作要用线程池。。为什么不直接用队列

统一缺点不支持自定义拒绝策略。

什么是线程池有哪几种创建方式

池化技术相比大家已经屡见不鲜了线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗提高对资源的利用率。

在面向对象编程中创建和销毁对象是很费时间的因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此虚拟机将试图跟踪每一个对象以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数特别是一些很耗资源的对象创建和销毁这就是”池化资源”技术产生的原因。

线程池顾名思义就是事先创建若干个可执行的线程放入一个池容器中需要的时候从池中获取线程不用自行创建使用完毕不需要销毁线程而是放回池中从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的尤其是对于线程池的原理不是很清楚的情况下因此在工具类 Executors 面提供了一些静态工厂方法生成一些常用的线程池如下所示

1newSingleThreadExecutor(1 : 1)创建一个单线程的线程池。这个线程池只有一个线程在工作也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2newFixedThreadPool(1:n)创建固定大小的线程池。每次提交一个任务就创建一个线程直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变如果某个线程因为执行异常而结束那么线程池会补充一个新线程。如果希望在服务器上使用线程池建议使用 newFixedThreadPool方法来创建线程池这样能获得更好的性能。

3 newCachedThreadPool(n : m)创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程那么就会回收部分空闲60 秒不执行任务的线程当任务数增加时此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制线程池大小完全依赖于操作系统或者说 JVM能够创建的最大线程大小。

4newScheduledThreadPool创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池有什么优点

  • 降低资源消耗重用存在的线程减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制最大并发线程数提高系统资源的使用率同时避免过多资源竞争避免堵塞。当任务到达时任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源如果无限制的创建不仅会消耗系统资源还会降低系统的稳定性使用线程池可以进行统一的分配调优和监控。
  • 附加功能提供定时执行、定期执行、单线程、并发数控制等功能。

综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

线程池都有哪些状态

  • RUNNING这是最正常的状态接受新的任务处理等待队列中的任务。
  • SHUTDOWN不接受新的任务提交但是会继续处理等待队列中的任务。
  • STOP不接受新的任务提交不再处理等待队列中的任务中断正在执行任务的线程。
  • TIDYING所有的任务都销毁了workCount 为 0线程池的状态在转换为 TIDYING 状态时会执行钩子方法 terminated()。
  • TERMINATEDterminated()方法结束后线程池的状态就会变成这个。

什么是 Executor 框架为什么使用 Executor 框架

Executor 框架是一个根据一组执行策略调用调度执行和控制的异步任务的框架。

每次执行任务创建线程 new Thread()比较消耗性能创建一个线程是比较耗时、耗资源的而且无限制的创建线程会引起应用程序内存溢出。

所以创建一个线程池是个更好的的解决方案因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors 框架可以非常方便的创建一个线程池。

在 Java 中 Executor 和 Executors 的区别

  • Executors 工具类的不同方法按照我们的需求创建了不同的线程池来满足业务的需求。
  • Executor 接口对象能执行我们的线程任务。
  • ExecutorService 接口继承了 Executor 接口并进行了扩展提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
  • 使用 ThreadPoolExecutor 可以创建自定义线程池。
  • Future 表示异步计算的结果他提供了检查计算是否完成的方法以等待计算的完成并可以使用 get()方法获取计算的结果。

线程池中 submit() 和 execute() 方法有什么区别

接收参数execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

返回值submit()方法可以返回持有计算结果的 Future 对象而execute()没有

异常处理submit()方便Exception处理

什么是线程组为什么在 Java 中不推荐使用

ThreadGroup 类可以把线程归属到某一个线程组中线程组中可以有线程对象也可以有线程组组中还可以有线程这样的组织结构有点类似于树的形式。

线程组和线程池是两个不同的概念他们的作用完全不同前者是为了方便线程的管理后者是为了管理线程的生命周期复用线程减少创建销毁线程的开销。

为什么不推荐使用线程组因为使用有很多的安全隐患吧没有具体追究如果需要使用推荐使用线程池。

线程池之ThreadPoolExecutor详解

Executors和ThreaPoolExecutor创建线程池的区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建而是通过 ThreadPoolExecutor 的方式这样的处理方式让写的同学更加明确线程池的运行规则规避资源耗尽的风险

Executors 各个方法的弊端

  • newFixedThreadPool 和 newSingleThreadExecutor:
    主要问题是堆积的请求处理队列可能会耗费非常大的内存甚至 OOM。
  • newCachedThreadPool 和 newScheduledThreadPool:
    主要问题是线程数最大数是 Integer.MAX_VALUE可能会创建数量非常多的线程甚至 OOM。

ThreaPoolExecutor创建线程池方式只有一种就是走它的构造函数参数自己指定

你知道怎么创建线程池吗

创建线程池的方式有多种这里你只需要答 ThreadPoolExecutor 即可。

ThreadPoolExecutor() 是最原始的线程池创建也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。

ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数

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

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime线程池中的线程数量大于 corePoolSize 的时候如果这时没有新的任务提交核心线程外的线程不会立即销毁而是会等待直到等待的时间超过了 keepAliveTime才会被回收销毁
  2. unit keepAliveTime 参数的时间单位。
  3. threadFactory为线程池提供创建新线程的线程工厂
  4. handler 线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

ThreadPoolExecutor饱和策略

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度影响程序的整体性能。另外这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话你可以选择这个策略。 返回给调用者
  • ThreadPoolExecutor.DiscardPolicy不处理新任务直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

举个例子 Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时此策略为我们提供可伸缩队列。这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出比较简单的原因这里就不贴代码了

image-20200806202255738

为了让大家更清楚上面的面试题中的一些概念我写了一个简单的线程池 Demo。

首先创建一个 Runnable 接口的实现类当然也可以是 Callable 接口我们上面也说了两者的区别。

import java.util.Date;

/**
 * 这是一个简单的Runnable类需要大约5秒钟来执行其任务。
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

编写测试程序我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象WorkerThread类实现了Runnable 接口
            Runnable worker = new MyRunnable("" + i);
            //执行Runnable
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

可以看到我们上面的代码指定了

  1. corePoolSize: 核心线程数为 5。
  2. maximumPoolSize 最大线程数 10
  3. keepAliveTime : 等待时间为 1L。
  4. unit: 等待时间的单位为 TimeUnit.SECONDS。
  5. workQueue任务队列为 ArrayBlockingQueue并且容量为 100;
  6. handler:饱和策略为 CallerRunsPolicy

Output

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

线程池之ScheduledThreadPoolExecutor详解

FutureTask详解

原子操作类

什么是原子操作在 Java Concurrency API 中有哪些原子类(atomic classes)

原子操作atomic operation意为”不可被中断的一个或一系列操作” 。

处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set或是 Compare & Swap现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

int++并不是一个原子操作所以当一个线程读取它的值并加 1 时另外一个线程有可能会读到之前的值这就会引发错误。

为了解决这个问题必须保证增加操作是原子的在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下当有多个线程同时执行这些类的实例包含的方法时具有排他性即当某个线程进入方法执行其中的指令时不会被其他线程打断而别的线程就像自旋锁一样一直等到该方法执行完成才由 JVM 从等待队列中选择另一个线程进入这只是一种逻辑上的理解。

原子类AtomicBooleanAtomicIntegerAtomicLongAtomicReference

原子数组AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

原子属性更新器AtomicLongFieldUpdaterAtomicIntegerFieldUpdaterAtomicReferenceFieldUpdater

解决 ABA 问题的原子类AtomicMarkableReference通过引入一个 boolean来反映中间有没有变过AtomicStampedReference通过引入一个 版本号来累加来反映中间有没有变过

说一下 atomic 的原理

Atomic包中的类基本的特性就是在多线程环境下当有多个线程同时对单个包括基本类型及引用类型变量进行操作时具有排他性即当多个线程同时对该变量的值进行更新时仅有一个线程能成功而未成功的线程可以向自旋锁一样继续尝试一直等到执行成功。

AtomicInteger 类的部分源码

// setup to use Unsafe.compareAndSwapInt for updates更新操作时提供“比较并替换”的作用
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
	try {
		valueOffset = unsafe.objectFieldOffset
		(AtomicInteger.class.getDeclaredField("value"));
	} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作从而避免 synchronized 的高开销执行效率大为提升。

CAS的原理是拿期望的值和原本的一个值作比较如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法这个方法是用来拿到“原来的值”的内存地址返回值是 valueOffset。另外 value 是一个volatile变量在内存中可见因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

并发工具

并发工具之CountDownLatch与CyclicBarrier

在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别

CountDownLatch与CyclicBarrier都是用于控制并发的工具类都可以理解成维护的就是一个计数器但是这两者还是各有不同侧重点的

  • CountDownLatch班长关门 相当于 - - 操作一般用于某个线程A等待若干个其他线程执行完任务之后它才执行而CyclicBarrier一般用于一组线程互相等待至某个状态然后这一组线程再同时执行
  • CycliBarriar集七龙珠相当于 + + 操作 强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等等大家都完成再携手共进。
  • 调用CountDownLatch的countDown方法后当前线程并不会阻塞会继续往下执行而调用CyclicBarrier的await方法会阻塞当前线程直到CyclicBarrier指定的线程全部都到达了指定点的时候才能继续往下执行
  • CountDownLatch方法比较少操作比较简单而CyclicBarrier提供的方法更多比如能够通过getNumberWaiting()isBroken()这些方法获取当前多个线程的状态并且CyclicBarrier的构造方法可以传入barrierAction指定当所有线程都到达时执行的业务功能
  • CountDownLatch是不能复用的而CyclicLatch是可以复用的。

并发工具之Semaphore与Exchanger

Semaphore 有什么作用

Semaphore 停车场 抢车位就是一个信号量它的作用是限制某段代码块的并发数。Semaphore有一个构造函数可以传入一个 int 型整数 n表示某段代码最多只有 n 个线程可以访问如果超出了 n那么请等待等到某个线程执行完毕这段代码块下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1相当于变成了一个 synchronized 了。

Semaphore(信号量)-允许多个线程同时访问 synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源Semaphore(信号量)可以指定多个线程同时访问某个资源。

* 控制多线程的并发数   共享资源的互斥使用另一个用于并发线程数的控制  抢车位
* 在信号量上我们定义的两种操作
* acquire(获取) 当一个线程调用acquire操作时它要么通过成功获取信号量(信号量减1)
* 要么一直等下去直到有线程释放信号量或者超时
* release(释放) 实际上会将信号量的值加1然后唤醒等待的线程
*  读读可共享  写读写写要独占
* 信号量主要用于两个目的一个是用于多个共享资源的互斥使用另一个用于并发线程数的控制

什么是线程间交换数据的工具Exchanger

Exchanger是一个用于线程间协作的工具类用于两个线程间交换数据。它提供了一个交换的同步点在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的如果一个线程先执行exchange方法那么它会同步等待另一个线程也执行exchange方法这个时候两个线程就都达到了同步点两个线程就可以交换数据。

常用的并发工具类有哪些

  • Semaphore(信号量)-允许多个线程同时访问 synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch(倒计时器) CountDownLatch是一个同步工具类用来协调多个线程之间的同步。这个工具通常用来控制线程等待它可以让某一个线程等待直到倒计时结束再开始执行。
  • CyclicBarrier(循环栅栏) CyclicBarrier 和 CountDownLatch 非常类似它也可以实现线程间的技术等待但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用Cyclic的屏障Barrier。它要做的事情是让一组线程到达一个屏障也可以叫同步点时被阻塞直到最后一个线程到达屏障时屏障才会开门所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties)其参数表示屏障拦截的线程数量每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障然后当前线程被阻塞。

AQS

AQS源码分析

https://blog.csdn.net/java_lyvee/article/details/98966684?spm=1001.2014.3001.5501

所谓AQS指的是AbstractQueuedSynchronizer它提供了一种实现**阻塞锁和一系列依赖FIFO等待队列的同步器的框架**ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的具体用法是通过继承AQS实现其模板方法然后将子类作为同步组件的内部类。

image-20200806204105691

AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO线程等待队列(多线程竞争state被阻塞时会进入此队列)。

State

首先说一下共享资源变量state它是int数据类型的其访问方式有3种

  • getState()
  • setState(int newState)
  • compareAndSetState(int expect, int update) cas

上述3种方式均是原子操作其中compareAndSetState()的实现依赖于Unsafe的compareAndSwapInt()方法。

private volatile int state;

// 具有内存读可见性语义
protected final int getState() {
    return state;
}

// 具有内存写可见性语义
protected final void setState(int newState) {
    state = newState;
}

// 具有内存读/写可见性语义
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

资源的共享方式分为2种

  • 独占式(Exclusive)

只有单个线程能够成功获取资源并执行如ReentrantLock。

  • 共享式(Shared)

多个线程可成功获取资源并执行如Semaphore/CountDownLatch等。

AQS将大部分的同步逻辑均已经实现好继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以主要包括下面方法

  • tryAcquire(int)独占方式。尝试获取资源成功则返回true失败则返回false。
  • tryRelease(int)独占方式。尝试释放资源成功则返回true失败则返回false。
  • tryAcquireShared(int)共享方式。尝试获取资源。负数表示失败0表示成功但没有剩余可用资源正数表示成功且有剩余资源。
  • tryReleaseShared(int)共享方式。尝试释放资源如果释放后允许唤醒后续等待结点返回true否则返回false。
  • isHeldExclusively()该线程是否正在独占资源。只有用到condition才需要去实现它。

AQS需要子类复写的方法均没有声明为abstract目的是避免子类需要强制性覆写多个方法因为一般自定义同步器要么是独占方法要么是共享方法只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。

当然AQS也支持子类同时实现独占和共享两种模式如ReentrantReadWriteLock。

CLH队列(FIFO)

AQS是通过内部类Node来实现FIFO队列的源代码解析如下

static final class Node {
    
    // 表明节点在共享模式下等待的标记
    static final Node SHARED = new Node();
    // 表明节点在独占模式下等待的标记
    static final Node EXCLUSIVE = null;

    // 表征等待线程已取消的
    static final int CANCELLED =  1;
    // 表征需要唤醒后续线程
    static final int SIGNAL    = -1;
    // 表征线程正在等待触发条件(condition)
    static final int CONDITION = -2;
    // 表征下一个acquireShared应无条件传播
    static final int PROPAGATE = -3;

    /**
     *   SIGNAL: 当前节点释放state或者取消后将通知后续节点竞争state。
     *   CANCELLED: 线程因timeout和interrupt而放弃竞争state当前节点将与state彻底拜拜
     *   CONDITION: 表征当前节点处于条件队列中它将不能用作同步队列节点直到其waitStatus被重置为0
     *   PROPAGATE: 表征下一个acquireShared应无条件传播
     *   0: None of the above
     */
    volatile int waitStatus;
    
    // 前继节点
    volatile Node prev;
    // 后继节点
    volatile Node next;
    // 持有的线程
    volatile Thread thread;
    // 链接下一个等待条件触发的节点
    Node nextWaiter;

    // 返回节点是否处于Shared状态下
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 返回前继节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
    // Shared模式下的Node构造函数
    Node() {  
    }

    // 用于addWaiter
    Node(Thread thread, Node mode) {  
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 用于Condition
    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

可以看到waitStatus非负的时候表征不可用正数代表处于等待状态所以waitStatus只需要检查其正负符号即可不用太多关注特定值。

至此独占模式下线程获取资源acquire的代码就跟完了总结一下过程

  1. 首先线程通过tryAcquire(arg)尝试获取共享资源若获取成功则直接返回
  2. 若不成功则将该线程以独占模式添加到等待队列尾部tryAcquire(arg)由继承AQS的自定义同步器来具体实现
  3. 当前线程加入等待队列后会通过acquireQueued方法基于CAS自旋不断尝试获取资源直至获取到资源
  4. 若在自旋过程中线程被中断过acquireQueued方法会标记此次中断并返回true。
  5. 若acquireQueued方法获取到资源后返回true则执行线程自我中断操作selfInterrupt()。

后继节点的阻塞线程被唤醒后就进入到acquireQueued()的if (p == head && tryAcquire(arg))的判断中此时被唤醒的线程将尝试获取资源。

当然如果被唤醒的线程所在节点的前继节点不是头结点经过shouldParkAfterFailedAcquire的调整也会移动到等待队列的前面直到其前继节点为头结点。

可以发现doAcquireShared与独占模式下的acquireQueued大同小异主要有2点不同

  1. doAcquireShared将线程的自我中断操作放在了方法体内部
  2. 当线程获取到资源后doAcquireShared会将当前线程所在的节点设为头结点若资源有剩余则唤醒后续节点比acquireQueued多了个唤醒后续节点的操作。

上述方法体现了共享的本质即当前线程吃饱了后若资源有剩余会招呼后面排队的来一起吃好东西要大家一起分享嘛哈哈。

整个获取/释放资源的过程是通过传播完成的如最开始有10个资源线程A、B、C分别需要5、4、3个资源。

  • A线程获取到5个资源其发现资源还剩余5个则唤醒B线程
  • B线程获取到4个资源其发现资源还剩余1个唤醒C线程
  • C线程尝试取3个资源但发现只有1个资源继续阻塞
  • A线程释放1个资源其发现资源还剩余2个故唤醒C线程
  • C线程尝试取3个资源但发现只有2个资源继续阻塞
  • B线程释放2个资源其发现资源还剩余4个唤醒C线程
  • C线程获取3个资源其发现资源还剩1个继续唤醒后续等待的D线程

锁升级过程

https://blog.csdn.net/tongdanping/article/details/79647337

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗引入了“偏向锁”和“轻量级锁”在Java SE 1.6中锁一共有4种状态级别从低到高依次是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略目的是为了提高获得锁和释放锁的效率。

一、偏向锁

1、对象头

synchronized用的锁是存在Java对象头里的。如果对象是数组类型则虚拟机用3个字宽Word存储对象头如果对象是非数组类型则用2字宽存储对象头。在32位虚拟机中1字宽等于4字节即32bit如表2-2所示

image-20200825161726613

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如表2-3所示

image-20200825161746307

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据如表2-4所示

image-20200825161810997

在64位虚拟机下Mark Word是64bit大小的其存储结构如表2-5所示

image-20200825161913256

大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得为了让线程获得锁的代价更低而引入偏向锁。
当一个线程访问同步代码块并获取锁时会在对象头和栈帧中的锁记录里存储锁偏向的线程ID以后该线程再进入和退出同步块时不需要进行CAS操作来加锁和解锁只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
如果测试成功表示线程已经获得了锁。

如果测试失败则需要再测试一下Mark Word中偏向锁的标识是否设置为1表示指向当前进程
如果没有则使用CAS竞争锁如果设置了则尝试使用CAS将对象头的偏向锁指向当前进程。

二、自旋锁轻量级锁

轻量级锁是由偏向所升级来的偏向锁运行在一个线程进入同步块的情况下当第二个线程加入锁争用的时候偏向锁就会升级为轻量级锁

轻量级锁的加锁过程

  1. 在代码进入同步块的时候如果同步对象锁状态为无锁状态锁标志位为“01”状态是否为偏向锁为“0”虚拟机首先将在当前线程的栈帧中建立一个名为锁记录Lock Record的空间用于存储锁对象目前的Mark Word的拷贝官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中
  3. 拷贝成功后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针并将Lock record里的owner指针指向object mark word。如果更新成功则执行步骤4否则执行步骤5。
  4. 如果这个更新动作成功了那么这个线程就拥有了该对象的锁并且对象Mark Word的锁标志位设置为“00”即表示此对象处于轻量级锁定状态。
  5. 如果这个更新操作失败了虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧如果是就说明当前线程已经拥有了这个对象的锁那就可以直接进入同步块继续执行。否则说明多个线程竞争锁轻量级锁就要膨胀为重量级锁锁标志的状态值变为“10”Mark Word中存储的就是指向重量级锁互斥量的指针后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁自旋就是为了不让线程阻塞而采用循环去获取锁的过程。

三、重量级锁

轻量级锁膨胀之后就升级为重量级锁了。
重量级锁是依赖对象内部的monitor锁来实现的而monitor又依赖操作系统的MutexLock(互斥锁)来实现的所以重量级锁也被成为互斥锁。

当线程1访问代码块并获取锁对象时会在java对象头和栈帧中记录偏向的锁的threadID因为偏向锁不会主动释放锁因此以后线程1再次获取锁的时候需要比较当前线程的threadID和Java对象头中的threadID是否一致如果一致还是线程1获取锁对象则无需使用CAS来加锁、解锁如果不一致其他线程如线程2要竞争锁对象而偏向锁不会主动释放因此还是存储的线程1的threadID那么需要查看Java对象头中记录的线程1是否存活如果没有存活那么锁对象被重置为无锁状态其它线程线程2可以竞争将其设置为偏向锁如果存活那么立刻查找该线程线程1的栈帧信息如果还是需要继续持有这个锁对象那么暂停当前线程1撤销偏向锁升级为轻量级锁 如果线程1 不再使用该锁对象那么将锁对象状态设为无锁状态重新偏向新的线程偏向锁。

偏向锁过程

  1. 访问Mark Word中偏向锁的标识是否设置成1锁标识位是否为01确认偏向状态
  2. 如果为可偏向状态则判断当前线程ID是否为偏向线程
  3. 如果偏向线程未只想当前线程则通过cas操作竞争锁如果竞争成功则操作Mark Word中线程ID设置为当前线程ID
  4. 如果cas偏向锁获取失败则挂起当前偏向锁线程偏向锁升级为轻量级锁。

为什么要引入轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态代价较大如果刚刚阻塞不久这个锁就被释放了那这个代价就有点得不偿失了因此这个时候就干脆不阻塞这个线程让它自旋这等待锁释放。

轻量级锁过程

  1. 线程由偏向锁升级为轻量级锁时会先把锁的对象头MarkWord复制一份到线程的栈帧中建立一个名为锁记录空间Lock Record用于存储当前Mark Word的拷贝。
  2. 虚拟机使用cas操作尝试将对象的Mark Word指向Lock Record的指针并将Lock record里的owner指针指对象的Mark Word。
  3. 如果cas操作成功则该线程拥有了对象的轻量级锁。第二个线程cas自旋等待锁线程释放锁。
  4. 如果多个线程竞争锁轻量级锁要膨胀为重量级锁Mark Word中存储的就是指向重量级锁互斥量的指针。其他等待线程进入阻塞状态。

轻量级锁什么时候升级为重量级锁

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间称为DisplacedMarkWord然后使用CAS把对象头中的内容替换为线程1存储的锁记录DisplacedMarkWord的地址

如果在线程1复制对象头的同时在线程1CAS之前线程2也准备获取锁复制了对象头到线程2的锁记录空间中但是在线程2CAS的时候发现线程1已经把对象头换了线程2的CAS失败那么线程2就尝试使用自旋锁来等待线程1释放锁

但是如果自旋的时间太长也不行因为自旋是要消耗CPU的因此自旋的次数是有限制的比如10次或者100次如果自旋次数到了线程1还没有释放锁或者线程1还在执行线程2还在自旋等待这时又有一个线程3过来竞争这个锁对象那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞防止CPU空转。

synchronized的执行过程

  1. 检测Mark Word里面是不是当前线程的ID如果是表示当前线程处于偏向锁
  2. 如果不是则使用CAS将当前线程的ID替换Mard Word如果成功则表示当前线程获得偏向锁置偏向标志位1
  3. 如果失败则说明发生竞争撤销偏向锁进而升级为轻量级锁。
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针如果成功当前线程获得锁
  5. 如果失败表示其他线程竞争锁当前线程便尝试使用自旋来获取锁。
  6. 如果自旋成功则依然处于轻量级状态。
  7. 如果自旋失败则升级为重量级锁。

image-20200825161411939

2、锁粗化

按理来说同步块的作用范围应该尽可能小仅在共享数据的实际作用域中才进行同步这样做的目的是为了使需要同步的操作数量尽可能缩小缩短阻塞时间如果存在锁竞争那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源如果存在一系列的连续加锁解锁操作可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁避免频繁的加锁解锁操作。

3、锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译又称即时编译)通过对运行上下文的扫描经过逃逸分析去除不可能存在共享资源竞争的锁通过这种方式消除没有必要的锁可以节省毫无意义的请求锁时间

同步锁异步锁和死锁

同步锁

①同步方法在方法声明上加上synchronized

public synchronized void method(){

可能会产生线程安全问题的代码

}

同步方法中的锁对象是 this即调用者对象

②静态同步方法: 在方法声明上加上static synchronized

public static synchronized void method(){

可能会产生线程安全问题的代码

}

静态同步方法中的锁对象是 类名.class因为在加载类文件的时候静态同步方法由于是静态的也被加载进内存了类名.class的加载优先级高于静态方法

③同步代码块在需要同步的代码外面包上一个

synchronized(Object o){

可能会产生线程安全问题的代码

}

同步代码块中的所对象可以是任意对象

异步锁

EgRedis 分布式锁 就是设置一个flag标识当一个服务拿到锁以后立即把对应的标识设置为false 用完后释放锁并把标识修改为true

死锁例子

https://www.cnblogs.com/abcdjava/p/11028951.html

补充同步锁与异步锁的区别

https://blog.csdn.net/qq_35447305/article/details/52663448?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

异步锁针对线程间互斥同步锁针对方法的同步互斥

线程常用方法.

①.取得当前线程的名称:  getName()

②.取得当前线程对象: currentThread()

③.判断线程是否启动:  isAlive()

④.线程的强行运行:  join()

⑤.线程的休眠sleep()

⑥.线程的礼让(针对生命周期是非常重要的 ) yield()

多线程相关关键字

join 读到join时使调用join的线程马上执行执行完其它线程才能执行
yieldyield让出CPU执行权但不让出锁,

如果没有锁的话其它线程和那个yield的线程争夺,而且他让出CPU执行权当他再抢到的时候是从yield后面执行不是重新执行

有锁的话其实yield让出CPU执行权但锁还在他手里,所以还是他执行

wait添加的时间是放弃当前线程的执行等待那个时间再和其它线程抢CPU执行没有时间就是一直等待直到被nitify或notifyal唤醒
sleep睡眠一段时间不放弃锁

notify:唤醒一个等待的线程具体是那个由操作系统对多线程管理的实现

notifyAll:唤醒所有等待的线程

sleep 不释放锁、释放cpu
join 释放锁、抢占cpu
yiled 不释放锁、释放cpu
wait 释放锁、释放cpu

*调用 sleep() 方法使线程进入等待状态等待休眠时间达到而调用我们的 yield() 方法线程会进入就绪状态也就是sleep()需要等待设置的时间后才会进行就绪状态而yield会立即进入就绪状态。*

wait、notify和notifyAll都会释放锁

wait 方法执行后会立即释放锁等待被唤醒的时候会重新持有锁。

notify和notifyAll也会释放锁但是不是立即释放锁执行完notify/notifyAll方法后会立即通知其它正在等待的线程但不是立即释放锁而是会等到其synchronized内中的代码全部执行完之后才会释放锁。所以我们一般都时在我们synchronized内的最后才会调用 notify/notifyAll。

WaitnotifynotifyAll必须在同步代码块中使用(网上说同步方法也可以但我没实验成功)

原因wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁

阻塞队列放入数据的方法

image-20200911154618282

synchronized的底层实现

java中synchronized的实现是基于进入和退出的 Monitor对象实现的无论是显式同步修饰代码块有明确的monitorenter和 monitorexit指令还是隐式同步(修饰方法体)

需要注意的是只有修饰代码块的时候才是基于monitorenter和 monitorexit指令来实现的修饰方法的时候是通过另一种方式实现的我会放到后面去说

在了解整个实现底层之前我还是希望你能够大致了解一下对象在内存中的结构详情

  • 实例变量存放类的属性数据信息包括父类的属性信息如果是数组的实例部分还包括数组的长度这部分内存按4字节对齐。
  • 填充数据由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的仅仅是为了字节对齐这点了解即可。

这两个概念我们简单理解就好我们今天并不去探究对象的构成原理我们着重探究一下对象头他对我们理解锁尤为重要

一般而言synchronized使用的锁存在于对象头里面如果是数组对象则虚拟机使用3个字宽存储对象如果是非数组对象则使用两个字宽存储对象头字虚拟机里面1字宽等于4字节主要结构是 Mark Word 和 Class Metadata Address组成,结构如下

虚拟机位数头对象结构说明32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息32/64bitClass Metadata Address存储到队形类型数据的指针32/64bit(数组)Aarray length数组的长度

通过上述表格能够看出 锁信息 存在于 Mark Word 内那么 Mark Word 内又是如何组成的呢

锁状态25bit4bit1bit是否是偏向锁2bit锁标志位无锁状态对象的hashcode对象的分代年龄001

在运行起见mark Word 里存储的数据会随着锁的标志位的变化而变化。mark Word可能变化为存储一下四种数据

Java SE 1.6为了减少获得锁和释放锁带来的消耗引入了偏向锁和轻量级锁从之前上来就是重量级锁到1.6之后锁膨胀升级的优化极大地提高了synchronized的效率

锁一共有4中状态级别从低到高

这几个状态会随着锁的竞争逐渐升级。锁可以升级但是不能降级其根本的原因就是为了提高获取锁和释放锁的效率

那么synchronized是又如何保证的线程安全的呢或许我们需要从字节码寻找答案

1.Synchronized

Synchronized进过编译会在同步块的前后分别形成monitorentermonitorexit这个两个字节码指令。在执行monitorenter指令时首先要尝试获取对象锁。如果这个对象没被锁定或者当前线程已经拥有了那个对象锁把锁的计算器加1相应的在执行monitorexit指令时会将锁计算器就减1当计算器为0时锁就被释放了。如果获取对象锁失败那当前线程就要阻塞直到对象锁被另一个线程释放为止。

线程池的三种队列区别SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue

https://blog.csdn.net/qq_34707456/article/details/103066565

线程开启需要占用的资源有哪些

https://blog.csdn.net/qq_42539194/article/details/106175198

伪共享和缓存行填充

当多线程修改互相独立的变量时如 果这些==变量共享同一个缓存行==就会无意中影响彼此的性能这就是伪共享。

Java线程模型

https://www.cnblogs.com/kaleidoscope/p/9598140.html

内存屏障

https://baijiahao.baidu.com/s?id=1667840029586081215&wfr=spider&for=pc

线程的生命周期

https://blog.csdn.net/yangying496875002/article/details/73613655

ReentrantLock公平锁和非公平锁

https://www.cnblogs.com/fxtx/p/11657021.html

最佳线程数的总结

https://www.cnblogs.com/igubai/p/7426157.html

最佳线程数的获取

1、通过用户慢慢递增来进行性能压测观察QPS响应时间

2、根据公式计算:服务器端最佳线程数量=((线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量

一般来说是IO和CPU。IO开销较多的应用其CPU线程等待时间会比较长所以线程数量可以开的多一些相反则线程数量要少一些其实有两种极端纯IO的应用比如proxy则线程数量可以开到非常大实在太大了则需要考虑线程切换的开销这种应用基本上后端比如这个proxy是代理搜索的的QPS能有多少proxy就有多少。

3、单用户压测查看CPU的消耗然后直接乘以百分比再进行压测一般这个值的附近应该就是最佳线程数量。

(小GC时间间隔/rt)*(并发线程数量 * thm) <=young

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYDsY2hp-1673797780326)(…/…/…/typora/image/image-20210318154014942.png)]

Java线程池实现原理及其在美团业务中的实践

进程、线程和协程之间的区别和联系

https://blog.csdn.net/daaikuaichuan/article/details/82951084

LinkedBlockingQueue和ArrayBlockingQueue的比较

https://blog.csdn.net/qq_40845019/article/details/103404785

Git中的revert和reset有什么区别?

Revert

如果我们想要用一个反向提交恢复项目的某个版本那就需要 revert 来协助我们完成了。什么是反向提交呢就是旧版本添加了的内容要在新版本中删除旧版本中删除了的内容要在新版本中添加。

revert 也不会修改历史提交记录实际的操作相当于是检出目标提交的项目快照到工作区与暂存区然后用一个新的提交完成版本的“回退”。

Reset

reset 操作与 revert 很像用来在当前分支进行版本的“回退”不同的是reset 是会修改历史提交记录的。

reset 常用的选项有三个分别是 —soft, —mixed, —hard。他们的作用域依次增大。

soft 会仅仅修改分支指向。而不修改工作区与暂存区的内容我们可以接着做一次提交形成一个新的 commit。这在我们撤销临时提交的场景下显得比较有用。

mixed 比 soft 的作用域多了一个 暂存区。实际上 mixed 选项与 soft 只差了一个 add 操作。

hard 会作用域又比 mixed 多了一个 工作区。

50个红球和50个篮球放入两个箱子怎么样放置才能使拿到红球的概率最大

50个红球和50个篮球放入两个箱子怎么样放置才能使拿到红球的概率最大
一个箱子放1个红球 另一个放49红球和50篮球 拿到红球概率=0.5+49/99约等于0.75

连接池的作用及讲解

如上所示ThreadPoolExecutor extends AbstractExecutorService implements ExecutorService extends Executor

execute()方法实际上是Executor中声明的方法在ThreadPoolExecutor进行了具体的实现这个方法是ThreadPoolExecutor的核心方法通过这个方法可以向线程池提交一个任务交由线程池去执行。

submit()方法是在ExecutorService中声明的方法在AbstractExecutorService就已经有了具体的实现在ThreadPoolExecutor中并没有对其进行重写这个方法也是用来向线程池提交任务的但是它和execute()方法不同它能够返回任务执行的结果去看submit()方法的实现会发现它实际上还是调用的execute()方法只不过它利用了Future来获取任务执行结果。

submit()方法中参数可以是Callable类型也可以是Runnable类型而execute()方法参数只能是Runnable类型。

版权声明本文为CSDN博主「任枫丶」的原创文章遵循CC 4.0 BY-SA版权协议转载请附上原文出处链接及本声明。
原文链接https://blog.csdn.net/qq_40718168/article/details/90288840

深入理解线程池

https://www.cnblogs.com/chiangchou/p/thread-pool.html

线程状态

https://www.jianshu.com/p/6aff628d05fb

从Java多线程可见性谈Happens-Before原则

深入分析Volatile的实现原理

https://cloud.tencent.com/developer/article/1672227

MESI协议

https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE

缓存行有4种不同的状态:

  • 已修改Modified (M)

    缓存行是脏的dirty与主存的值不同。如果别的CPU内核要读主存这块数据该缓存行必须回写到主存状态变为共享(S).

  • 独占Exclusive (E)

    缓存行只在当前缓存中但是干净的clean–缓存数据同于主存数据。当别的缓存读取它时状态变为共享当前写数据时变为已修改状态。

  • 共享Shared (S)

    缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。

  • 无效Invalid (I)

    缓存行是无效的

KafKa

https://juejin.cn/post/6963101806402469902

https://zhuanlan.zhihu.com/p/282993811

用Java如何设计一个阻塞队列然后说说ArrayBlockingQueue和LinkedBlockingQueue

https://cloud.tencent.com/developer/article/1837462

Java的JMM内存模型

https://www.cnblogs.com/duanxz/p/3724117.html

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java