Java基础系列-最全面的多线程核心解析

1.线程

线程就是进程中运行的多个子任务,是操作系统调度的最小单元。

2.线程的状态

Thread类中定义了一个枚举State,用来说明线程的几种状态: New:新建状态,new出来,还没有调用start。

Runnable:可运行状态,调用start进入可运行状态,可能运行也可能没有运行,取决于操作系统的调度。

Blocked:阻塞状态,被锁阻塞,暂时不活动,只有等待synchronized的状态才是Blocked状态。

Waiting:等待状态,不活动,不运行任何代码,等待线程调度器调度,会使线程进入此状态的方法有:

scss 复制代码
1.object.wait()  
2.Thread.join()  
3.LockSupport.park()

Timed Waiting:超时等待,该状态不同于waiting,他可以在指定时间自行返回,会使线程进入此状态的方法有:

scss 复制代码
1.Thread.sleep(指定休眠时间)  
2.Object.wait(指定等待时间)  
3.Thread.join(指定时间)  
4.LockSupport.parkNanos(指定时间)  
5.LockSupport.parkUntil(指定时间)

Terminated:终止状态,包括正常终止和异常终止

2.线程的创建

a.继承Thread重写run方法

b.实现Runnable重写run方法

c.实现Callable重写call方法

3.线程的中断

Thread.stop()

已废弃

1.即刻抛出ThreadDeath异常,在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中。

2.释放该线程所持有的所有的锁。调用thread.stop()后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。

Thread.interrupt()

interrupt()方法会将中断标识位设置为true,具体是否要中断由程序来判断。

在线程六种状态下调用线程中断的代码的结果:

1.New和Terminated状态下,线程不会理会线程中断的请求,既不会设置标记位。

2.在Runnable和Blocked状态下调用interrupt会将标志位设置位true。

3.在Waiting和Timed Waiting状态下会发生InterruptedException异常。

正确使用线程中断的方法

scss 复制代码
new Thread(new Runnable() {
    @Override
    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            try {
                Thread.sleep(2000);
            } catch(InterruptedException e) {
                //发生中断异常时,中断标志位会被复位(被设置为false),我们需要重新将中断标
                //志位设置为true,这样外界可以通过这个状态判断是否需要中断线程
                Thread.currentThread().interrupt();
            }
        }
    }
}).start();

4.线程的挂起

thread.suspend()

已废弃,thread.suspend() 该方法不会释放线程所占用的资源。如果使用该方法将某个线程挂起,则可能会使其他等待资源的线程死锁。

thread.resume()恢复通过suspend挂起的线程。

thread.wait()

暂停执行、释放锁、进入等待状态. notify() 随机唤醒一个在等待锁的线程

notifyAll() 唤醒所有在等待锁的线程,自行抢占cpu资源

5.线程的优先级

注意,不要过度依赖线程优先级。线程的优先级设置可以为1-10的任一数值,Thread类中定义了三个线程优先级,分别是:MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10),一般情况下推荐使用这几个常量,不要自行设置数值。

6.线程的守护者

守护线程:任何一个守护线程都是整个程序中所有用户线程的守护者,只要有活着的用户线程,守护线程就活着。

7.线程的安全性

当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。

产生线程安全性的原因:

1.多线程环境
2.多线程操作同一共享资源
3.对该共享资源进行了非原子性操作

8.单例中的线程安全

1.volatile禁止指令重排序。一行这样的代码执行可能涉及到多少指令?A a = new A();

css 复制代码
a.类加载
b.在堆内存开辟空间创建对象
c.创建对象引用
d。将引用指向堆内存地址。

如果不使用volatile,可能会发生指令重排序,则存在这样的风向,C、D两个线程同时执行单例对象创建,C先拿到了锁,正在创建对象,此时创建对象执行了上边4条指令,由于没有使用volatile,则创建对象的顺序为a、c、b、d,执行到c之后挂起,此时D线程开始执行,判断引用非空直接返回,导致空指针发生。

2.外层判空提高第一次创建之后的使用效率,避开锁。

3.内存盘空防止创建两个对象,如A、B两个线程,A线程进入第一层盘空后挂起,B线程拿到执行权也进行第一层盘空,这种情况下会出现创建两次的情况。

