专栏首页用户3288143的专栏Java多线程与并发-原理

Java多线程与并发-原理

synchronized

堆是线程间共享的,要合理的给一个对象上锁。

线程安全主要诱因

互斥锁特性

获取对象锁

同对象,异步

同对象,同步代码块

同对象,非静态同步方法

同对象,同步代码块 对比 非静态同步方法

不同对象,同步代码块 对比 非静态同步方法

获取类锁

类锁通过对象锁实现的

和对象锁的变动-代码块和静态方法

同对象,类锁 对比 对象锁

因为类锁是对象锁实现的 同对象,传入了同种对象实例

不同对象,类锁 对比 对象锁

互不干扰,因为锁两个对象锁

同对象,类锁 对比 对象锁

代码实现

SyncDemo

package thread;

public class SyncDemo {
    public static void main(String... args) {
        SyncThread syncThread = new SyncThread();
        // 异步
        Thread A_thread1 = new Thread(syncThread, "A_thread1");
        Thread A_thread2 = new Thread(syncThread, "A_thread2");

        // 对象锁
        // 同步代码块
        Thread B_thread1 = new Thread(syncThread, "B_thread1");
        Thread B_thread2 = new Thread(syncThread, "B_thread2");
        // 同步非静态方法
        Thread C_thread1 = new Thread(syncThread, "C_thread1");
        Thread C_thread2 = new Thread(syncThread, "C_thread2");

        // 类锁
        // 同步代码块
        Thread D_thread1 = new Thread(syncThread, "D_thread1");
        Thread D_thread2 = new Thread(syncThread, "D_thread2");
        // 同步静态方法
        Thread E_thread1 = new Thread(syncThread, "E_thread1");
        Thread E_thread2 = new Thread(syncThread, "E_thread2");
        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
        D_thread1.start();
        D_thread2.start();
        E_thread1.start();
        E_thread2.start();
    }
}

SyncThread

