文章目录

  • 一、什么是多线程
    • 一、初识多线程
      • 1.1介绍进程
      • 1.2回到线程
      • 1.3进程与线程
      • 1.4并行与并发
      • 1.5Java实现多线程
        • 1.5.1继承Thread,重写run方法
        • 1.5.2实现Runnable接口,重写run方法
      • 1.6Java实现多线程需要注意的细节
  • 二、Thread类解析
    • 一、Thread线程类API
      • 1.1设置线程名
      • 1.2守护线程
      • 1.3优先级线程
      • 1.4线程生命周期
        • 1.4.1sleep方法
        • 1.4.2yield方法
        • 1.4.3join方法
        • 1.4.3interrupt方法
  • 三、使用多线程需要注意的问题
    • 一、使用多线程遇到的问题
      • 1.1线程安全问题
      • 1.3性能问题
    • 二、对象的发布与逸出
      • 2.1安全发布对象
    • 三、解决多线程遇到的问题
      • 3.1简述解决线程安全性的办法
      • 3.2原子性和可见性
        • 3.2.1原子性
        • 3.2.2可见性
      • 3.3线程封闭
      • 3.4不变性
      • 3.5线程安全性委托
    • 四、多线程需要注意的事 -总结
  • 四、synchronized锁和lock锁
    • 一、synchronized锁
      • 1.1synchronized锁是什么?
      • 1.2synchronized用处是什么?
      • 1.3synchronized的原理
      • 1.4synchronized如何使用
        • 1.4.1修饰普通方法:
        • 1.4.2修饰代码块:
        • 1.4.3修饰静态方法
        • 1.4.4类锁与对象锁
      • 1.5重入锁
      • 1.6释放锁的时机
    • 二、Lock显式锁
      • 2.1Lock显式锁简单介绍
      • 2.2synchronized锁和Lock锁使用哪个
      • 2.3公平锁
    • 三、Java锁简单总结
  • 五、AQS
    • 一、AQS是什么?
    • 二、简单看看AQS
      • 2.1同步状态
      • 2.2先进先出队列
      • 2.3acquire方法
      • 2.4release方法
  • 六、ReentrantLock和ReentrantReadWriteLock
    • 一、ReentrantLock锁
      • 1.1内部类
      • 1.2构造方法
      • 1.3非公平lock方法
      • 1.4公平lock方法
      • 1.5unlock方法
    • 二、ReentrantReadWriteLock
      • 2.1ReentrantReadWriteLock内部类
      • 2.2读锁和写锁的状态表示
      • 2.3写锁的获取
      • 2.4读锁获取
    • 三、最后
  • 七、线程池
    • 一、线程池简介
    • 二、JDK提供的线程池API
      • 2.1ForkJoinPool线程池
      • 2.2补充:Callable和Future
    • 三、ThreadPoolExecutor详解
      • 3.1内部状态
      • 3.2已默认实现的池
        • 3.2.1newFixedThreadPool
        • 3.2.2newCachedThreadPool
        • 3.2.3SingleThreadExecutor
      • 3.3构造方法
    • 四、execute执行方法
    • 五、线程池关闭
  • 八、死锁
    • 一、死锁讲解
      • 1.1锁顺序死锁
      • 1.2动态锁顺序死锁
      • 1.3协作对象之间发生死锁
    • 二、避免死锁的方法
      • 2.1固定锁顺序避免死锁
      • 2.2开放调用避免死锁
      • 2.3使用定时锁
      • 2.4死锁检测
    • 三、死锁总结
  • 九、线程常用的工具类
    • 一、CountDownLatch
      • 1.1CountDownLatch简介
      • 1.2CountDownLatch例子
    • 二、CyclicBarrier
      • 2.1CyclicBarrier简介
      • 2.2CyclicBarrier例子
    • 三、Semaphore
      • 3.1Semaphore简介
      • 3.2Semaphore例子
    • 四、总结
  • 十、Atomic
    • 一、基础铺垫
      • 1.2CAS再来看看
        • 1.2.1CAS失败重试(自旋)
        • 1.2.2CAS失败什么都不做
    • 二、原子变量类简单介绍
      • 2.1原子变量类使用
      • 2.2ABA问题
      • 2.3解决ABA问题
      • 2.4LongAdder性能比AtomicLong要好
  • 十一、ThreadLocal
    • 一、什么是ThreadLocal
    • 二、为什么要学习ThreadLocal?
      • 2.1管理Connection
      • 2.2避免一些参数传递
    • 三、ThreadLocal实现的原理
      • 3.1ThreadLocal原理总结
    • 四、避免内存泄露
    • 五、总结

一、什么是多线程

一、初识多线程

1.1介绍进程

讲到线程,又不得不提进程了~

进程我们估计是很了解的了,在windows下打开任务管理器,可以发现我们在操作系统上运行的程序都是进程:

进程的定义:

进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位

  • 进程是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源

1.2回到线程

那系统有了进程这么一个概念了,进程已经是可以进行资源分配和调度了,为什么还要线程呢

为使程序能并发执行,系统必须进行以下的一系列操作:

  • (1)创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB;
  • (2)撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消PCB;
  • (3)进程切换,对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。

可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销

引入线程主要是**为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。**使OS具有更好的并发性

  • 简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能**【调度】**)。

那么线程在哪呢??举个例子:

也就是说:在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程

  • 所以说:一个进程会有1个或多个线程的

1.3进程与线程

于是我们可以总结出:

  • 进程作为资源分配的基本单位
  • 线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。

线程有3个基本状态

  • 执行、就绪、阻塞

线程有5种基本操作

  • 派生、阻塞、激活、 调度、 结束

线程的属性:

  • 1)轻型实体;
  • 2)独立调度和分派的基本单位;
  • 3)可并发执行;
  • 4)共享进程资源。

线程有两个基本类型

    1. 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。
    1. 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行以及撤消线程。

值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率,程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权

1.4并行与并发

并行:

  • 并行性是指同一时刻内发生两个或多个事件。
  • 并行是在不同实体上的多个事件

并发:

  • 并发性是指同一时间间隔内发生两个或多个事件。
  • 并发是在同一实体上的多个事件

由此可见:并行是针对进程的,并发是针对线程的

1.5Java实现多线程

上面说了一大堆基础,理解完的话。我们回到Java中,看看Java是如何实现多线程的~

Java实现多线程是使用Thread这个类的,我们来看看Thread类的顶部注释

通过上面的顶部注释我们就可以发现,创建多线程有两种方法:

  • 继承Thread,重写run方法
  • 实现Runnable接口,重写run方法
  • 实现Callable接口,重写run方法

1.5.1继承Thread,重写run方法

创建一个类,继承Thread,重写run方法

public class MyThread extends Thread {@Overridepublic void run() {for (int x = 0; x < 200; x++) {System.out.println(x);}}}

我们调用一下测试看看:

public class MyThreadDemo {public static void main(String[] args) {// 创建两个线程对象MyThread my1 = new MyThread();MyThread my2 = new MyThread();my1.start();my2.start();}
}

1.5.2实现Runnable接口,重写run方法

实现Runnable接口,重写run方法

public class MyRunnable implements Runnable {@Overridepublic void run() {for (int x = 0; x < 100; x++) {System.out.println(x);}}}

我们调用一下测试看看:

public class MyRunnableDemo {public static void main(String[] args) {// 创建MyRunnable类的对象MyRunnable my = new MyRunnable();Thread t1 = new Thread(my);Thread t2 = new Thread(my);t1.start();t2.start();}
}

结果还是跟上面是一样的,这里我就不贴图了~~~

1.6Java实现多线程需要注意的细节

不要将run()start()搞混了~

run()和start()方法区别:

  • run():仅仅是封装被线程执行的代码,直接调用是普通方法
  • start():首先启动了线程,然后再由jvm去调用该线程的run()方法。

jvm虚拟机的启动是单线程的还是多线程的?

  • 是多线程的。不仅仅是启动main线程,还至少会启动垃圾回收线程的,不然谁帮你回收不用的内存~

那么,既然有两种方式实现多线程,我们使用哪一种???

一般我们使用实现Runnable接口

  • 可以避免java中的单继承的限制
  • 应该将并发运行任务和运行机制解耦,因此我们选择实现Runnable接口这种方式!

二、Thread类解析

一、Thread线程类API

声明本文使用的是JDK1.8

实现多线程从本质上都是由Thread类来进行操作的~我们来看看Thread类一些重要的知识点。Thread这个类很大,不可能整个把它看下来,只能看一些常见的、重要的方法

1.1设置线程名

我们在使用多线程的时候,想要查看线程名是很简单的,调用Thread.currentThread().getName()即可。

如果没有做什么的设置,我们会发现线程的名字是这样子的:主线程叫做main,其他线程是Thread-x

下面我就带着大家来看看它是怎么命名的:

nextThreadNum()的方法实现是这样的:

基于这么一个变量–>线程初始化的数量

点进去看到init方法就可以确定了:

看到这里,如果我们想要为线程起个名字,那也是很简单的。Thread给我们提供了构造方法

下面我们来测试一下:

  • 实现了Runnable的方式来实现多线程:
public class MyThread implements Runnable {@Overridepublic void run() {// 打印出当前线程的名字System.out.println(Thread.currentThread().getName());}
}

测试:

public class MyThreadDemo {public static void main(String[] args) {MyThread myThread = new MyThread();//带参构造方法给线程起名字Thread thread1 = new Thread(myThread, "关注公众号Java3y");Thread thread2 = new Thread(myThread, "qq群:742919422");thread1.start();thread2.start();// 打印当前线程的名字System.out.println(Thread.currentThread().getName());}
}

结果:

当然了,我们还可以通过setName(String name)的方法来改掉线程的名字的。我们来看看方法实现;

检查是否有权限修改:

至于threadStatus这个状态属性,貌似没发现他会在哪里修改

1.2守护线程

守护线程是为其他线程服务的

  • 垃圾回收线程就是守护线程~

守护线程有一个特点

  • 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。
  • 也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行

使用线程的时候要注意的地方

