java多线程笔记总结

多线程笔记总结

一、线程与进程

1、进程和线程的基本关系

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

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

线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。

引入线程是作为调度和分派的基本单位(取代进程的部分基本功能是调度)。 多线程的存在是为了提高应用程序的使用率,而不是提高程序的执行速度。

总结:

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

线程有两个基本类型:

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

2、并行和并发

并行:

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

并发:

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

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

3、java实现多线程

1.实现方式
  • 继承Thread,重写run方法

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

    使用时,创建实现类的对象,传入Tread中。

  • 实现Callable接口(有返回值)

  • 使用线程池获取

2.注意细节

**1. **run()和start()方法区别:

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

​ 不仅仅是启动main线程,还至少会启动垃圾回收线程的。

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

二、Tread类解析

1、Tread线程类API

查看线程名,调用Thread.currentThread().getName()

通过setName(String name)的方法来改掉线程的名字。

2、守护线程

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

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

守护线程有一个特点

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

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

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

3、优先级线程

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

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

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

4、线程生命周期

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

调用yield方法会先让别的线程执行,进入等待状态,但是不确保真正让出

调用join方法,会等待该线程执行完毕后才执行别的线程。底层循环调用wait方法,当线程终止会调用notifyAll唤醒。

wait方法实际上它也是**计时等待(如果带时间参数)**的一种。

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

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

  • 静态方法interrupted()–>会清除中断标志位
  • 实例方法isInterrupted()–>不会清除中断标志位
  • 如果阻塞线程调用了interrupt()方法,那么会抛出异常,设置标志位为false,同时该线程会退出阻塞的。

5、使用多线程应该注意的问题

线程安全问题

性能问题。防止发生死锁

对象的发布和逸出

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

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

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

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

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

如何安全发布对象:

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

  1. 在静态域中直接初始化public static Person = new Person();

    静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象

    对应的引用保存到volatile或者AtomicReferance引用中

    保证了该对象的引用的可见性和原子性

  2. 由final修饰

    该对象是不可变的,那么线程就一定是安全的,所以是安全发布

  3. 由锁来保护

    发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出

6、原子性和可见性

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

  • 比如上面所说的count++操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~

    JDK中有atomic包提供给我们实现原子性操作

2、可见性,Java提供关键字:volatile

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

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

  • 保证该变量对所有线程的可见性

    • 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
  • 不保证原子性

    • 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的

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

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

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

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

7、线程封闭

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

Servlet所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰

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

8、不变性

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

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

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

三、加锁机制

1、synchronized锁机制

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

使用:要在代码块(方法)添加关键字synchronized,即可以实现同步的功能。

细节:

​ synchronized是一种互斥锁一次只能允许一个线程进入被锁住的代码块

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

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

用处:

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

原理:

底层通过 monitorenter和monitorexit指令实现的。

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

具体使用方式:

1、修饰普通方法

2、修饰代码块,用的锁是对象的内置锁

3、修饰静态方法,获取到的是类锁(类的字节码文件)

​ 获取了类锁的线程和获取了对象锁的线程是不冲突的。

重入锁:

子类重写父类的方法,加锁,并且访问了父类的方法。此时父类的方法没必要再加锁。

因为锁的持有者是“线程”。这是内置锁的可重入性。

释放锁的时机:

1、当方法(代码块)执行完毕会自动释放锁,不需要任何操作。

2、线程执行的代码出现异常时,其持有的锁会自动释放。

2、lock显式锁

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

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

非公平锁:线程发出请求的时可以**“插队”**获取锁

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

  • 公平锁会来带一些性能的消耗的
2.2 java锁简单总结

JDK1.6开始Synchronized锁就做了各种的优化

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

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁

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

四、AQS

4.1 AQS基本概念

lock包下有三个抽象类, AbstractQueuedSynchronizer简称为AQS

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

acquire方法:获取独占锁;

release方法:释放独占锁

五、ReentrantLock和ReentrantReadWriteLock

1、ReentrantLock锁

  • 比synchronized更有伸缩性(灵活)
  • 支持公平锁(是相对公平的),默认是非公平锁
  • 使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁

三个内部类都是AQS的子类,默认的实现都在Syn中,所以 AQS是ReentrantLock的基础,AQS是构建锁、同步器的框架

2、ReentrantReadWriteLock

ReentrantReadWriteLock是一个读写锁

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

总结出读写锁的一些要点了:

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

六、线程池

1、线程池简介

可以看作是线程的集合,没有任务时,线程空闲;有任务时,线程池分配一个空闲的线程,使用完毕后归还给线程池,等待下次使用。实现了线程的重用

单独为每一个请求都开一个新的线程的缺点

线程生命周期开销高,创建和销毁线程花费的时间资源比较高,空闲线程也会占用资源;

程序的稳定性和健壮性会下降。

2、JDK提供的线程池API

Executor框架是线程池的基础。提供了一种将“任务提交”和“任务执行”分离开的机制。