csharp 复制代码
public class SingleTon {
    private static volatile SingleTon instance = null; //1
    private SingleTon() {}
    public static SingleTon getInstance() {
        if(instance == null) { //2
            synchronized(SingleTon.class) {
                if(instance == null) { //3
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}

9.如何确保线程安全运行

多线程的三大特性

原子性

一个操作或者多个操作要么都不执行,要么全部执行并且执行的过程不会被任何因素打断,这就是原子性。

ini 复制代码
对基本数据类型的读取和赋值操作是原子性操作,这些操作不可被中断,是一步到位的,例如x=3是原子性
操作,而y = x就不是,它包含两步:第一读取x,第二将x写入工作内存;x++也不是原子性操作,它包含
三部,第一,读取x,第二,对x加1,第三,写入内存。

可见性

和Java的线程模型有关,存在主存和线程副本的概念。线程之间的可见性,既一个线程修改的状态对另一个线程是可见的。volatile修饰可以保证可见性,它会保证修改的值会立即从线程副本更新到主存,所以对其他线程是可见的,普通的共享变量不能保证可见性,因为被修改后不会立即写入主存,何时被写入主存是不确定的,所以其他线程去读取的时候可能读到的还是旧值。

有序性

Java中的指令重排序(包括编译器重排序和运行期重排序)可以起到优化代码的作用,但是在多线程中会影响到并发执行的正确性,使用volatile可以保证有序性,禁止指令重排。

volatile

volatile可以保证可见性和有序性(java重排序的前提是在不影响单线程运行结果的前提下进行重排序。也就是在单线程环境运行,重排序后的结果和重排序之前按代码顺序运行的结果相同)。 volatile的底层(汇编代码)是通过lock前缀指令、内存屏障来实现的。volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。通过观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。不过,volatile变量在字节码级别没有任何区别,在汇编级别使用了lock指令前缀。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理器指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。

Synchronized

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

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

当synchronized关键字用在不同地方时的含义

a.修饰非静态方法:锁住对象的实例,使用同一个对象的锁时才有互斥效果,加入一个类创建了两个对象,那么这两个对象的锁是没有互斥关系的。

b.修饰静态方法:锁住整个类。因为是锁住的类,那么不管这个类创建多少个对象,这些对象调用这个同步方法的时候都存在互斥关系,会等待一个执行完成在执行另一个。

c.修饰代码块: 锁住一个对象 synchronized (lock) 即synchronized后面括号里的内容,传哪个对象就锁哪个

synchronized 关键字的底层原理

指在汇编指令层面,先编译成class,再进行反编译得到。

同步代码块

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

同步方法

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

未进入synchronized锁的线程如何挂起

执行monitor enter指令如果获取锁不成功,会将该线程添加到该monitor对象对应的同步队列 SynchronizedQueue,当占用该monitor对象的线程执行monitor out时就会唤醒同步队列中的线程。唤醒线程时是选择同步队列上哪一个线程呢?java8默认选择最近添加到同步队列中的那个线程。

JDK1.6 之后synchronized关键字底层优化

偏向锁、

轻量级锁、

自旋锁:通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)

适应性自旋锁:自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,自适应自旋解决的是"锁竞争时间不确定"的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。

锁消除、

锁粗化等技术来减少锁操作的开销。

synchronized 关键字和 volatile 关键字的区别

a.volatile关键字是线程同步的轻量级实现,只能用于变量而synchronized关键字可以修饰方法以及代码块。 b.多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。 c.volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。 d.volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

CAS

CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

看看AtomicInteger如何实现并发下的累加操作:

arduino 复制代码
public final int getAndAdd(int delta) {    
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

//unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):

a. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。

b. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。

c. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。

d. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。

e. 重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

CAS缺点

ABA问题。

问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?

如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

占用CPU

大量空转占用CPU

线程池ThreadPoolExecutor

线程池的关键参数

corePoolSize       核心线程数量
maximumPoolSize    最大可创建的线程数量:等于核心线程数+加非核心线程数的和
keepAliveTime      线程执行完成后存活的最大时间
unit               时间单位
threadFactory      自己可以提供一个线程工厂,可以为每个线程设置线程名
workQueue          线程队列
handler            达到最大线程数并且线程队列已满的情况下,执行的执行策略

线程池的工作原理

a. 判断核心线程创建数量是否达到最大值,如果没有,则创建一个核心线程执行任务,否则进入下一步。 b. 判断工作队列是否已满,没有则将新建的线程加入工作队列,否则执行下一步。

c. 判断已创建的线程数是否达到了最大值,如果没有,则创建非核心线程执行任务,否则执行下一步。 d. 执行饱和策略,默认抛出异常。

AQS (AbstractQueuedSynchronizer)

AQS是一个用来构建锁和同步器的框架。AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

ThreadLocal

主要是两个方法set和get,可以看到,在set值的时候,会获取到当前线程,然后将值保存在这个线程的ThreadLocalMap中,get值的时候也是先获取到当前线程,然后拿到这个线程中的ThreadLocalMap对象,再从ThreadLocalMap中取出这个值,值是和当前线程绑定的,那么每个线程中的值都是无关的了。

相关推荐
落落落sss14 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
代码敲上天.35 分钟前
数据库语句优化
android·数据库·adb
GEEKVIP3 小时前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20055 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6895 小时前
Android广播
android·java·开发语言
与衫6 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了12 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵13 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru18 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng19 小时前
android 原生加载pdf
android·pdf