  1. 在线程启动前设置为守护线程,方法是setDaemon(boolean on)
  2. 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了。
  3. 守护线程中产生的新线程也是守护线程

测试一波:

public class MyThreadDemo {public static void main(String[] args) {MyThread myThread = new MyThread();//带参构造方法给线程起名字Thread thread1 = new Thread(myThread, "关注公众号Java3y");Thread thread2 = new Thread(myThread, "qq群:742919422");// 设置为守护线程thread2.setDaemon(true);thread1.start();thread2.start();System.out.println(Thread.currentThread().getName());}
}

上面的代码运行多次可以出现(电脑性能足够好的同学可能测试不出来):线程1和主线程执行完了,我们的守护线程就不执行了~

原理:这也就为什么我们要在启动之前设置守护线程了。

1.3优先级线程

线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素

线程的优先级是高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了)~

可以看到的是,Java提供的优先级默认是5,最低是1,最高是10:

实现:

setPriority0是一个本地(navite)的方法:

 private native void setPriority0(int newPriority);

1.4线程生命周期

在上一篇介绍的时候其实也提过了线程的线程有3个基本状态:执行、就绪、阻塞

在Java中我们就有了这个图,Thread上很多的方法都是用来切换线程的状态的,这一部分是重点!

其实上面这个图是不够完整的,省略掉了一些东西。后面在讲解的线程状态的时候我会重新画一个~

下面就来讲解与线程生命周期相关的方法~

1.4.1sleep方法

调用sleep方法会进入计时等待状态,等时间到了,进入的是就绪状态而并非是运行状态

于是乎,我们的图就可以补充成这样:

1.4.2yield方法

调用yield方法会先让别的线程执行,但是不确保真正让出

  • 意思是:我有空,可以的话,让你们先执行

于是乎,我们的图就可以补充成这样:

1.4.3join方法

调用join方法,会等待该线程执行完毕后才执行别的线程~

我们进去看看具体的实现

wait方法是在Object上定义的,它是native本地方法,所以就看不了了:

wait方法实际上它也是**计时等待(如果带时间参数)**的一种!,于是我们可以补充我们的图:

1.4.3interrupt方法

线程中断在之前的版本有stop方法,但是被设置过时了。现在已经没有强制线程终止的方法了!

由于stop方法可以让一个线程A终止掉另一个线程B

  • 被终止的线程B会立即释放锁,这可能会让对象处于不一致的状态
  • 线程A也不知道线程B什么时候能够被终止掉,万一线程B还处理运行计算阶段,线程A调用stop方法将线程B终止,那就很无辜了~

总而言之,Stop方法太暴力了,不安全,所以被设置过时了。

我们一般使用的是interrupt来请求终止线程~

