自学内容网 自学内容网

Java学习-并发编程

目录

1. 概念

1.1 线程和进程

1.2 并发和并行

1.3 线程上下文切换

2. 线程的基本实现方式

2.1 继承Thread类

2.2 实现Runnable接口

2.3 Callable和Future创建线程

3. 线程状态

3.1 wait与sleep()的区别

4. 多线程源码剖析

5. 线程安全

5.1 线程安全问题

5.2 引发线程安全问题的根本原因

5.3 解决线程安全问题

6. 线程并发三大特性

6.1 指令重排

6.2 CPU和缓存一致性

6.3 Java内存模型

6.4 JMM内存模型抽象结构示意图

6.5 JMM线程操作内存基本规则

6.6 可见性

6.7 happens-before规则

7. 线程同步-synchronized

7.1 作用

7.2 如何解决可见性问题

7.3 Synchronized是如何实现同步

7.4 synchronized原理

7.5 锁优化

8. Volatile关键字

8.1 Volatile简介

8.2 Volatile实现原理

8.3 JMM 内存屏障插入策略

8.4 Volatile缺陷

8.5 Volatile和Synchronized特点比较

1. 概念

1.1 线程和进程

进程:是指内存运行的一个应用程序,是系统运行程序的基本单位,是程序的一次执行过程

线程:是进程中的一个执行单元,负责当前进程中的任务的执行,一个进程会产生很多线程

两者主要区别:每个进程都有独立内存空间。线程之间的堆空间和方法区共享,线程栈空间和 程序计数器是独立的。线程消耗资源比进程小的多

1.2 并发和并行

并发Concurrency:同一时间段,多个任务都在执行,单位时间内不一定是同时执行

并行Parallel:单位时间内,多个任务同时执行,单位时间内一定是同时执行

jvm

1.3 线程上下文切换

一个CPU同一时刻只能被一个线程使用,为了提升效率CPU采用时间片算法将CPU时间片轮流分配给多个线 程。在分配的时间片内线程执行,如果没有执行完毕,则需要挂起然后把CPU让给其他线程。

那么问题来了:线程下次运行时,怎么知道上次运行到哪里呢?
➢ CPU切换线程,会把当前线程的执行位置记录下来,用于下次执行时找到准确位置
➢ 线程执行位置的记录与加载过程就叫做上下文切换
➢ 线程执行位置记录在程序计数器

上下文切换过程:

① 挂起线程01,将线程在CPU的状态(上下文)存储在内存
② 恢复线程02,将内存中的上下文在CPU寄存器中恢复
③ 调转到程序计数器所指定的位置,继续执行之后的代码

jvm

2. 线程的基本实现方式

2.1 继承Thread类

// 线程类
// 1.定义线程类 Remix_Thread 继承 Thread 类
class Remix_Thread extends Thread {
    /**
     * 示例:使用继承Thread类的方式创建一个线程,实现输出1-5
     * 2.子类中重写Thread类中的run方法
     */
    @Override
    public void run() {
        for (int i = 1; i < 6; i++) {
            System.out.println(i);
        }
    }
}

// 测试类 Remix_Test
class Remix_Test {
    public static void main(String[] args) {
        // 方式一:继承Thread类
        // 3.创建线程对象
        Remix_Thread rt = new Remix_Thread();
        // 4.调用线程对象的start方法启动线程
        rt.start();
    }
}

2.2 实现Runnable接口

// 1.定义线程实现类 Remix_Runnable类 实现Runnable接口
class Remix_Runnable implements Runnable {
    /**
     * 示例:使用实现Runnable接口的方式创建一个线程,实现从1输出到5。
     * 2.实现类中重写Runnable接口中的run方法
     */
    @Override
    public void run() {
        for (int i = 1; i < 6; i++) {
            System.out.println(i);
        }
    }
}