https://user-gold-cdn.xitu.io/2018/4/3/162896ab1a1d1e2e?w=720&h=452&f=jpeg&s=31576
ThreadPoolExecutor是要重点学习的线程池。

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

补充:Calllable和Future

Callable就是Runnable的扩展

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

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

3、TreadPoolExecutor详解

3.1 内部状态

变量ctl定义为AtomicInteger,记录了“线程池的任务数量”(低29位)和“线程池的状态”(高3位)。

线程池的状态:

  • RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理。–正在运行
  • SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理。–关闭接收接口
  • STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。–整体暂停
  • TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
  • TERMINATED:线程池彻底终止的状态
3.2 已默认实现的池
  • newFixedThreadPool— 一个固定线程数的池,corePoolSize==maximumPoolSize
  • newCachedThreadPool— 有弹性的线程池,没有空闲线程,创建一条新的线程池
  • SingleThreadExecutor— 使用单个worker
3.3 构造方法参数要点

线程数量要点

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

线程空闲时间要点:

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

排队策略要点

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

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

拒绝任务策略:

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

4、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);
    }

5、线程池关闭

shutdown():按过去执行已提交任务的顺序发起一个有序的关闭,但是不接收新任务。状态立即变成shutdown。

shutdownNow():尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。状态立即变成stop。

七、死锁

造成死锁的原因:

​ 线程之间等待对方拥有的资源;

​ 都不放弃自己拥有的资源。

1、死锁讲解

1.1 锁顺序死锁

A得到left锁,B得到right锁,但是A继续执行下去,需要得到right锁,B继续执行方法,需要得到left锁,二者都在等待对方拥有的资源,形成死锁。

1.2 动态锁顺序死锁

transferMoney方法中,首先锁定汇款账户,然后锁定来帐账户,方法下边进行转账操作;

当A线程从X账户向Y账户转钱;B线程从Y账户向X账户转钱,此时形成死锁。

1.3 协作对象之间发生死锁

两个方法都需要获取锁,并且操作过程中那个没有释放锁,隐式获取两个锁,容易造成死锁。

2、避免死锁的方法

2.1 固定锁顺序避免死锁

线程之间交错运行,容易出现顺序死锁。

如果所有的线程以固定顺序来获得锁,那么程序中就不会出现锁顺序死锁的问题。

将transferMoney上锁的方式改成根据对象的hash值来判断。

2.2 开放调用避免死锁

执行某方法时就需要持有锁,且不释放,出现协作对象死锁。

缩减同步代码块范围,最好仅在操作共享变量时才加锁

// TODO:读懂例子

2.3 使用定时锁

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

2.4 死锁检测

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

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

具体可参考:

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

八、线程常用的工具类

1.CountDownLatch

作用:允许一个或多个线程一直等待,直到其他线程完成他们的操作。

原理:等待的线程调用await()方法,通过count变量来控制等待,其他线程完成后,调用countDown(),count–,直到减到0。

2. CyclicBarrier

作用:一组线程之间相互等待,直到到达某个公共屏障点;当所有等待线程都被释放后,CyclicBarrier可以被重用。

使用说明:

  • CountDownLatch注重的是等待其他线程完成,CyclicBarrier注重的是:当线程到达某个状态后,暂停下来等待其他线程,所有线程均到达以后,继续执行。
  • 调用cyclicBarrier.await(),所有线程都到这个点后再一起向下执行。

3.Semaphore

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

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

九、Atomic

1.基础铺垫

问题:多线程对共享变量count进行++操作,最后的结果不是线程安全的。

1.1 CAS

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

CAS有3个操作数:

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

执行时,CAS有两种情况:

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

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

1.1.1 CAS失败自旋

线程Z和线程Y同时进入方法,内存值为0,二者的预期值都是0;

Z抢到CPU执行权,CAS原子操作后,内存值为1;

Y重新试图++,发现预期值0和内存值1不同,所以失败自旋—将预期值更新为当前的内存值,重复以上操作。

1.1.2 CAS失败什么都不做

如上,Y发现预期值和内存值不同时,结束当前线程。

2. 原子变量类

在这里插入图片描述

改造上述count++操作,

private AtomicInteger count = new AtomicInteger(0); // 使count变成原子变量
2.1 CAS的ABA问题

在这里插入图片描述

2.2 解决ABA问题

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

简单来说就是在给为这个对象提供了一个版本,并且这个版本如果被修改了,是自动更新的。因为多了一个版本号的比较,所以避免了ABA问题。

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

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

十、ThreadLocal

1. 什么是TreadLocal类

TreadLocal类提供了线程的局部变量,每个线程可以使用set()和get()方法对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了数据的数据隔离

2. ThreadLocal的作用

2.1 管理connection

ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!

//为不同的线程管理连接
    private static ThreadLocal<Connection> local;
    
    getConnection时把connection装配到ThreadLocal里;closeConnection时把connection.close,并且在ThreadLocal中romove connection
2.2 管理参数

ThreadLocal类相当于一个机构,管理了参数,避免了参数之间的传递。

3. ThreadLocal原理

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值