  • 要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了(明白这一点非常重要!)
  • 也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了。
  • 具体到底中断还是继续运行,应该由被通知的线程自己处理
Thread t1 = new Thread( new Runnable(){public void run(){// 若未发生中断,就正常执行任务while(!Thread.currentThread.isInterrupted()){// 正常任务代码……}// 中断的处理代码……doSomething();}
} ).start();

再次说明:调用interrupt()并不是要真正终止掉当前线程,仅仅是设置了一个中断标志。这个中断标志可以给我们用来判断什么时候该干什么活!什么时候中断由我们自己来决定,这样就可以安全地终止线程了!

我们来看看源码是怎么讲的吧:

再来看看刚才说抛出的异常是什么东东吧:

所以说:interrupt方法压根是不会对线程的状态造成影响的,它仅仅设置一个标志位罢了

interrupt线程中断还有另外两个方法(检查该线程是否被中断)

  • 静态方法interrupted()–>会清除中断标志位
  • 实例方法isInterrupted()–>不会清除中断标志位

上面还提到了,如果阻塞线程调用了interrupt()方法,那么会抛出异常,设置标志位为false,同时该线程会退出阻塞的。我们来测试一波:

public class Main {/*** @param args*/public static void main(String[] args) {Main main = new Main();// 创建线程并启动Thread t = new Thread(main.runnable);System.out.println("This is main ");t.start();try {// 在 main线程睡个3秒钟Thread.sleep(3000);} catch (InterruptedException e) {System.out.println("In main");e.printStackTrace();}// 设置中断t.interrupt();}Runnable runnable = () -> {int i = 0;try {while (i < 1000) {// 睡个半秒钟我们再执行Thread.sleep(500);System.out.println(i++);}} catch (InterruptedException e) {// 判断该阻塞线程是否还在System.out.println(Thread.currentThread().isAlive());// 判断该线程的中断标志位状态System.out.println(Thread.currentThread().isInterrupted());System.out.println("In Runnable");e.printStackTrace();}};
}

结果:

接下来我们分析它的执行流程是怎么样的:

三、使用多线程需要注意的问题

首先来预览一下《Java并发编程实战》前4章的目录究竟在讲什么吧:

第1章 简介

  • 1.1 并发简史
  • 1.2 线程的优势
  • 1.2.1 发挥多处理器的强大能力
  • 1.2.2 建模的简单性
  • 1.2.3 异步事件的简化处理
  • 1.2.4 响应更灵敏的用户界面
  • 1.3 线程带来的风险
  • 1.3.1 安全性问题
  • 1.3.2 活跃性问题
  • 1.3.3 性能问题
  • 1.4 线程无处不在

ps:这一部分我就不讲了,主要是引出我们接下来的知识点,有兴趣的同学可翻看原书~

第2章 线程安全性

  • 2.1 什么是线程安全性
  • 2.2 原子性
  • 2.2.1 竞态条件
  • 2.2.2 示例:延迟初始化中的竞态条件
  • 2.2.3 复合操作
  • 2.3 加锁机制
  • 2.3.1 内置锁
  • 2.3.2 重入
  • 2.4 用锁来保护状态
  • 2.5 活跃性与性能

第3章 对象的共享

  • 3.1 可见性
  • 3.1.1 失效数据
  • 3.1.2 非原子的64位操作
  • 3.1.3 加锁与可见性
  • 3.1.4 Volatile变量
  • 3.2 发布与逸出
  • 3.3 线程封闭
  • 3.3.1 Ad-hoc线程封闭
  • 3.3.2 栈封闭
  • 3.3.3 ThreadLocal类
  • 3.4 不变性
  • 3.4.1 Final域
  • 3.4.2 示例:使用Volatile类型来发布不可变对象
  • 3.5 安全发布
  • 3.5.1 不正确的发布:正确的对象被破坏
  • 3.5.2  不可变对象与初始化安全性
  • 3.5.3 安全发布的常用模式
  • 3.5.4 事实不可变对象
  • 3.5.5 可变对象
  • 3.5.6 安全地共享对象

第4章 对象的组合

  • 4.1 设计线程安全的类
  • 4.1.1 收集同步需求
  • 4.1.2 依赖状态的操作
  • 4.1.3 状态的所有权
  • 4.2 实例封闭
  • 4.2.1 Java监视器模式
  • 4.2.2 示例:车辆追踪
  • 4.3 线程安全性的委托
  • 4.3.1 示例:基于委托的车辆追踪器
  • 4.3.2 独立的状态变量
  • 4.3.3 当委托失效时
  • 4.3.4 发布底层的状态变量
  • 4.3.5 示例:发布状态的车辆追踪器
  • 4.4 在现有的线程安全类中添加功能
  • 4.4.1 客户端加锁机制
  • 4.4.2 组合
  • 4.5 将同步策略文档化

那么接下来我们就开始吧~

一、使用多线程遇到的问题

1.1线程安全问题

在前面的文章中已经讲解了线程【多线程三分钟就可以入个门了!】,多线程主要是为了提高我们应用程序的使用率。但同时,这会给我们带来很多安全问题

如果我们在单线程中以“顺序”(串行–>独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全性问题

因为在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题

简单举个例子:

  • 下面的程序在单线程中跑起来,是没有问题的
public class UnsafeCountingServlet extends GenericServlet implements Servlet {private long count = 0;public long getCount() {return count;}public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {++count;// To something else...}
}

但是在多线程环境下跑起来,它的count值计算就不对了!

首先,它共享了count这个变量,其次来说++count;这是一个组合的操作(注意,它并非是原子性

  • ++count实际上的操作是这样子的:
    • 读取count值
    • 将值+1
    • 将计算结果写入count

于是多线程执行的时候很可能就会有这样的情况:

  • 当线程A读取到count的值是8的时候,同时线程B也进去这个方法上了,也是读取到count的值为8
  • 它俩都对值进行加1
  • 将计算结果写入到count上。但是,写入到count上的结果是9
  • 也就是说:两个线程进来了,但是正确的结果是应该返回10,而它返回了9,这是不正常的!

如果说:当多个线程访问某个类的时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!

有个原则:能使用JDK提供的线程安全机制,就使用JDK的

当然了,此部分其实是我们学习多线程最重要的环节,这里我就不详细说了。这里只是一个总览,这些知识点在后面的学习中都会遇到~~~

1.3性能问题

使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁

就比如说我们的Servlet,一个Servlet对象可以处理多个请求的,Servlet显然是一个天然支持多线程的

又以下面的例子来说吧:

public class UnsafeCountingServlet extends GenericServlet implements Servlet {private long count = 0;public long getCount() {return count;}public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {++count;// To something else...}
}

从上面我们已经说了,上面这个类是线程不安全的。最简单的方式:如果我们在service方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全了。

public class UnsafeCountingServlet extends GenericServlet implements Servlet {private long count = 0;public long getCount() {return count;}public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {++count;// To something else...}
}

虽然实现了线程安全了,但是这会带来很严重的性能问题

  • 每个请求都得等待上一个请求的service方法处理了以后才可以完成对应的操作

这就导致了:我们完成一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题

在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。

这些都是我们接下来要学习的地方:学习使用哪种同步机制来实现线程安全,并且性能是提高了而不是降低了~

二、对象的发布与逸出

书上是这样定义发布和逸出的:

发布(publish) 使对象能够在当前作用域之外的代码中使用

逸出(escape) 当某个不应该发布的对象被发布了

常见逸出的有下面几种方式:

  • 静态域逸出
  • public修饰的get方法
  • 方法参数传递
  • 隐式的this

静态域逸出:

public修饰get方法:

方法参数传递我就不再演示了,因为把对象传递过去给另外的方法,已经是逸出了~

下面来看看该书给出this逸出的例子

逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!理解起来是不是简单了一丢丢?

2.1安全发布对象

上面谈到了好几种逸出的情况,我们接下来来谈谈如何安全发布对象

安全发布对象有几种常见的方式:

  • 在静态域中直接初始化public static Person = new Person();
    • 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
  • 对应的引用保存到volatile或者AtomicReferance引用中
    • 保证了该对象的引用的可见性和原子性
  • 由final修饰
    • 该对象是不可变的,那么线程就一定是安全的,所以是安全发布~
  • 由锁来保护
    • 发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出

三、解决多线程遇到的问题

从上面我们就可以看到,使用多线程会把我们的系统搞得挺复杂的。是需要我们去处理很多事情,为了防止多线程给我们带来的安全和性能的问题~

下面就来简单总结一下我们需要哪些知识点来解决多线程遇到的问题。

3.1简述解决线程安全性的办法

使用多线程就一定要保证我们的线程是安全的,这是最重要的地方!

在Java中,我们一般会有下面这么几种办法来实现线程安全问题:

  • 无状态(没有共享变量)
  • 使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
  • 加锁(内置锁,显示Lock锁)
  • 使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
    • 原子性(就比如上面的count++操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
    • 容器(ConcurrentHashMap等等…)
  • …等等

3.2原子性和可见性

何为原子性?何为可见性?当初我在ConcurrentHashMap基于JDK1.8源码剖析中已经简单说了一下了。不了解的同学可以进去看看。

3.2.1原子性

在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!

  • count++,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了(因为没有中间的三部环节,一步到位【原子性】~

原子性就是执行某一个操作是不可分割的

  • 比如上面所说的count++操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~
  • JDK中有atomic包提供给我们实现原子性操作

也有人将其做成了表格来分类,我们来看看:

图片来源:https://blog.csdn.net/eson_15/article/details/51553338

3.2.2可见性

对于可见性,Java提供了一个关键字:volatile给我们使用~

  • 我们可以简单认为:volatile是一种轻量级的同步机制

volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性

我们将其拆开来解释一下:

  • 保证该变量对所有线程的可见性
    • 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
  • 不保证原子性
    • 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的

使用了volatile修饰的变量保证了三点

  • 一旦你完成写入,任何访问这个字段的线程将会得到最新的值
  • 在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
  • volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方。

一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:

  • 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
  • 该变量不会纳入到不变性条件中(该变量是可变的)
  • 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)

3.3线程封闭

在多线程的环境下,只要我们不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。

就用我们熟悉的Servlet来举例子,写了那么多的Servlet,你见过我们说要加锁吗??我们所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰

在方法上操作,只要我们保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的

在线程封闭上还有另一种方法,就是我之前写过的:ThreadLocal就是这么简单

使用这个类的API就可以保证每个线程自己独占一个变量。(详情去读上面的文章即可)~

3.4不变性

不可变对象一定线程安全的。

上面我们共享的变量都是可变的,正由于是可变的才会出现线程安全问题。如果该状态是不可变的,那么随便多个线程访问都是没有问题的

Java提供了final修饰符给我们使用,final的身影我们可能就见得比较多了,但值得说明的是:

  • final仅仅是不能修改该变量的引用,但是引用里边的数据是可以改的!

就好像下面这个HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~~~

  • 因此,仅仅只能够说明hashMap是一个不可变的对象引用
  final HashMap<Person> hashMap = new HashMap<>();

不可变的对象引用在使用的时候还是需要加锁

  • 或者把Person也设计成是一个线程安全的类~
  • 因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的

要想将对象设计成不可变对象,那么要满足下面三个条件:

  • 对象创建后状态就不能修改
  • 对象所有的域都是final修饰的
  • 对象是正确创建的(没有this引用逸出)

String在我们学习的过程中我们就知道它是一个不可变对象,但是它没有遵循第二点(对象所有的域都是final修饰的),因为JVM在内部做了优化的。但是我们如果是要自己设计不可变对象,是需要满足三个条件的。

3.5线程安全性委托

很多时候我们要实现线程安全未必就需要自己加锁,自己来设计

我们可以使用JDK给我们提供的对象来完成线程安全的设计:

非常多的"工具类"供我们使用,这些在往后的学习中都会有所介绍的~~这里就不介绍了

四、多线程需要注意的事 -总结

正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。

无论是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,我们的程序才可以更加健壮!

四、synchronized锁和lock锁

本文章主要讲的是Java多线程加锁机制,有两种:

  • Synchronized
  • 显式Lock

不得不唠叨几句:

  • 在《Java核心技术卷 一》是先讲比较难的显式Lock,而再讲的是比较简单的Synchronized
  • 而《Java并发编程实战》在前4章零散地讲解了Synchronized,将显式Lock放到了13章

其实都比较坑,如果能先系统讲了Synchronized锁机制,接着讲显式Lock锁机制,那就很容易理解了。也不需要跨那么多章节。

那么接下来我们就开始吧~

一、synchronized锁

1.1synchronized锁是什么?

synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

  • 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~
    public synchronized void test() {// 关注公众号Java3y// doSomething}

synchronized是一种互斥锁

  • 一次只能允许一个线程进入被锁住的代码块

synchronized是一种内置锁/监视器锁

  • Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的! (锁的是对象,但我们同步的是方法/代码块)

1.2synchronized用处是什么?

  • synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
  • synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3synchronized的原理

我们首先来看一段synchronized修饰方法和代码块的代码:

public class Main {//修饰方法public synchronized void test1(){}public void test2(){// 修饰代码块synchronized (this){}}
}

来反编译看一下:

同步代码块

  • monitorenter和monitorexit指令实现的

同步方法(在这看不出来需要看JVM底层实现)

  • 方法修饰符上的ACC_SYNCHRONIZED实现。

synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有

1.4synchronized如何使用

synchronized一般我们用来修饰三种东西:

  • 修饰普通方法
  • 修饰代码块
  • 修饰静态方法

1.4.1修饰普通方法:

用的锁是Java3y对象(内置锁)

public class Java3y {// 修饰普通方法,此时用的锁是Java3y对象(内置锁)public synchronized void test() {// 关注公众号Java3y// doSomething}}

1.4.2修饰代码块:

用的锁是Java3y对象(内置锁)—>this

public class Java3y {public  void test() {// 修饰代码块,此时用的锁是Java3y对象(内置锁)--->thissynchronized (this){// 关注公众号Java3y// doSomething}}
}

当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)

所以,我们可以这样干:

public class Java3y {// 使用object作为锁(任何对象都有对应的锁标记,object也不例外)private Object object = new Object();public void test() {// 修饰代码块,此时用的锁是自己创建的锁Objectsynchronized (object){// 关注公众号Java3y// doSomething}}}

上面那种方式(随便使用一个对象作为锁)在书上称之为–>客户端锁,这是不建议使用的

书上想要实现的功能是:给ArrayList添加一个putIfAbsent(),这需要是线程安全的。

假定直接添加synchronized是不可行的

使用客户端锁,会将当前的实现与原本的list耦合了

书上给出的办法是使用组合的方式(也就是装饰器模式)

1.4.3修饰静态方法

获取到的是类锁(类的字节码文件对象):Java3y.class

public class Java3y {// 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.classpublic static  synchronized void test() {// 关注公众号Java3y// doSomething}
}

1.4.4类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

  • 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的
public class SynchoronizedDemo {//synchronized修饰非静态方法public synchronized void function() throws InterruptedException {for (int i = 0; i <3; i++) {Thread.sleep(1000);System.out.println("function running...");}}//synchronized修饰静态方法public static synchronized void staticFunction()throws InterruptedException {for (int i = 0; i < 3; i++) {Thread.sleep(1000);System.out.println("Static function running...");}}public static void main(String[] args) {final SynchoronizedDemo demo = new SynchoronizedDemo();// 创建线程执行静态方法Thread t1 = new Thread(() -> {try {staticFunction();} catch (InterruptedException e) {e.printStackTrace();}});// 创建线程执行实例方法Thread t2 = new Thread(() -> {try {demo.function();} catch (InterruptedException e) {e.printStackTrace();}});// 启动t1.start();t2.start();}
}

结果证明:类锁和对象锁是不会冲突的

1.5重入锁

我们来看下面的代码:

public class Widget {// 锁住了public synchronized void doSomething() {...}
}public class LoggingWidget extends Widget {// 锁住了public synchronized void doSomething() {System.out.println(toString() + ": calling doSomething");super.doSomething();}
}
  1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁
  2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰
  3. 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?

不需要的!

因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续**“开锁”**进去的!

这就是内置锁的可重入性。记住,持有锁的是线程

1.6释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放
  • 不会由于异常导致出现死锁现象~

二、Lock显式锁

2.1Lock显式锁简单介绍

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~

Lock显式锁是一个接口,我们来看看:

随便翻译一下他的顶部注释,看看是干嘛用的:

可以简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的
  • 提高了语义化,哪里加锁,哪里解锁都得写出来
  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
  • 支持Condition条件对象
  • 允许多个读线程同时访问共享资源

2.2synchronized锁和Lock锁使用哪个

前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??

必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子,牛逼)

  • 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)

所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~

2.3公平锁

公平锁理解起来非常简单:

  • 线程将按照它们发出请求的顺序来获取锁

非公平锁就是:

  • 线程发出请求的时可以**“插队”**获取锁

Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

  • 公平锁会来带一些性能的消耗的

三、Java锁简单总结

本文讲了synchronized内置锁和简单描述了一下Lock显式锁,总得来说:

  • synchronized好用,简单,性能不差
  • 没有使用到Lock显式锁的特性就不要使用Lock锁了。

五、AQS

本来我是打算在这章节中写Lock的子类实现的,但看到了AQS的这么一个概念,可以说Lock的子类实现都是基于AQS的

AQS我在面试题中也见过他的身影,但一直不知道是什么东西。所以本篇我就来讲讲AQS这个玩意吧,至少知道它的概念是什么,对吧~

那么接下来我们就开始吧~

一、AQS是什么?

首先我们来普及一下juc是什么:juc其实就是包的缩写(java.util.concurrnt)

  • 不要被人家唬到了,以为juc是什么一个牛逼的东西。其实指的是包而已~

我们可以发现lock包下有三个抽象的类:

  • AbstractOwnableSynchronizer
  • AbstractQueuedLongSynchronizer
  • AbstractQueuedSynchronizer

通常地:AbstractQueuedSynchronizer简称为AQS

我们Lock之类的两个常见的锁都是基于它来实现的:

那么我们来看看AbstractQueuedSynchronizer到底是什么,看一个类是干什么的最快途径就是看它的顶部注释

通读了一遍,可以总结出以下比较关键的信息:

  • AQS其实就是一个可以给我们实现锁的框架
  • 内部实现的关键是:先进先出的队列、state状态
  • 定义了内部类ConditionObject
  • 拥有两种线程模式
    • 独占模式
    • 共享模式
  • 在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
  • 一般我们叫AQS为同步器

二、简单看看AQS

上面也提到了AQS里边最重要的是状态和队列,我们接下来就看看其源码是怎么样的…

2.1同步状态

使用volatile修饰实现线程可见性:

修改state状态值时使用CAS算法来实现:

2.2先进先出队列

这个队列被称为:CLH队列(三个名字组成),是一个双向队列

看看它队列源码的组成:

    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;// 共享式同步状态获取将会无条件地传播下去(没看懂)static final int PROPAGATE = -3;// 初始为0,状态是上面的几种volatile int waitStatus;// 前置节点volatile Node prev;// 后继节点volatile Node next;volatile Thread thread;Node nextWaiter;final boolean isShared() {return nextWaiter == SHARED;}final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {    // Used to establish initial head or SHARED marker}Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}}

2.3acquire方法

获取独占锁的过程就是在acquire定义的,该方法用到了模板设计模式,由子类实现的~

过程:acquire(int)尝试获取资源,如果获取失败,将线程插入等待队列。插入等待队列后,acquire(int)并没有放弃获取资源,而是根据前置节点状态状态判断是否应该继续获取资源,如果前置节点是头结点,继续尝试获取资源,如果前置节点是SIGNAL状态,就中断当前线程,否则继续尝试获取资源。直到当前线程被park()或者获取到资源,acquire(int)结束。

来源:

  • https://blog.csdn.net/panweiwei1994/article/details/78769703

2.4release方法

释放独占锁的过程就是在acquire定义的,该方法也用到了模板设计模式,由子类实现的~

过程:首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.

来源:

  • https://zhuanlan.zhihu.com/p/27134110

六、ReentrantLock和ReentrantReadWriteLock

上一篇已经将Lock锁的基础AQS简单地过了一遍了,因此本篇主要是讲解Lock锁主要的两个子类:

  • ReentrantLock
  • ReentrantReadWriteLock

那么接下来我们就开始吧~

一、ReentrantLock锁

首先我们来看看ReentrantLock锁的顶部注释,来看看他的相关特性呗:

来总结一下要点吧:

  • 比synchronized更有伸缩性(灵活)
  • 支持公平锁(是相对公平的)
  • 使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁
class X {private final ReentrantLock lock = new ReentrantLock();// ...public void m() { lock.lock();  // block until condition holdstry {// ... method body} finally {lock.unlock()}}
}

1.1内部类

首先我们可以看到有三个内部类:

这些内部类都是AQS的子类,这就印证了我们之前所说的:AQS是ReentrantLock的基础,AQS是构建锁、同步器的框架

  • 可以很清晰的看到,我们的ReentrantLock锁是支持公平锁和非公平锁的~

1.2构造方法

1.3非公平lock方法

尝试获取锁,获取失败的话就调用AQS的acquire(1)方法

acquire(1)方法我们在AQS时简单看过,其中tryAcquire()是子类来实现的

我们去看看tryAcquire()

1.4公平lock方法

公平的lock方法其实就多了一个状态条件

这个方法主要是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true

1.5unlock方法

unlock方法也是在AQS中定义的:

去看看tryRelease(arg)是怎么实现的:

二、ReentrantReadWriteLock

我们知道synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))

而ReentrantReadWriteLock是一个读写锁

  • 取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)
  • 数据的时候,无论是读线程还是写线程都是互斥

一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!

读写锁有一个接口ReadWriteLock,定义的方法就两个:

我们还是来看看顶部注释说得啥吧:

其实大概也是说明了:在读的时候可以共享,在写的时候是互斥的

接下来我们还是来看看对应的实现类吧:

按照惯例也简单看看它的顶部注释:

于是我们可以总结出读写锁的一些要点了:

  • 读锁不支持条件对象,写锁支持条件对象
  • 读锁不能升级为写锁,写锁可以降级为读锁
  • 读写锁也有公平和非公平模式
  • 读锁支持多个读线程进入临界区,写锁是互斥的

2.1ReentrantReadWriteLock内部类

ReentrantReadWriteLock比ReentrantLock锁多了两个内部类(都是Lock实现)来维护读锁和写锁,但是主体还是使用Syn

  • WriteLock
  • ReadLock

2.2读锁和写锁的状态表示

在ReentrantLock锁上使用的是state来表示同步状态(也可以表示重入的次数),而在ReentrantReadWriteLock是这样代表读写状态的:

2.3写锁的获取

主要还是调用syn的acquire(1)

进去看看实现:

2.4读锁获取

写锁的获取调用的是acquireShared(int arg)方法:

内部调用的是:doAcquireShared(arg);方法(实现也是在Syn的),我们来看看:

三、最后

这里就简单总结一下本文的内容吧:

  • AQS是ReentrantReadWriteLock和ReentrantLock的基础,因为默认的实现都是在内部类Syn中,而Syn是继承AQS的~
  • ReentrantReadWriteLock和ReentrantLock都支持公平和非公平模式,公平模式下会去看FIFO队列线程是否是在队头,而非公平模式下是没有的
  • ReentrantReadWriteLock是一个读写锁,如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁
  • 写锁可以降级为读锁,读锁不能升级为写锁
  • 写锁是互斥的,读锁是共享的

七、线程池

一、线程池简介

线程池可以看做是线程的集合。在没有任务时线程处于空闲状态,当请求到来:线程池给这个请求分配一个空闲的线程,任务完成后回到线程池中等待下次任务**(而不是销毁)。这样就实现了线程的重用**。

我们来看看如果没有使用线程池的情况是这样的:

  • 为每个请求都新开一个线程
public class ThreadPerTaskWebServer {public static void main(String[] args) throws IOException {ServerSocket socket = new ServerSocket(80);while (true) {// 为每个请求都创建一个新的线程final Socket connection = socket.accept();Runnable task = () -> handleRequest(connection);new Thread(task).start();}}private static void handleRequest(Socket connection) {// request-handling logic here}
}

为每个请求都开一个新的线程虽然理论上是可以的,但是会有缺点

  • 线程生命周期的开销非常高。每个线程都有自己的生命周期,创建和销毁线程所花费的时间和资源可能比处理客户端的任务花费的时间和资源更多,并且还会有某些空闲线程也会占用资源
  • 程序的稳定性和健壮性会下降,每个请求开一个线程。如果受到了恶意攻击或者请求过多(内存不足),程序很容易就奔溃掉了。

所以说:我们的线程最好是交由线程池来管理,这样可以减少对线程生命周期的管理,一定程度上提高性能。

二、JDK提供的线程池API

JDK给我们提供了Excutor框架来使用线程池,它是线程池的基础

  • Executor提供了一种将**“任务提交”与“任务执行”**分离开来的机制(解耦)

下面我们来看看JDK线程池的总体api架构:

接下来我们把这些API都过一遍看看:

Executor接口:

ExcutorService接口:

AbstractExecutorService类:

ScheduledExecutorService接口:

ThreadPoolExecutor类:

ScheduledThreadPoolExecutor类:

2.1ForkJoinPool线程池

除了ScheduledThreadPoolExecutor和ThreadPoolExecutor类线程池以外,还有一个是JDK1.7新增的线程池:ForkJoinPool线程池

于是我们的类图就可以变得完整一些:

JDK1.7中新增的一个线程池,与ThreadPoolExecutor一样,同样继承了AbstractExecutorService。ForkJoinPool是Fork/Join框架的两大核心类之一。与其它类型的ExecutorService相比,其主要的不同在于采用了工作窃取算法(work-stealing):所有池中线程会尝试找到并执行已被提交到池中的或由其他线程创建的任务。这样很少有线程会处于空闲状态,非常高效。这使得能够有效地处理以下情景:大多数由任务产生大量子任务的情况;从外部客户端大量提交小任务到池中的情况。

来源:

  • https://blog.csdn.net/panweiwei1994/article/details/78969238

2.2补充:Callable和Future

学到了线程池,我们可以很容易地发现:很多的API都有Callable和Future这么两个东西。

	Future<?> submit(Runnable task)<T> Future<T> submit(Callable<T> task)

其实它们也不是什么高深的东西~~~

我们可以简单认为:Callable就是Runnable的扩展

  • Runnable没有返回值,不能抛出受检查的异常,而Callable可以

也就是说:当我们的任务需要返回值的时,我们就可以使用Callable!

Future一般我们认为是Callable的返回值,但他其实代表的是任务的生命周期(当然了,它是能获取得到Callable的返回值的)

简单来看一下他们的用法:

public class CallableDemo {public static void main(String[] args) throws InterruptedException, ExecutionException {// 创建线程池对象ExecutorService pool = Executors.newFixedThreadPool(2);// 可以执行Runnable对象或者Callable对象代表的线程Future<Integer> f1 = pool.submit(new MyCallable(100));Future<Integer> f2 = pool.submit(new MyCallable(200));// V get()Integer i1 = f1.get();Integer i2 = f2.get();System.out.println(i1);System.out.println(i2);// 结束pool.shutdown();}
}

Callable任务:

public class MyCallable implements Callable<Integer> {private int number;public MyCallable(int number) {this.number = number;}@Overridepublic Integer call() throws Exception {int sum = 0;for (int x = 1; x <= number; x++) {sum += x;}return sum;}}

执行完任务之后可以获取得到任务返回的数据

三、ThreadPoolExecutor详解

这是用得最多的线程池,所以本文会重点讲解它。

我们来看看顶部注释:

3.1内部状态

变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息

线程的状态:

  • RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理。
  • SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理。
  • STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务
  • TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
  • TERMINATED:线程池彻底终止的状态

各个状态之间转换:

3.2已默认实现的池

下面我就列举三个比较常见的实现池:

  • newFixedThreadPool
  • newCachedThreadPool
  • SingleThreadExecutor

如果读懂了上面对应的策略呀,线程数量这些,应该就不会太难看懂了。

3.2.1newFixedThreadPool

一个固定线程数的线程池,它将返回一个corePoolSize和maximumPoolSize相等的线程池

   public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

3.2.2newCachedThreadPool

非常有弹性的线程池,对于新的任务,如果此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程去处理这个任务

    public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

3.2.3SingleThreadExecutor

使用单个worker线程的Executor

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));}

3.3构造方法

我们读完上面的默认实现池还有对应的属性,再回到构造方法看看

  • 构造方法可以让我们自定义(扩展)线程池
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}
  1. 指定核心线程数量
  2. 指定最大线程数量
  3. 允许线程空闲时间
  4. 时间对象
  5. 阻塞队列
  6. 线程工厂
  7. 任务拒绝策略

再总结一遍这些参数的要点:

线程数量要点

  • 如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
  • 如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
  • 如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池
  • 如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量

线程空闲时间要点:

  • 当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁

排队策略要点

  • 同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
  • 无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数
  • 有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量

当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了:

拒绝任务策略:

  • 直接抛出异常
  • 使用调用者的线程来处理
  • 直接丢掉这个任务
  • 丢掉最老的任务

四、execute执行方法

execute执行方法分了三步,以注释的方式写在代码上了~

    public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();//如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}//如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态,// 1.如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。// 2.否则如果线程池中运行的线程数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}// 如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。else if (!addWorker(command, false))reject(command);}

五、线程池关闭

ThreadPoolExecutor提供了shutdown()shutdownNow()两个方法来关闭线程池

shutdown() :

shutdownNow():

区别:

  • 调用shutdown()后,线程池状态立刻变为SHUTDOWN,而调用shutdownNow(),线程池状态立刻变为STOP
  • shutdown()等待任务执行完才中断线程,而shutdownNow()不等任务执行完就中断了线程。

八、死锁

一、死锁讲解

在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。

  • 这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!

造成死锁的原因可以概括成三句话:

  • 当前线程拥有其他线程需要的资源
  • 当前线程等待其他线程已拥有的资源
  • 都不放弃自己拥有的资源

1.1锁顺序死锁

首先我们来看一下最简单的死锁(锁顺序死锁)是怎么样发生的:

public class LeftRightDeadlock {private final Object left = new Object();private final Object right = new Object();public void leftRight() {// 得到left锁synchronized (left) {// 得到right锁synchronized (right) {doSomething();}}}public void rightLeft() {// 得到right锁synchronized (right) {// 得到left锁synchronized (left) {doSomethingElse();}}}
}

我们的线程是交错执行的,那么就很有可能出现以下的情况:

  • 线程A调用leftRight()方法,得到left锁
  • 同时线程B调用rightLeft()方法,得到right锁
  • 线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。
  • 但是:线程A的left锁并没有释放,线程B的right锁也没有释放
  • 所以他们都只能等待,而这种等待是无期限的–>永久等待–>死锁

1.2动态锁顺序死锁

我们看一下下面的例子,你认为会发生死锁吗?

    // 转账public static void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount)throws InsufficientFundsException {// 锁定汇账账户synchronized (fromAccount) {// 锁定来账账户synchronized (toAccount) {// 判余额是否大于0if (fromAccount.getBalance().compareTo(amount) < 0) {throw new InsufficientFundsException();} else {// 汇账账户减钱fromAccount.debit(amount);// 来账账户增钱toAccount.credit(amount);}}}}

上面的代码看起来是没有问题的:锁定两个账户来判断余额是否充足才进行转账!

但是,同样有可能会发生死锁

  • 如果两个线程同时调用transferMoney()
  • 线程A从X账户向Y账户转账
  • 线程B从账户Y向账户X转账
  • 那么就会发生死锁。
A:transferMoney(myAccount,yourAccount,10);B:transferMoney(yourAccount,myAccount,20);

1.3协作对象之间发生死锁

我们来看一下下面的例子:

public class CooperatingDeadlock {// Warning: deadlock-prone!class Taxi {@GuardedBy("this") private Point location, destination;private final Dispatcher dispatcher;public Taxi(Dispatcher dispatcher) {this.dispatcher = dispatcher;}public synchronized Point getLocation() {return location;}// setLocation 需要Taxi内置锁public synchronized void setLocation(Point location) {this.location = location;if (location.equals(destination))// 调用notifyAvailable()需要Dispatcher内置锁dispatcher.notifyAvailable(this);}public synchronized Point getDestination() {return destination;}public synchronized void setDestination(Point destination) {this.destination = destination;}}class Dispatcher {@GuardedBy("this") private final Set<Taxi> taxis;@GuardedBy("this") private final Set<Taxi> availableTaxis;public Dispatcher() {taxis = new HashSet<Taxi>();availableTaxis = new HashSet<Taxi>();}public synchronized void notifyAvailable(Taxi taxi) {availableTaxis.add(taxi);}// 调用getImage()需要Dispatcher内置锁public synchronized Image getImage() {Image image = new Image();for (Taxi t : taxis)// 调用getLocation()需要Taxi内置锁image.drawMarker(t.getLocation());return image;}}class Image {public void drawMarker(Point p) {}}
}

上面的getImage()setLocation(Point location)都需要获取两个锁的

  • 并且在操作途中是没有释放锁的

这就是隐式获取两个锁(对象之间协作)…

这种方式也很容易就造成死锁

二、避免死锁的方法

避免死锁可以概括成三种方法:

  • 固定加锁的顺序(针对锁顺序死锁)
  • 开放调用(针对对象之间协作造成的死锁)
  • 使用定时锁–>tryLock()
    • 如果等待获取锁时间超时,则抛出异常而不是一直等待

2.1固定锁顺序避免死锁

上面transferMoney()发生死锁的原因是因为加锁顺序不一致而出现的~

  • 正如书上所说的:如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!

那么上面的例子我们就可以改造成这样子:

public class InduceLockOrder {// 额外的锁、避免两个对象hash值相等的情况(即使很少)private static final Object tieLock = new Object();public void transferMoney(final Account fromAcct,final Account toAcct,final DollarAmount amount)throws InsufficientFundsException {class Helper {public void transfer() throws InsufficientFundsException {if (fromAcct.getBalance().compareTo(amount) < 0)throw new InsufficientFundsException();else {fromAcct.debit(amount);toAcct.credit(amount);}}}// 得到锁的hash值int fromHash = System.identityHashCode(fromAcct);int toHash = System.identityHashCode(toAcct);// 根据hash值来上锁if (fromHash < toHash) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}} else if (fromHash > toHash) {// 根据hash值来上锁synchronized (toAcct) {synchronized (fromAcct) {new Helper().transfer();}}} else {// 额外的锁、避免两个对象hash值相等的情况(即使很少)synchronized (tieLock) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}}}}
}

得到对应的hash值来固定加锁的顺序,这样我们就不会发生死锁的问题了!

2.2开放调用避免死锁

在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!

  • 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用

我们可以这样来改造:

  • 同步代码块最好仅被用于保护那些涉及共享状态的操作
class CooperatingNoDeadlock {@ThreadSafeclass Taxi {@GuardedBy("this") private Point location, destination;private final Dispatcher dispatcher;public Taxi(Dispatcher dispatcher) {this.dispatcher = dispatcher;}public synchronized Point getLocation() {return location;}public synchronized void setLocation(Point location) {boolean reachedDestination;// 加Taxi内置锁synchronized (this) {this.location = location;reachedDestination = location.equals(destination);}// 执行同步代码块后完毕,释放锁if (reachedDestination)// 加Dispatcher内置锁dispatcher.notifyAvailable(this);}public synchronized Point getDestination() {return destination;}public synchronized void setDestination(Point destination) {this.destination = destination;}}@ThreadSafeclass Dispatcher {@GuardedBy("this") private final Set<Taxi> taxis;@GuardedBy("this") private final Set<Taxi> availableTaxis;public Dispatcher() {taxis = new HashSet<Taxi>();availableTaxis = new HashSet<Taxi>();}public synchronized void notifyAvailable(Taxi taxi) {availableTaxis.add(taxi);}public Image getImage() {Set<Taxi> copy;// Dispatcher内置锁synchronized (this) {copy = new HashSet<Taxi>(taxis);}// 执行同步代码块后完毕,释放锁Image image = new Image();for (Taxi t : copy)// 加Taix内置锁image.drawMarker(t.getLocation());return image;}}class Image {public void drawMarker(Point p) {}}}

使用开放调用是非常好的一种方式,应该尽量使用它~

2.3使用定时锁

使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过时限的时候,tryLock()不会一直等待,而是返回错误信息。

使用tryLock()能够有效避免死锁问题~~

2.4死锁检测

虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。

JDK提供了两种方式来给我们检测:

  • JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
  • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

具体可参考:

  • https://www.cnblogs.com/flyingeagle/articles/6853167.html

三、死锁总结

发生死锁的原因主要由于:

  • 线程之间交错执行
    • 解决:以固定的顺序加锁
  • 执行某方法时就需要持有锁,且不释放
    • 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
  • 永久等待
    • 解决:使用tryLock()定时锁,超过时限则返回错误信息

九、线程常用的工具类

Java为我们提供了三个同步工具类

  • CountDownLatch(闭锁)
  • CyclicBarrier(栅栏)
  • Semaphore(信号量)

这几个工具类其实说白了就是为了能够更好控制线程之间的通讯问题~

一、CountDownLatch

1.1CountDownLatch简介

  • A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

简单来说:CountDownLatch是一个同步的辅助类,允许一个或多个线程一直等待直到其它线程完成它们的操作。

它常用的API其实就两个:await()countDown()

使用说明:

  • count初始化CountDownLatch,然后需要等待的线程调用await方法。await方法会一直受阻塞直到count=0。而其它线程完成自己的操作后,调用countDown()使计数器count减1。当count减到0时,所有在等待的线程均会被释放
  • 说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。

1.2CountDownLatch例子

例子:3y现在去做实习生了,其他的员工还没下班,3y不好意思先走,等其他的员工都走光了,3y再走。

import java.util.concurrent.CountDownLatch;public class Test {public static void main(String[] args) {final CountDownLatch countDownLatch = new CountDownLatch(5);System.out.println("现在6点下班了.....");// 3y线程启动new Thread(new Runnable() {@Overridepublic void run() {try {// 这里调用的是await()不是wait()countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("...其他的5个员工走光了,3y终于可以走了");}}).start();// 其他员工线程启动for (int i = 0; i < 5; i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println("员工xxxx下班了");countDownLatch.countDown();}}).start();}}
}

输出结果:

再写个例子:3y现在负责仓库模块功能,但是能力太差了,写得很慢,别的员工都需要等3y写好了才能继续往下写。

import java.util.concurrent.CountDownLatch;public class Test {public static void main(String[] args) {final CountDownLatch countDownLatch = new CountDownLatch(1);// 3y线程启动new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("3y终于写完了");countDownLatch.countDown();}}).start();// 其他员工线程启动for (int i = 0; i < 5; i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println("其他员工需要等待3y");try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("3y终于写完了,其他员工可以开始了!");}}).start();}}
}

输出结果:

二、CyclicBarrier

2.1CyclicBarrier简介

  • A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.

简单来说:CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点。叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)

使用说明:

  • CountDownLatch注重的是等待其他线程完成,CyclicBarrier注重的是:当线程到达某个状态后,暂停下来等待其他线程,所有线程均到达以后,继续执行。

2.2CyclicBarrier例子

例子:3y和女朋友约了去广州夜上海吃东西,由于3y和3y女朋友住的地方不同,自然去的路径也就不一样了。于是他俩约定在体育西路地铁站集合,约定等到相互见面的时候就发一条朋友圈。

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class Test {public static void main(String[] args) {final CyclicBarrier CyclicBarrier = new CyclicBarrier(2);for (int i = 0; i < 2; i++) {new Thread(() -> {String name = Thread.currentThread().getName();if (name.equals("Thread-0")) {name = "3y";} else {name = "女朋友";}System.out.println(name + "到了体育西");try {// 两个人都要到体育西才能发朋友圈CyclicBarrier.await();// 他俩到达了体育西,看见了对方发了一条朋友圈:System.out.println("跟" + name + "去夜上海吃东西~");} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}).start();}}
}

测试结果:

玩了一天以后,各自回到家里,3y和女朋友约定各自洗澡完之后再聊天

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class Test {public static void main(String[] args) {final CyclicBarrier CyclicBarrier = new CyclicBarrier(2);for (int i = 0; i < 2; i++) {new Thread(() -> {String name = Thread.currentThread().getName();if (name.equals("Thread-0")) {name = "3y";} else {name = "女朋友";}System.out.println(name + "到了体育西");try {// 两个人都要到体育西才能发朋友圈CyclicBarrier.await();// 他俩到达了体育西,看见了对方发了一条朋友圈:System.out.println("跟" + name + "去夜上海吃东西~");// 回家CyclicBarrier.await();System.out.println(name + "洗澡");// 洗澡完之后一起聊天CyclicBarrier.await();System.out.println("一起聊天");} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}).start();}}
}

测试结果:

三、Semaphore

3.1Semaphore简介

Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource.


  • A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each {@link #acquire} blocks if necessary until a permit is available, and then takes it. Each {@link #release} adds a permit,potentially releasing a blocking acquirer.However, no actual permit objects are used; the {@code Semaphore} just
    keeps a count of the number available and acts accordingly.

Semaphore(信号量)实际上就是可以控制同时访问的线程个数,它维护了一组**“许可证”**。

  • 当调用acquire()方法时,会消费一个许可证。如果没有许可证了,会阻塞起来
  • 当调用release()方法时,会添加一个许可证。
  • 这些"许可证"的个数其实就是一个count变量罢了~

3.2Semaphore例子

3y女朋友开了一间卖酸奶的小店,小店一次只能容纳5个顾客挑选购买,超过5个就需要排队啦~~~

import java.util.concurrent.Semaphore;public class Test {public static void main(String[] args) {// 假设有50个同时来到酸奶店门口int nums = 50;// 酸奶店只能容纳10个人同时挑选酸奶Semaphore semaphore = new Semaphore(10);for (int i = 0; i < nums; i++) {int finalI = i;new Thread(() -> {try {// 有"号"的才能进酸奶店挑选购买semaphore.acquire();System.out.println("顾客" + finalI + "在挑选商品,购买...");// 假设挑选了xx长时间,购买了Thread.sleep(1000);// 归还一个许可,后边的就可以进来购买了System.out.println("顾客" + finalI + "购买完毕了...");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}

输出结果:

反正每次只能5个客户同时进酸奶小店购买挑选。

四、总结

Java为我们提供了三个同步工具类

  • CountDownLatch(闭锁)
    • 某个线程等待其他线程执行完毕后,它才执行(其他线程等待某个线程执行完毕后,它才执行)
  • CyclicBarrier(栅栏)
    • 一组线程互相等待至某个状态,这组线程再同时执行。
  • Semaphore(信号量)
    • 控制一组线程同时执行

本文简单的介绍了一下这三个同步工具类是干嘛用的,要深入还得看源码或者借鉴其他的资料。

十、Atomic

一、基础铺垫

首先我们来个例子:

public class AtomicMain {public static void main(String[] args) throws InterruptedException {ExecutorService service = Executors.newCachedThreadPool();Count count = new Count();// 100个线程对共享变量进行加1for (int i = 0; i < 100; i++) {service.execute(() -> count.increase());}// 等待上述的线程执行完service.shutdown();service.awaitTermination(1, TimeUnit.DAYS);System.out.println("公众号:Java3y---------");System.out.println(count.getCount());}}class Count{// 共享变量private Integer count = 0;public Integer getCount() {return count;}public  void increase() {count++;}
}

你们猜猜得出的结果是多少?是100吗?

多运行几次可以发现:结果是不确定的,可能是95,也可能是98,也可能是100

结果不确定

根据结果我们得知:上面的代码是线程不安全的!如果线程安全的代码,多次执行的结果是一致的!

我们可以发现问题所在:count++不是原子操作。因为count++需要经过读取-修改-写入三个步骤。举个例子:

  • 如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10
  • 线程A对count++,此时count的值为11
  • 线程B对count++,此时count的值也是11(因为线程B读到的count是10)
  • 所以到这里应该知道为啥我们的结果是不确定了吧。

要将上面的代码变成线程安全的(每次得出的结果是100),那也很简单,毕竟我们是学过synchronized锁的人:

  • increase()加synchronized锁就好了
public synchronized void increase() {count++;
}

无论执行多少次,得出的都是100:

结果都是100

从上面的代码我们也可以发现,只做一个++这么简单的操作,都用到了synchronized锁,未免有点小题大做了。

  • Synchronized锁是独占的,意味着如果有别的线程在执行,当前线程只能是等待!

于是我们原子变量的类就登场了!

1.2CAS再来看看

在写文章之前,本以为对CAS有一定的了解了(因为之前已经看过相关概念,以为自己理解了)…但真正敲起键盘写的时候,还是发现没完全弄懂…所以再来看看CAS吧。

来源维基百科:

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

CAS有3个操作数:

  • 内存值V
  • 旧的预期值A
  • 要修改的新值B

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试(或者什么都不做)

我们画张图来理解一下:

CAS理解

我们可以发现CAS有两种情况:

  • 如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!
  • 如果内存值V和我们的预期值A不相等,一般也有两种情况:
    • 重试(自旋)
    • 什么都不做

我们再继续往下看,如果内存值V和我们的预期值A不相等时,应该什么时候重试,什么时候什么都不做。

1.2.1CAS失败重试(自旋)

比如说,我上面用了100个线程,对count值进行加1。我们都知道:如果在线程安全的情况下,这个count值最终的结果一定是为100的。那就意味着:每个线程都会对这个count值实质地进行加1

我继续画张图来说明一下CAS是如何重试(循环再试)的:

CAS循环重试

上面图只模拟出两个线程的情况,但足够说明问题了。

1.2.2CAS失败什么都不做

上面是每个线程都要为count值加1,但我们也可以有这种情况:将count值设置为5

我也来画个图说明一下:

CAS失败什么都不做

理解CAS的核心就是:CAS是原子性的,虽然你可能看到比较后再修改(compare and swap)觉得会有两个操作,但终究是原子性的!

二、原子变量类简单介绍

原子变量类在java.util.concurrent.atomic包下,总体来看有这么多个:

原子变量类

我们可以对其进行分类:

  • 基本类型:
    • AtomicBoolean:布尔型
    • AtomicInteger:整型
    • AtomicLong:长整型
  • 数组:
    • AtomicIntegerArray:数组里的整型
    • AtomicLongArray:数组里的长整型
    • AtomicReferenceArray:数组里的引用类型
  • 引用类型:
    • AtomicReference:引用类型
    • AtomicStampedReference:带有版本号的引用类型
    • AtomicMarkableReference:带有标记位的引用类型
  • 对象的属性:
    • AtomicIntegerFieldUpdater:对象的属性是整型
    • AtomicLongFieldUpdater:对象的属性是长整型
    • AtomicReferenceFieldUpdater:对象的属性是引用类型
  • JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder
    • 是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

Atomic包里的类基本都是使用Unsafe实现的包装类。

Unsafe里边有几个我们喜欢的方法(CAS):

// 第一和第二个参数代表对象的实例以及地址,第三个参数代表期望值,第四个参数代表更新值
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

从原理上概述就是:Atomic包的类的实现绝大调用Unsafe的方法,而Unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作。这也就为啥CAS是原子性的,因为它是一条CPU指令,不会被打断。

2.1原子变量类使用

既然我们上面也说到了,使用Synchronized锁有点小题大作了,我们用原子变量类来改一下:

class Count{// 共享变量(使用AtomicInteger来替代Synchronized锁)private AtomicInteger count = new AtomicInteger(0);public Integer getCount() {return count.get();}public void increase() {count.incrementAndGet();}
}// Main方法还是如上

修改完,无论执行多少次,我们的结果永远是100!

其实Atomic包下原子类的使用方式都不会差太多,了解原子类各种类型,看看API,基本就会用了(网上也写得比较详细,所以我这里果断偷懒了)…

2.2ABA问题

使用CAS有个缺点就是ABA的问题,什么是ABA问题呢?首先我用文字描述一下:

  • 现在我有一个变量count=10,现在有三个线程,分别为A、B、C
  • 线程A和线程C同时读到count变量,所以线程A和线程C的内存值和预期值都为10
  • 此时线程A使用CAS将count值修改成100
  • 修改完后,就在这时,线程B进来了,读取得到count的值为100(内存值和预期值都是100),将count值修改成10
  • 线程C拿到执行权,发现内存值是10,预期值也是10,将count值修改成11

上面的操作都可以正常执行完的,这样会发生什么问题呢??线程C无法得知线程A和线程B修改过的count值,这样是有风险的。

下面我再画个图来说明一下ABA的问题(以链表为例):

CAS ABA的问题讲解

2.3解决ABA问题

要解决ABA的问题,我们可以使用JDK给我们提供的AtomicStampedReference和AtomicMarkableReference类。

AtomicStampedReference:

An {@code AtomicStampedReference} maintains an object referencealong with an integer “stamp”, that can be updated atomically.

简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。

原理大概就是:维护了一个Pair对象,Pair对象存储我们的对象引用和一个stamp值。每次CAS比较的是两个Pair对象

	// Pair对象private static class Pair<T> {final T reference;final int stamp;private Pair(T reference, int stamp) {this.reference = reference;this.stamp = stamp;}static <T> Pair<T> of(T reference, int stamp) {return new Pair<T>(reference, stamp);}}private volatile Pair<V> pair;// 比较的是Pari对象public boolean compareAndSet(V   expectedReference,V   newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;returnexpectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));}

因为多了一个版本号比较,所以就不会存在ABA的问题了。

2.4LongAdder性能比AtomicLong要好

如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。

去查阅了一些博客和资料,大概的意思就是:

  • 使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS会成功,所以其他线程会不断尝试自旋尝试CAS操作,这会浪费不少的CPU资源。
  • 而LongAdder可以概括成这样:内部核心数据value分离成一个数组(Cell),每个线程访问时,通过哈希等算法映射到其中一个数字进行计数,而最终的计数结果,则为这个数组的求和累加
    • 简单来说就是将一个值分散成多个值,在并发的时候就可以分散压力,性能有所提高。

参考资料:

  • AtomicLong与LongAdder性能对比https://zhuanlan.zhihu.com/p/45489739
  • LongAdder源码详解https://zhuanlan.zhihu.com/p/38288416

十一、ThreadLocal

一、什么是ThreadLocal

声明:本文使用的是JDK 1.8

首先我们来看一下JDK的文档介绍:

/*** This class provides thread-local variables.  These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable.  {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).* * <p>For example, the class below generates unique identifiers local to each* thread.* A thread's id is assigned the first time it invokes {@code ThreadId.get()}* and remains unchanged on subsequent calls.*/  	

结合我的总结可以这样理解:ThreadLocal提供了线程的局部变量,每个线程都可以通过set()get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离~。

简要言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

二、为什么要学习ThreadLocal?

从上面可以得出:ThreadLocal可以让我们拥有当前线程的变量,那这个作用有什么用呢???

2.1管理Connection

**最典型的是管理数据库的Connection:**当时在学JDBC的时候,为了方便操作写了一个简单数据库连接池,需要数据库连接池的理由也很简单,频繁创建和关闭Connection是一件非常耗费资源的操作,因此需要创建数据库连接池~

那么,数据库连接池的连接怎么管理呢??我们交由ThreadLocal来进行管理。为什么交给它来管理呢??ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

当时候写的代码:

public class DBUtil {//数据库连接池private static BasicDataSource source;//为不同的线程管理连接private static ThreadLocal<Connection> local;static {try {//加载配置文件Properties properties = new Properties();//获取读取流InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");//从配置文件中读取数据properties.load(stream);//关闭流stream.close();//初始化连接池source = new BasicDataSource();//设置驱动source.setDriverClassName(properties.getProperty("driver"));//设置urlsource.setUrl(properties.getProperty("url"));//设置用户名source.setUsername(properties.getProperty("user"));//设置密码source.setPassword(properties.getProperty("pwd"));//设置初始连接数量source.setInitialSize(Integer.parseInt(properties.getProperty("initsize")));//设置最大的连接数量source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive")));//设置最长的等待时间source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait")));//设置最小空闲数source.setMinIdle(Integer.parseInt(properties.getProperty("minidle")));//初始化线程本地local = new ThreadLocal<>();} catch (IOException e) {e.printStackTrace();}}public static Connection getConnection() throws SQLException {if(local.get()!=null){return local.get();}else{//获取Connection对象Connection connection = source.getConnection();//把Connection放进ThreadLocal里面local.set(connection);//返回Connection对象return connection;}}//关闭数据库连接public static void closeConnection() {//从线程中拿到Connection对象Connection connection = local.get();try {if (connection != null) {//恢复连接为自动提交connection.setAutoCommit(true);//这里不是真的把连接关了,只是将该连接归还给连接池connection.close();//既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了local.remove();}} catch (SQLException e) {e.printStackTrace();}}}

同样的,Hibernate对Connection的管理也是采用了相同的手法(使用ThreadLocal,当然了Hibernate的实现是更强大的)~

2.2避免一些参数传递

避免一些参数的传递的理解可以参考一下Cookie和Session:

  • 每当我访问一个页面的时候,浏览器都会帮我们从硬盘中找到对应的Cookie发送过去。
  • 浏览器是十分聪明的,不会发送别的网站的Cookie过去,只带当前网站发布过来的Cookie过去

浏览器就相当于我们的ThreadLocal,它仅仅会发送我们当前浏览器存在的Cookie(ThreadLocal的局部变量),不同的浏览器对Cookie是隔离的(Chrome,Opera,IE的Cookie是隔离的【在Chrome登陆了,在IE你也得重新登陆】),同样地:线程之间ThreadLocal变量也是隔离的…

那上面避免了参数的传递了吗??其实是避免了。Cookie并不是我们手动传递过去的,并不需要写<input name= cookie/>来进行传递参数…

在编写程序中也是一样的:日常中我们要去办理业务可能会有很多地方用到身份证,各类证件,每次我们都要掏出来很麻烦

    // 咨询时要用身份证,学生证,房产证等等....public void consult(IdCard idCard,StudentCard studentCard,HourseCard hourseCard){}// 办理时还要用身份证,学生证,房产证等等....public void manage(IdCard idCard,StudentCard studentCard,HourseCard hourseCard) {}//......

而如果用了ThreadLocal的话,ThreadLocal就相当于一个机构,ThreadLocal机构做了记录你有那么多张证件。用到的时候就不用自己掏了,问机构拿就可以了。

在咨询时的时候就告诉机构:来,把我的身份证、房产证、学生证通通给他。在办理时又告诉机构:来,把我的身份证、房产证、学生证通通给他。…

    // 咨询时要用身份证,学生证,房产证等等....public void consult(){threadLocal.get();}// 办理时还要用身份证,学生证,房产证等等....public void takePlane() {threadLocal.get();}

这样是不是比自己掏方便多了。

当然了,ThreadLocal可能还会有其他更好的作用,如果知道的同学可在评论留言哦~~~

三、ThreadLocal实现的原理

想要更好地去理解ThreadLocal,那就得翻翻它是怎么实现的了~~~

声明:本文使用的是JDK 1.8

首先,我们来看一下ThreadLocal的set()方法,因为我们一般使用都是new完对象,就往里边set对象了

    public void set(T value) {// 得到当前线程对象Thread t = Thread.currentThread();// 这里获取ThreadLocalMapThreadLocalMap map = getMap(t);// 如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去if (map != null)map.set(this, value);elsecreateMap(t, value);}

上面有个ThreadLocalMap,我们去看看这是什么?

static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object).  Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table.  Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//....很长
}

通过上面我们可以发现的是ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储

我们的值都是存储到这个Map上的,key是当前ThreadLocal对象

如果该Map不存在,则初始化一个:

    void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

如果该Map存在,则从Thread中获取

    /*** Get the map associated with a ThreadLocal. Overridden in* InheritableThreadLocal.** @param  t the current thread* @return the map*/ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

Thread维护了ThreadLocalMap变量

    /* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null

从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中

于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象

有了上面的基础,我们看get()方法就一点都不难理解了:

    public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

3.1ThreadLocal原理总结

  1. 每个Thread维护着一个ThreadLocalMap的引用
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

四、避免内存泄露

我们来看一下ThreadLocal的对象关系引用图:

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

想要避免内存泄露就要手动remove()掉

五、总结

ThreadLocal这方面的博文真的是数不胜数,随便一搜就很多很多~站在前人的肩膀上总结了这篇博文~

最后要记住的是:ThreadLocal设计的目的就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题

已经看到这里了,收藏肯定是不够的,给我点个赞吧!对我真的很重要!

查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. CodeForces - 1312E Array Shrinking

    Solution 先放一下自己的错误代码: #include <cstdio> #include <cstring> #include <iostream> using namespace std;const int N = 505;int n, a[N], dp[N][N];int read() {int x = 0, f = 1; char s;while((s = getchar()) > 9 || s < 0) if(s == …...

    2024/4/17 6:25:37
  2. mysql02基础命令:ddl操作

    Mysql的DDL操作: 修改表名、列名、列属性、删除列语句。 1.修改表名: alter table tb_teacher rename tb_teacher1;2.添加表列:alter table tb_teacher1 add column phone varchar(18); 3.删除表列:alter table tb_teacher1 drop column phone; 4.修改表列类型:alter tabl…...

    2024/4/20 14:13:44
  3. Java抽象类

    Java抽象类一般的类都是拥有完整功能的类,可以直接产生实例化对象,并且在普通类中可以包含有构造方法、普通方法、static方法等。而抽象类是指在普通类的结构里增加抽象方法的组成部分。 那么什么叫抽象方法呢?在所有的普通方法上面都会有一个“{}”,这个表示方法体,有方法…...

    2024/4/10 13:24:16
  4. flask_python_web框架源码阅读(1)

    引言 还是老样子,想研究源码,从最新版看没有太大的意义,最好是从第一个版本v0.1开始看起,因为东西最少,但是涉及到的整个框架的设计思路是基本上不会变的 文章目录一些概念flaskwerkzeugwsgiweb server /web application简单了解werkzeug开始了解flask快速开发一个hello,w…...

    2024/4/26 21:37:00
  5. JAVA基础——泛型和容器类7_27

    创建HashMap对象,初始化,替换,遍历的三种方式//创建HashMap对象Map<Integer,String> map1=new HashMap <>();map1.put(001, "Andy");map1.put(001,"Cintry");//key不可以重复,value可以重复map1.put(002,"Smith");System.out.pr…...

    2024/4/10 13:24:16
  6. listagg Oracle ,根据条件汇总某些字段,多行字段合并

    我们的某些业务需要做汇总处理目标:现状:程序处理比较麻烦,这时候oracle listagg 函数就帮上忙了。可以参考一下使用方法:SELECTxzmc "cityName",listagg ( address, , ) within GROUP ( ORDER BY xzmc ) "roadList" FROMZHXT_GD_YDSJLB GROUP BYxzm…...

    2024/4/24 21:28:08
  7. 操作系统中程序的内存结构说明

    一个程序在内存上由BSS段、data段、text段三个组成的。在没有调入内存前,可执行程序分为代码段、数据区和未初始化数据区三部分。 BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存…...

    2024/4/21 17:34:34
  8. Java学习-MySQL数据库

    Java学习-MySQL数据库1.基础1.1MySQL数据库在MAC电脑上的安装1.2 数据基本概念1.3 MySQL的一些基本操作2.数据库的基本操作2.1 DDL:操作数据库、表 (CRUD)2.1.1 操作数据库2.2.2 操作表2.2 DML(增删改表中的数据)2.3 DQL:查询表中的记录2.3.1 基础查询2.3.2 排序查询2.3.3 聚…...

    2024/4/27 22:48:56
  9. ShellEOF说明,Expect详解

    文章目录一、EOF说明cat和eof结合使用具有追加功能二、Expect概述三、基本命令四、Expect语法单一分支语法多分支模式语法五、Expect执行方式直接执行嵌入执行案例一:创建用户tom,密码tom123**案例二:SSH登录案例三:FTP登录 前言 一、EOF说明 ● Shell中通常将EOF与 <<…...

    2024/4/26 13:10:58
  10. 快速分析竞争对手的店铺的方法有哪些?

    "强者生存,弱者淘汰,它是大自然的生存之道,一样适用如今市场竞争激烈的电商行业。 怎样锁定自身的竞争者,并迅速地对竞争者开展剖析,随后在剖析后制订有关的对决对策,在经营店面的全过程中就至关重要。 假如连自身都不清楚自身的店面和市场定位,那麼你要沒有宣战就…...

    2024/4/10 13:24:11
  11. cmd命令大全

    CMD命令:开始->运行->键入cmd或command(在命令行里可以看到系统版本、文件系统版本) chcp 修改默认字符集 chcp 936默认中文 chcp 65001 1. appwiz.cpl:程序和功能 2. calc:启动计算器 5. chkdsk.exe:Chkdsk磁盘检查(管理员身份运行命令提示符) 6. cleanmgr: …...

    2024/4/19 9:17:40
  12. Spring 常见注解原理和自定义@interface注解

    一、认识注解 注解(Annotation)很重要,未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,现在的Struts2有一部分也是基于注解的了,注解是一种趋势,现在已经有不少的人开始用注解了,注解是JDK1.5之后才有的新…...

    2024/4/21 4:46:12
  13. 获取最长不重复子串

    def get_longest_substr1(s):"""暴力破解1.获取全部不重复子串2.按长度,降序,取第一个:param s::return:"""length = len(s)non_repeats = [(len(s[x:y+1]), s[x:y+1]) for x in range(length) for y in range(x, length) if len(s[x:y+1]) ==…...

    2024/4/26 18:09:33
  14. 网站上线后可以修改哪些内容

    公司网站上线后,有时客户会由于公司原因而需要正价或者减少网站的内容。可是一些公司会担心网站建成后修改网站会不会带来不好的效果?用户会不会找不到公司网站了?网站上线后可以修改哪些内容?事实上,只要网站的大框架不动,是可以修改一些图片和文字什么的。接下来,跟着…...

    2024/4/25 3:32:50
  15. 机器学习(part3)--机器学习与数据挖掘的区别

    学习笔记,仅供参考,有错必纠机器学习与数据挖掘的区别机器学习的定义目前被广泛采用的机器学习的定义是“利用经验来 改善计算机系统自身的性能”。由于“经验”在计算机系统中主要是以数据的形式存在的,因此机器学习需要运用机器学习技术对数据进行分析.数据挖掘的定义所谓…...

    2024/4/10 13:24:08
  16. 05.类、对象、构造方法

    第 1章 面向对象思想 1.1 面向对象思想概述 概述 Java语言是一种面向对象的程序设计语言,而面向对象思想是一种程序设计思想,我们在面向对象思想的指引下,使用Java语言去设计、开发计算机程序。 这里的对象泛指现实中一切事物,每种事物都具备自己的属性和行为。面向对象思…...

    2024/4/10 13:24:07
  17. sqlserver:链接服务器和同义词

    环境:window 10 x64 sqlserver2014 x64参考: Sqlserver 链接服务器和同义词 链接服务器(数据库引擎) 同义词(数据库引擎) 一、链接服务器 1.1 概念 通过链接服务器,你可以从远程数据源中读取或更新数据。 比如说,我们在服务器A上新建链接服务器,让他们分别指向服务器B…...

    2024/4/26 20:32:14
  18. 全媒体环境下高校思想政治教育的嬗变

    高校思想政治教育是学校党建的重要组成部分,是大学生健康成长的重要环节,是培养中国特色社会主义接班人的根本保障。全媒体时代的到来对高校思想政治教育既带来机会也提出挑战。大学生具有接受新鲜事物快、彰显个性自由、民主意识日益增强等特点,是新媒体最直接的受益者、体…...

    2024/4/26 17:21:34
  19. Style2Paints V3 / V4.5 风格迁移上色-软件使用教程

    版权声明: 未经同意,禁止转载。(更新时间:2020-07-28)1. 风格迁移上色教程(如何使用Style2paints进行风格迁移?)1.1 Style2paints V3操作说明目前GitHub仓库内公开的代码和模型为Style2paints V3。虽然V3主要专注于基于颜色提示的线稿上色,但基于风格迁移的线稿上色功…...

    2024/4/22 20:55:26
  20. 第十二章 Logstash入门

    一、概念介绍Logstash是一个类似实时流水线的开源数据传输引擎,它像一个两头连接不 同数据源的数据传输管道,将数据实时地从一个数据源传输到另一个数据源中。在数据传输的过程中,Logstash还可以对数据进行清洗、加工和整理,使数据在 到达目的地时直接可用或接近可用,为更…...

    2024/4/10 13:24:03

最新文章

  1. 新版本Qt Creator安装配置

    新版本Qt Creator安装配置 文章目录 新版本Qt Creator安装配置1、前言2、环境3、安装配置4、总结 更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&#x1f449;Qt开发经验 &#x1f448; 1、前言 Qt是一个跨平台的C应用程序开发框架&#xff0c;而Qt Creator是专为Q…...

    2024/4/27 23:11:17
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. 备战蓝桥杯Day37 - 真题 - 特殊日期

    一、题目描述 思路&#xff1a; 1、统计2000年到2000000年的日期&#xff0c;肯定是需要遍历 2、闰年的2月是29天&#xff0c;非闰年的2月是28天。我们需要判断这一年是否是闰年。 1、3、5、7、8、10、12月是31天&#xff0c;4、6、9、11月是30天。 3、年份yy是月份mm的倍数…...

    2024/4/24 9:33:16
  4. 【Angular】什么是Angular中的APP_BASE_HREF

    1 概述: 在这篇文章中&#xff0c;我们将看到Angular 10中的APP_BASE_HREF是什么以及如何使用它。 APP_BASE_HREF为当前页面的基础href返回一个预定义的DI标记。 APP_BASE_HREF是应该被保留的URL前缀。 2 语法: provide: APP_BASE_HREF, useValue: /gfgapp3 步骤: 在app.m…...

    2024/4/26 3:05:48
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/4/27 1:53:53
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/4/27 3:39:11
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/4/27 12:24:35
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/4/27 12:24:46
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/4/27 3:39:08
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/4/27 3:39:07
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/4/27 3:39:07
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/4/27 12:44:49
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/4/27 21:08:20
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/4/26 22:35:59
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/4/27 18:40:35
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/4/27 3:39:03
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/4/27 13:52:15
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/4/27 13:38:13
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/4/27 1:03:20
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/4/27 3:22:12
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/4/27 22:51:49
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/4/27 3:39:00
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/4/26 23:53:24
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/4/27 20:28:35
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57