class Remix_Test {
    public static void main(String[] args) {
        // 方式二:实现Runnable接口
        // 3.创建Runnable接口的子类对象
        Remix_Runnable rr = new Remix_Runnable();
        // 4.通过Thread类创建线程对象
        // 将Runnable接口的子类对象作为参数传递给Thread类的构造方法
        Thread t1 = new Thread(rr);
        // 5.调用Thread类的start方法启动线程
        t1.start();  
    }
}

2.3 Callable和Future创建线程

// 1.定义子类Remix_Callable,实现Callable接口,带有返回值;
class Remix_Callable implements Callable<Integer> {
    /**
     * 示例:使用Callable和Future的方式创建一个线程,实现从1输出到5。
     * 2.子类中重写Callable接口中的call方法
     */
    @Override
    public Integer call(){
        Integer num = 0;
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
            num = i;
        }
        return num;
    }
}

class Remix_Test {
    public static void main(String[] args) {
        // 方式三:过Callable和Future创建线程
        // 3.创建Callable接口的实现类对象
        Remix_Callable rc = new Remix_Callable();
        // 4.使用FutureTask类来包装Callable对象
        FutureTask task = new FutureTask(rc);
        // 5.通过Thread类的构造器创建线程对象
        // 使用FutureTask象作为Thread对象的 target创建
        // 6.创建Thread对象并调用start方法启动线程
        new Thread(task).start();
        // 输出线程执行后的返回值
        try {
            // 7.调用FutureTask对象的get方法来获得线程执行结束后的返回值;
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

总结: 实现Runnable接口比继承Thread类所具有的优势:
适合多个相同的程序代码的线程去共享同一个资源。
可以避免java中的单继承的局限性。
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

3. 线程状态

01-线程从出生到死亡会出现六种状态:
➢ ①New(新建)、②Runnable(可运行) 、③Terminated(终止) ➢ ④Blocked(锁阻塞)、⑤Waiting(无限等待)、⑥Timed_Waiting(超时等待)

jvm

3.1 wait与sleep()的区别

主要区别:sleep()方法没有释放锁,wait()方法释放了锁
➢ 两者都可以暂停线程执行:wait()常用于线程间交互/通信,sleep()用于暂停线程执行
➢ wait()方法被调用后,需要别的线程调用同一个对象的notify和notifyAll。超时苏醒使用wait(long)方法
➢ sleep()方法执行完成后,线程会自动苏醒

4. 多线程源码剖析

jvm

流程小结:
① 线程类被JVM加载时会绑定native方法与对应的C++方法
② start()方法执行:
➢ start()➔native start0()➔JVM_Thread➔ 创建线程JavaThread::JavaThread
③ 创建OS线程,指定OS线程运行入口:
➢ 创建线程构造方法➔ 创建OS线程➔指定OS线程执行入口,就是线程的run()方法
④ 启动OS线程,运行时会调用指定的运行入口run()方法。至此,实现一个的线程运行
⑤ 创建线程的过程是线程安全的,基于操作系统互斥量(MutexLocker)保证互斥,所以说创建线程性能很差

jvm

jvm

jvm

5. 线程安全

5.1 线程安全问题

➢ 多个线程同时执行,可能会运行同一行代码,如果程序每次运行结果与单线程执行结果一致,且变量的 预期值也一样,就是线程安全的,反之则是线程不安全。

5.2 引发线程安全问题的根本原因

多个线程共享变量

➢ 如果多个线程对共享变量只有读操作,无写操作,那么此操作是线程安全的
➢ 如果多个线程同时执行共享变量的写和读操作,则操作不是线程安全的

5.3 解决线程安全问题

➢ 同步机制Synchronized
➢ Volatile关键字:内存屏障
➢ 原子类:CAS
➢ 锁:AQS
➢ 并发容器

6. 线程并发三大特性

01-并发编程最重要的三个特性:
➢ 原子性:一个系列指令代码,要么全执行,要么都不执行,执行过程不能被打断
➢ 有序性:程序代码按照先后顺序执行
➢ 为什么会出现无序问题呢?因为指令重排
➢ 可见性:当多个线程访问同一个变量时,一个线程修改了共享变量的值,其他线程能够立即看到
➢ 为什么会出现不可见问题呢?因为Java内存模型(JMM)

6.1 指令重排

编译器和处理器会对执行指令进行重排序优化,目的是提高程序运行效率。现象是,我们编写的Java代码 语句的先后顺序,不一定是按照我们写的顺序执行。

jvm​​

6.2 CPU和缓存一致性

➢ 在多核 CPU 中每个核都有自己的缓存,同一个数据的缓存与内存可能不一致
➢ 为什么需要CPU缓存?随着CPU技术发展,CPU执行速度和内存读取速度差距越来越大,导致CPU每次操作内存都 要耗费很多等待时间。为了解决这个问题,在CPU和物理内存上新增高速缓存。
➢ 程序在运行过程中会将运算所需数据从主内存复制到CPU高速缓存,当CPU计算直接操作高速缓存数据,运算结束 将结果刷回主内存。

6.3 Java内存模型

➢ Java为了保证满足原子性、可见性及有序性,诞生了一个重要的规范JSR133,Java内存模型简称JMM
➢ JMM定义了共享内存系统中多线程应用读写操作行为的规范
➢ JMM规范定义的规则,规范了内存的读写操作,从而保证指令执行的正确性
➢ JMM规范解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题
➢ Java实现了JMM规范因此有了Synchronized、Volatile、锁等概念
➢ JMM的实现屏蔽各种硬件和操作系统的差异,在各种平台下对内存的访问都能保证效果一致

6.4 JMM内存模型抽象结构示意图

➢ JMM定义共享变量何时写入,何时对另一个线程可见
➢ 线程之间的共享变量存储在主内存
➢ 每个线程都有一个私有的本地内存,本地内存存储共享变量的副本
➢ 本地内存是抽象的,不真实存在,涵盖:缓存,写缓冲区,寄存器等

jvm​​

6.5 JMM线程操作内存基本规则

① 线程操作共享变量必须在本地内存中,不能直接操作主内存的
② 线程间无法直接访问对方的共享变量,需经过主内存传递

6.6 可见性

什么是内存可见性?
➢ 可见性是一个线程对共享变量的修改,能够及时被其他线程看到

举个栗子:线程A和线程B保证共享变量共享
① 线程A把本地内存A的共享变量副本值更新到主内存
② 线程B到主内存读取最新的共享变量

JMM通过控制线程与本地内存之间的交互,来保证内存可见性

6.7 happens-before规则

在JMM中使用happens-before规则约束编译器优化行为,Java允许编译器优化,但是不能无条件优化。
如果一个操作的执行结果需要对另一个操作可见,那么这两个操作必须存在happens-before的关系!

程序员需要关注的happens-before规则:
➢ 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
➢ 锁规则:对一个锁的解锁, happens-before与随后对这个锁的加锁
➢ Volatile变量规则:对一个volatile修饰的变量的写, happens-before与任意后续对这个变量的读
➢ 传递性:如果A happens-before B,B happens-before C,那么A happens-before C

7. 线程同步-synchronized

7.1 作用

保证方法或代码块在多线程环境运行时,同一个时刻只有一个线程执行代码块

保证方法或代码块在多线程环境运行时,同一个时刻只有一个线程执行代码块。
➢ JDK1.6之前,synchronized的实现依赖于OS底层互斥锁的MutexLock,存在严重的性能问题,是一个重量级锁
➢ JDK1.6之后,Java对synchronized进行的了一系列优化,实现方式也改为Monitor(管程)了,性能与JUC锁比不相上下
➢ 一句话:有了Synchronized,就线程安全了,保证原子性、可见性、有序性

可以修饰方法(静态和非静态)和代码块:
➢ 同步代码块的锁:当前对象,字节码对象,其他对象
➢ 非静态同步方法:锁当前对象
➢ 静态同步方法:锁是当前类的Class对象

7.2 如何解决可见性问题

JMM对于Synchronized的规定:
➢ 加锁前:必须把自己本地内存中共享变量的最新值刷到主内存
➢ 加锁时:清空本地内存中的共享变量,从主内存中读取共享变量最新的值

7.3 Synchronized是如何实现同步

同步操作主要是monitorenter和monitorexit两个jvm指令实现。背后原理是Monitor(管程)

7.4 synchronized原理

什么是Monitor
➢ Monitor意译为管程,直译为监视器。所谓管程,就是管理共享变量及对共享变量操作的过程。让这个过 程可以并发执行。
➢ Java所有对象都可以做为锁,为什么?
➢ 因为每个对象都都有一个Monitor与之关联。然后线程对monitor执行lock和unlock操作,相当于对对象 执行上锁和解锁操作。
➢ Synchronized里面不可以直接使用lock和unlock方法,但当我们使用了synchronized之后,JVM会自 动加入两个指令monitorenter和monitorexit,对应的就是lock和unlock操作。

Monitor的实现原理:将共享变量和对共享变量的操作统一封装起来

7.5 锁优化

加了锁之后,不一定就是好的,很多程序员功力不够,盲目使用Synchronized,虽然解决了线程安全问题, 但也给系统埋下了迟缓的种子。

➢ 并发编程的几种情况:①只有一个线程运行,②两个线程交替执行,③多个线程并发执行
➢ 经过实践经验总结:前两种情况,可以针对性优化
➢ JDK1.6基于这两个场景,设计了两种优化方案:偏向锁和轻量级锁
➢ 同步锁一共有四个状态:无锁,偏向锁,轻量级锁,重量级锁
➢ JVM会视情况来逐渐升级锁,而不是上来就加重量级锁,这就是JDK1.6的锁优化

偏向锁:只有一个线程访问锁资源,偏向锁就会把整个同步措施消除

轻量级锁:只有两个线程交替竞争锁资源,如果线程竞争锁失败了不立即挂起,而是让它飞一会(自旋), 在等待过程中可能锁就会被释放出来,这时尝试重新获取锁

8. Volatile关键字

8.1 Volatile简介

Java语言对volatile的定义:

➢ Java语言允许线程访问共享变量,为了确保共享变量能被准确的一致地更新,线程应该确保通过互斥锁单 独获取这个变量。Java语言提供了volatile,在某些情况下,它比锁要更方便。如果一个变量被声明成 volatile,JMM确保所有线程看到这个变量的值是一致的。

一句话:volatile可以保证多线程场景下共享变量的可见性、有序性

➢ 可见性:保证对此共享变量的修改,所有线程的可见性
➢ 有序性:禁止指令重排序的优化,遵循JMM的happens-before规则

 

8.2 Volatile实现原理

volatile实现内存可见性原理:内存屏障
内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译 器会根据内存屏障的规则禁止重排序。
➢ Volatile变量写操作时:在写操作后加一条store屏障指令,让本地内存中变量的值能够刷新到主内存
➢ Volatile变量读操作时:在读操作前加一条load屏障指令,及时读取到变量在主内存的值

jvm​​​​

 

8.3 JMM 内存屏障插入策略

➢ 在每个 volatile 写前,插入StoreStore 屏障
➢ 在每个 volatile 写后,插入StoreLoad 屏障
➢ 在每个 volatile 读后,插入LoadLoad 屏障
➢ 在每个 volatile 读后,插入LoadStore 屏障

jvm​​​​

 

8.4 Volatile缺陷

存在原子性的问题:虽然volatile可以保证可见性,但是不能满足原子性

volatile适合使用场景:
共享变量独立于其他变量和自己之前的值,这类变量单独使用的时候适合用volatile
➢ 对共享变量的写入操作不依赖其当前值:例如++和--,就不行(比如count++,虽然是一行代码,但是却不是原子性,是jvm指令层面的原子操作)
➢ 共享变量没有包含在有其他变量的不等式中

 

8.5 Volatile和Synchronized特点比较

jvm​​​​


原文地址:https://blog.csdn.net/qq_56760790/article/details/142960225

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!