package thread;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SyncThread implements Runnable {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            syncObjectBlock1();
        } else if (threadName.startsWith("C")) {
            syncObjectMethod1();
        } else if (threadName.startsWith("D")) {
            syncClassBlock1();
        } else if (threadName.startsWith("E")) {
            syncClassMethod1();
        }

    }

    /**
     * 异步方法
     */
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法中有 synchronized(this|object) {} 同步代码块
     */
    private void syncObjectBlock1() {
        System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized 修饰非静态方法
     */
    private synchronized void syncObjectMethod1() {
        System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void syncClassBlock1() {
        System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (SyncThread.class) {
            try {
                System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized static void syncClassMethod1() {
        System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

总结

synchronized底层实现原理

实现synchronized基础

Java对象头 Monitor

对象在内存中的布局

对象头 实例数据 对齐填充

对象头结构

synchronized的锁对象是存在对象头里的 MarkWord存储对象运行时数据。 是实现 轻量级锁 和 偏向锁(Java6后新增加的)的关键 由于对象头信息是与对象自身定义没有关系的额外存储成本,因此考虑到JVM的效率,MarkWord 被设计为一个非固定的数据结构,以便存储更多的有效数据,根据对象状态复用自身的存储空间 重量级锁:synchronized对象锁,指向的指针是Monitor的起始地址。每个对象和Monitor存在关联,对象与Monitor之间有多种实现形式 如:对象可以和Monitor同时创建或销毁 或 单线程试图获得对象锁时,自动生成。 Monitor被某个线程持有后变为锁定状态 在Java虚拟机及hospot虚拟机,Monitor是由hospotMonitor实现的(源码C++实现的)

MonitorJava娘胎创建的锁 管程 或 监视器锁(描述为 同步工具,或同步机制,通常被描述为同步对象)

Monitor锁的竞争、获取与释放

Monitor存在每个对象的对象头中,synchronized通过这种方式获取锁 ,这也是Java任意对象可以作为锁的原因

字节码层面分析

看.class里边的文件 javap -verbose

什么是重入-知识补充

重入请求将会成功,synchronize是Java原子性的内部锁机制是可重入的

同步块-字节码分析

线程通过计数器判断,本次是否应该先阻塞

同步方法体-字节码分析

没有monitorenter、monitorexit、指令少。 方法体是隐式的,即无需通过字节码控制 正常完成或非正常完成,都释放monitor 如果内有异常,内部无法处理,会抛到外部直到释放

为什么会对synchronized嗤之以鼻

重量级锁,监视器锁Monitor运行在底层操作系统。切换线程,时间长。效率低

Java6后,在JVM层面做了大量优化

自适应自旋 锁消除 锁粗化 轻量级锁 偏向锁 …… 为了在线程间更高效的共享数据,解决竞争问题,高效程序执行效率

自旋锁

等一会,不放弃cpu,忙循环等待持有锁的线程释放锁 不像sleep一样放弃cpu的执行时间 Java4被引入默认关闭,但Java6才启用 本质上与阻塞并不相通,考虑其对多处理器 的要求

自适应自旋锁

每次线程需要等待的时间还有次数是不固定的 因此PreBlockSpin无法设计的很合理 JVM对锁的预测越来越精准,JVM越来越聪明

锁消除

StringBuffer虽然是线程安全的,但操作本地变量(不是共享的)JVM消除内部锁,避免资源浪费

锁粗化

synchronized的四种状态

随竞争情况逐渐升级 其实锁降级会发生:当JVM进入前面的安全点,会检测是否有闲置的Monitor ,然后试图降级

偏向锁

CAS无锁算法

轻量级锁

加锁过程

初始-无锁01 拷贝-(CAS更新)双向指向-成功轻量级00 失败-判断markword是否指向栈帧 ?有则说明当前线程拥有这个对象的锁,进入同步块:没有则说明多个线程竞争锁,轻量级锁->重量级锁10 变成重量级锁时候,其他线程会阻塞,自旋会防止阻塞

解锁

锁的内存语义-补充知识-解锁轻量级怎么就成功了?

偏向锁、轻量级锁、重量级锁 汇总

synchronized和ReentrantLock

ReentrantLock的区别

Java5之前只有synchronized,从5之后有了ReentrantLock(再入锁)

源码ReentrantLock-AQS

AQS

state数组成员表存状态 队列 CAS基础 acquire获取资源独占权 release释放对某个资源的独占

ReentrantLock公平性设置

理解

公平性Java中一般用的不多,synchronized一般也不会引起饥饿, 除非必须要用公平性,因为开启公平性会增加额外的开销,导致程序吞吐量下降

公平效果图

不公平效果图

ReentrantLock将锁对象化

其他对象化

追源码

ArrayBlockingQueue 数组实现、线程安全的(ReentrantLock互斥锁保护资源)、有界的(数组长度)、阻塞队列 ArrayBlockingQueue与Condition是组合的关系 Condition依附于ArrayBlockingQueue存在

队列为空等待有新的消息加入再返回 条件判断通知等待线程

来自AQS框架

总结synchronized和ReentrantLock区别

区别

追源码分析

unsafe 类似后门工具 可以在任意内存位置读写数据,对普通用户操作比较危险 支持一些CAS操作

jmm的内存可见性

Java内存模型中的happens-before

Jmm

工作内存是每个线程的私有区域

Jmm主内存

Jmm工作内存

Jmm和Java比较

主内存:好似,堆、方法区 工作内存:

主内存和工作内存-数据存储类型-操作方式 归纳

Jmm如何解决可见性

忽略硬件部分,简单理解为从内存读取到缓存取处理,后存入内存。 线程共享变量,一致性问题。由于多线程,很有可能第二条线程处理的数据是前一条线程处理前的旧状态,为此引入了复杂的数据依赖性。 重排序要尊重数据依赖性的要求,否则就打破了数据的正确性。

指令重排序的满足条件

为了提高性能。处理器,编译器常常对指令重排序(不能随意重排序) JMM一般通过禁止某些指令的重排序,来保障内存可见性,也就是实现了happens-before规则。

happens-before原则

使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行。

Thread Join 方法。在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

上述不满足线程安全,需要通过以下两条变成happens-before 修复代码:(使满足锁 或 volatile变量规则)

volatile内存语义-补充知识

volatile保证写的可见性 但是多线程的运算 操作并不保证安全性

synchronized控制直接反馈给主存

利用立即可见的原子性

volatile如何立即可见

通过(对主内存) 写 读 的方式,达到立即可见的效果

volatile如何禁止重排优化

内存屏障 cpu指令 因为编译器、处理器,都会影响指令的重排优化。 如果插入 内存屏障 ,则告诉编译器、或cpu,任何指令都不能与 内存屏障 重排序,不能打乱他们的顺序

线程安全的单例实现算法-单例双重检测

volatile和synchronize的区别

CAS

CAS线程安全,乐观锁

cynchronize属于悲观锁,始终假定会出现并发冲突,因此屏蔽一切可能违反数据完整性的操作 乐观锁则,假设不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败,则会重试,乐观锁最常见的是CAS CAS上层感觉无锁,其实底层还是有加锁行为的。操作失败,自己决定,因此不会被阻塞挂起

CAS思想

V和A相比较,如果一致,会将该位置的值更新为新值。否则不做任何操作 V是主内存的值

场景举例,追源码

场景举例: 当要更新一个共享变量的值。 会取该变量的值到A,然后经过计算,得到新值B 需要更新共享变量的值的时候,调用CAS方法去更新共享变量的值

volatile保证线程可见性,同时不允许线程对其重排序,但是不能保证下面三个指令原子执行,在多线程并发无法做到线程安全得到正确结果 改进方案(可行,但近一步提升性能,不用synchronized的悲观锁)

深入到jvm指令集源码会发现,不同的体系架构指令集不同

CAS多数情况下对开发者来说是透明的

多数情况下,开发者不需要直接利用CAS代码实现线程安全容器 更多的是使用并发包间接的享受在扩展性的好处

CAS虽然高效的解决了原子操作问题 循环长 比如上方:getAddInt,如果失败一直while尝试 CAS可保障一个共享变量 原子性,多个共享变量就得用锁来保障原子性 ABA:如果V初次读取的是A,(这期间可能被改变过B)并在准备赋值的时候仍然为A。可能被误认为重来没有被改变过 为解决ABA问题:JUC带有标记的原子引用类, 通过控制变量值的版本来解决ABA的问题 改为传统的互斥同步,可能比原子的更高效 设计程序前,想好ABA问题是否影响程序的并发性

Java线程池

利用Executors创建五种不同线程池满足不同场景需求

服务端处理并发请求多,但每个线程执行的时间很短,就会频繁的创建销毁请求,会大大降低系统效率。可能出现创建销毁的时间,要比处理用户请求的时间和资源更多,那么有没有一种方法重复利用线程去完成新任务呢?

Fork/Join框架

为啥要用线程池

通过重复利用已经创建的线程,降低(线程频繁创建、销毁)资源消耗 使用线程池可统一进行分配、调优、监控

追源码

1

2

3

深入

4

5

后面又是Excecutor

Executor

以上都是属于此框架的体系的类或接口 根据一组执行策略调度异步任务的框架,目的是将任务提交,和任务如何运行分离开的机制 Executors提供尽量简化操作的工厂方法

JUC的三个Executor接口

三接口

Executor

对与不同execute实现可能情况 创建新线程 传入已有线程 根据线程池的容量 阻塞队列的容量,来决定传入的线程是否放入阻塞队列中,或者拒绝接受阻塞的线程 执行方法 直接将线程给execute方法去执行

ExecutorService实现更多管理生命周期的方法

非5场景的,自定义创建线程池场景-ThreadPoolExecutor来创建

简单了解ThreadPoolExecutor来创建

大多数场景使用Executor提供的5种线程池就够,但其他场景还是要自己用ThreadPoolExecutor来创建 分析线程池的设计与实现: 队列存储用户的提交,是一个工作队列 队列接到客,就排队提交给内部的线程池(工作线程的集合),该集合需要管理线程的创建和销毁 当线程池压力较大,会创建新的线程,当任务量变小,线程池会闲置一段时间,结束线程 。线程池的工作线程,被抽象为静态内部类worker,TreadPool实际上维护的就是一组worker对象

worker追源码,分析组成

firstTask来保存传入的任务 thread调用构造方法函数时,通过ThreadFactory来创建的线程

因为worker实现了runable方法 调用run方法去执行里面的逻辑

比如任务提交被拒绝,线程池已经处于shutdown的状态,此时新来的任务需要有处理机制来处理

组成成分体现在-构造函数

以及最后补充的饱和策略

长期驻留线程数,对不同线程池可能会有很大区别

比如

比如固定

或动态

线程提交时,线程池的线程数量大于corePoolSize,把该任务封装成一个worker对象放入等待队列。 由于存在许多种等待队列,每种不同队列有不同的排队机制。可以自行体会感受线程池带来的灵活

线程池维护线程,所允许的空闲时间。 当线程池里的线程数量大于corePoolSize,如果这时没有新的任务提交,核心线程不会立即被销毁,直到等待时间超过keepAliveTime,才会被销毁

会使新创建的线程具有相同的优先级,并且是非守护线程,同时也设置了线程名称

如果阻塞队列满了,并且没有空闲的线程,如果继续提交任务,就需要采取一种策略

组成成分流程-简单模拟

细节-一种优雅的传值方式

线程池和线程一样也会有生命周期,生命周期通过状态值来表示的。 线程池用来管理线程,因此对线程的数量一定是了如指掌的,有存储当前有效线程数 ThreadPoolExecutor将状态值和有效线程数合二为一,存储到了ctl ctl高三位存状态值,低29位保存有效线程数

对ctl进行优雅计算的(用的与或非,执行起来相当高效) 获取运行状态 获取活动线程数 ctl获取两者

线程池的五种状态

TIDYING workercount有效线程数为零,正在做打扫操作 TERMINATED什么也不做只作为一个标识

线程池大小如何选定

没有固定,看工作经验

推荐书籍

Java并发编程实战

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 解决:已损坏,无法打开,您应该将它移到废纸篓

    macOS Catalina下,即便已通过在终端中输入命令 sudo spctl --master-disable 允许APP任意来源,也会出现某些APP安装后...

    瑞新
  • VLAN是什么?划分VLAN的作用及方法

    有朋友提到了如何划分vlan,其实划分vlan是网络技术应用中必不可少,很多的网络都需要进行vlan的划分,今天就一起了解下这方面的内容。

    瑞新
  • python_#函数求本息

    瑞新
  • Java原子类操作原理剖析

    对于并发控制来说,使用锁是一种悲观的策略。它总是假设每次请求都会产生冲突,如果多个线程请求同一个资源,则使用锁宁可牺牲性能也要保证线程安全。而无锁则是比较乐观的...

    Java学习录
  • 优化指南,详解 Tomcat 的连接数与线程池

    在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)。

    java思维导图
  • Java多线程Thread VS Runnable详解

    进程是程序在处理机中的一次运行。一个进程既包括其所要执行的指令,也包括了执行指令所需的系统资源,不同进程所占用的系统资源相对独立。所以进程是重量级的任务,它们之...

    Java后端工程师
  • 打通 Java 任督二脉 —— 并发数据结构的基石

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLoc...

    老钱
  • JDK14性能管理工具:jstack使用介绍

    在之前的文章中,我们介绍了JDK14中jstat工具的使用,本文我们再深入探讨一下jstack工具的使用。

    程序那些事
  • 【转】Java并发的AQS原理详解

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantL...

    一枝花算不算浪漫
  • 详解 Tomcat 的连接数与线程池

    前言 在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)。 在前面的文章 详...

    Java高级架构

扫码关注云+社区

领取腾讯云代金券