2023年Java核心技术第九篇(篇篇万字精讲)(上)

简介: 2023年Java核心技术第九篇(篇篇万字精讲)(上)

十七 . 并发相关基础概念



可能前面几讲,一些同学理解可以有一些困难,这一篇将进行一些并发相关概念比较模糊,我们将进行并发相关概念的补充,


17.1 线程安全


线程安全就是在多线程的环境下正确的一个概念,保证在多线程的环境下是实现共享的,可修改的状态是正确性,状态可以类比为程序里面的数据。


如果状态不是共享的,或者不是可修改的,就不存在线程安全的问题。


17.2 保证线程安全的两个方法


17.2.1 封装


进行封装,我们将对象内部的状态隐藏,保护起来。


17.2.2 不可变


可以进行final和immutable进行设置。


17.2.2.1 final 和 immutable解释


finalimmutable 是 Java 中用来描述对象特性的关键字。


final:用于修饰变量、方法和类。它的作用如下:

  • 变量:final 修饰的变量表示该变量是一个常量,不可再被修改。一旦赋值后,其值不能被改变。通常用大写字母表示常量,并在声明时进行初始化。
  • 方法:final 修饰的方法表示该方法不能被子类重写(覆盖)。
  • 类:final 修饰的类表示该类不能被继承。
  1. immutable:指的是对象一旦创建后,其状态(数据)不能被修改。不可变对象在创建后不可更改,任何操作都不会改变原始对象的值,而是返回一个新的对象。


不可变对象的主要特点包括:

  • 对象创建后,其状态无法更改。
  • 所有字段都是 final 和私有的,不可直接访问和修改。
  • 不提供可以修改对象状态的公共方法。


不可变对象的优点包括:

  • 线程安全:由于对象状态不可更改,因此多线程环境下不需要额外的同步措施。
  • 缓存友好:不可变对象的哈希值不会改变,因此可以在哈希表等数据结构中获得更好的性能。


17.3 线程安全的基本特性


17.3.1 原子性(Atomicity)


指的是一系列操作要么全部执行成功,要么全部失败回滚。即一个操作在执行过程中不会被其他线程打断,保证了操作的完整性。


17.3.2 可见性(Visibility)


指的是当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。需要通过使用 volatile 关键字、synchronized 关键字、Lock 接口等机制来确保可见性。

详细解释:


17.3.2.1  volatile 关键字


当一个变量被声明为volatile时,任何对该变量的修改都会立即被其他线程可见。

当写线程将flag值修改为true后,读线程会立即看到最新的值,并进行相应的操作。这是因为flag变量被声明为volatile,确保了可见性。


public class VisibilityExample {
    private volatile boolean flag = false;
    public void writerThread() {
        flag = true; // 修改共享变量的值
    }
    public void readerThread() {
        while (!flag) {
            // 循环等待直到可见性满足条件
        }
        System.out.println("Flag is now true");
    }
}


17.3.2.2 synchronized 关键字


两个方法都使用synchronized关键字修饰,确保了对flag变量的原子性操作和可见性。当写线程修改flag的值为true后,读线程能够立即看到最新的值。


public class VisibilityExample {
    private boolean flag = false;
    public synchronized void writerThread() {
        flag = true; // 修改共享变量的值
    }
    public synchronized void readerThread() {
        while (!flag) {
            // 循环等待直到可见性满足条件
        }
        System.out.println("Flag is now true");
    }
}


17.3.2.3  Lock 接口


通过使用ReentrantLock实现了显式的加锁和释放锁操作。当写线程获取锁并修改flag的值为true后,读线程也需要获取同样的锁才能看到最新的值。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class VisibilityExample {
    private boolean flag = false;
    private Lock lock = new ReentrantLock();
    public void writerThread() {
        lock.lock();
        try {
            flag = true; // 修改共享变量的值
        } finally {
            lock.unlock();
        }
    }
    public void readerThread() {
        lock.lock();
        try {
            while (!flag) {
                // 循环等待直到可见性满足条件
            }
            System.out.println("Flag is now true");
        } finally {
            lock.unlock();
        }
    }
}


17.3.2.3.1 解释Lock接口:


使用Lock接口进行同步时,通过持有锁可以确保在临界区内的操作是互斥的,即同一时间只能有一个线程执行临界区的代码。这样可以避免多个线程同时对共享变量进行修改带来的问题。


当读线程在访问共享变量之前,发现变量的值不符合预期,即不满足可见性条件时,它会进入循环等待的状态。这样做的目的是等待写线程将最新的值写回共享变量,并使其对其他线程可见。


循环等待的方式可以有效地解决可见性问题。当写线程修改共享变量的值后,它会释放锁。此时,读线程能够重新获取锁并再次检查共享变量的值。如果值已经满足可见性条件,读线程就能够继续执行后续的操作。


需要注意的是,在循环等待的过程中,读线程应该使用适当的等待方式,例如Thread.sleep()或者Lock接口提供的Condition条件对象的await()方法,以避免占用过多的CPU资源。


通过循环等待直到可见性满足条件,可以确保读线程在访问共享变量时能够看到最新的值,从而实现了可见性的要求。


17.3.3 有序性


指的是程序执行的顺序与预期的顺序一致,不会受到指令重排序等因素的影响。可以通过 volatile 关键字、synchronized 关键字、Lock 接口、happens-before 原则等来保证有序性。

例子:


17.3.3.1 volatile 关键字


使用volatile关键字修饰counter变量,确保了对变量的读写操作具有可见性和有序性。其他线程能够立即看到最新的值,并且操作的顺序不会被重排序。


public class OrderingExample {
    private volatile int counter = 0;
    public void increment() {
        counter++; // 非原子操作,但通过volatile关键字确保了可见性和有序性
    }
    public int getCounter() {
        return counter; // 获取变量的值
    }
}


17.3.3.2 synchronized 关键字


使用synchronized关键字修饰了increment()和getCounter()方法,确保了对counter变量的原子操作,同时也提供了可见性和有序性的保证。


public class OrderingExample {
    private int counter = 0;
    public synchronized void increment() {
        counter++; // 原子操作,同时具备可见性和有序性
    }
    public synchronized int getCounter() {
        return counter; // 获取变量的值
    }
}


17.3.3.3 Lock 接口


通过使用Lock接口实现了显式的加锁和释放锁操作,确保了对counter变量的原子操作,同时也提供了可见性和有序性的保证。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class OrderingExample {
    private int counter = 0;
    private Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            counter++; // 原子操作,同时具备可见性和有序性
        } finally {
            lock.unlock();
        }
    }
    public int getCounter() {
        return counter; // 获取变量的值
    }
}


17.3.3.4 happens-before 原则


happens-before是并发编程中的一个概念,用于描述事件之间的顺序关系。在多线程或多进程的环境中,经常会出现多个事件同时发生的情况,而它们之间的执行顺序可能是不确定的。为了确保程序正确地执行,我们需要定义一些规则来解决竞态条件和并发问题。


happens-before关系用于描述事件之间的顺序关系,并指定了一个事件在执行结果上的先于另一个事件。如果一个事件A happens-before 另一个事件B,那么我们可以说事件A在时间上 "早于" 事件B,而事件B在时间上 "晚于" 事件A。


根据Java内存模型(Java Memory Model,简称JMM)的规定。


happens-before关系例子:

  1. 程序顺序原则(Program Order Rule):在单个线程中,按照程序的顺序,前面的操作 happens-before 后面的操作。
  2. volatile变量规则(Volatile Variable Rule):对一个volatile域的写操作 happens-before 于后续对该域的读操作。volatile变量的写-读能够确保可见性。
  3. 传递性(Transitive):如果事件A happens-before 事件B,事件B happens-before 事件C,那么可以推导出事件A happens-before 事件C。通过传递性,可以推断出不同事件之间的happens-before关系。
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法调用 happens-before 新线程的所有操作。
  5. 线程终止规则(Thread Termination Rule):线程的所有操作 happens-before 其他线程中对该线程终止检测的操作。
  6. 线程中断规则(Thread Interruption Rule):对线程的interrupt()方法的调用 happens-before 所被中断线程中的代码检测到中断事件的发生。


例子:


17.3.3.4.1 线程中断规则(Thread Interruption Rule):


线程A会执行一段任务。在线程A的任务执行的过程中,会循环检查中断状态,当线程B调用线程A的interrupt()方法进行中断时,线程A会在检查中断状态的代码处发现自己已被中断并返回。这里,线程B的interrupt()调用和线程A的检查中断状态的操作之间存在一个happens-before关系,保证线程B中的中断操作能被线程A正确检测到。


class MyTask implements Runnable {
    @Override
    public void run() {
        // 执行任务的代码
        // ...
        // 检查中断状态
        if (Thread.interrupted()) {
            // 在此处被中断
            return;
        }
        // 继续执行任务的代码
        // ...
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new MyTask());
        threadA.start();
        // 主线程等待一段时间后中断线程A
        Thread.sleep(1000);
        threadA.interrupt();
    }
}


17.3.3.4.2  线程终止规则


主线程首先创建一个子线程,并将isRunning设置为true,然后子线程进入一个死循环,并在每次循环中检查isRunning的值。主线程等待2秒后,将isRunning设置为false,终止子线程的执行,并使用join()方法等待子线程终止。最后,主线程打印出"主线程继续执行"。


子线程的终止操作isRunning = false happens-before 主线程中对isRunning的读取操作,因此主线程能够观察到子线程的终止,并能够继续执行。这符合线程终止规则。


public class ThreadTerminationExample {
    private static volatile boolean isRunning = true;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (isRunning) {
                // 线程执行的工作...
            }
            System.out.println("线程已终止");
        });
        thread.start();
        Thread.sleep(2000);
        isRunning = false; // 终止线程
        thread.join(); // 等待线程终止
        System.out.println("主线程继续执行");
    }
}


happens-before关系的定义保证了程序执行的可见性和有序性,为并发编程提供了一定的保证。开发人员可以利用这些规则来避免竞态条件和并发问题。


17.3.4 互斥性


指的是同一时间只允许一个线程对共享资源进行操作,其他线程必须等待。可以通过使用 synchronized 关键字、Lock 接口来实现互斥性。


17.3.4.1 synchronized 关键字例子:


使用synchronized关键字修饰了increment()和getCount()方法,这意味着同一时间只能有一个线程访问这两个方法。当一个线程在执行increment()方法时,其他线程需要等待,直到当前线程执行完毕才能继续访问。这样可以保证count的操作是原子的,避免了并发访问导致的数据冲突。


public class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public synchronized int getCount() {
        return count;
    }
}


17.3.4.2 Lock 接口例子:


使用ReentrantLock来创建一个锁,并在increment()和getCount()方法中使用lock()方法获取锁,unlock()方法释放锁。这样同一时间只允许一个线程获取锁并执行代码块,其他线程需要等待锁被释放后才能继续执行,从而实现了互斥性。


无论是使用synchronized关键字还是Lock接口,它们都能够实现互斥性,保证多线程对共享资源的访问是同步的,避免了数据冲突和不一致的问题。但Lock接口相比synchronized关键字更加灵活,可以更精细地控制锁的获取和释放,提供了更多的功能。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}



相关文章
|
2月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
270 44
|
2月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
313 1
|
3月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
97 4
|
3月前
|
Java API Maven
2025 Java 零基础到实战最新技术实操全攻略与学习指南
本教程涵盖Java从零基础到实战的全流程,基于2025年最新技术栈,包括JDK 21、IntelliJ IDEA 2025.1、Spring Boot 3.x、Maven 4及Docker容器化部署,帮助开发者快速掌握现代Java开发技能。
712 1
|
4月前
|
人工智能 Java
Java多任务编排技术
JDK 5引入Future接口实现异步任务处理,但获取结果不够灵活。Java 8新增CompletableFuture,实现异步任务编排,支持流式处理、多任务组合及异常处理,提升执行效率与代码可读性,简化并发编程复杂度。
102 0
|
3月前
|
Java 测试技术 API
2025 年 Java 开发者必知的最新技术实操指南全览
本指南涵盖Java 21+核心实操,详解虚拟线程、Spring Boot 3.3+GraalVM、Jakarta EE 10+MicroProfile 6微服务开发,并提供现代Java开发最佳实践,助力开发者高效构建高性能应用。
525 4
|
2月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
224 0
|
3月前
|
JavaScript 安全 前端开发
Java开发:最新技术驱动的病人挂号系统实操指南与全流程操作技巧汇总
本文介绍基于Spring Boot 3.x、Vue 3等最新技术构建现代化病人挂号系统,涵盖技术选型、核心功能实现与部署方案,助力开发者快速搭建高效、安全的医疗挂号平台。
195 3
|
4月前
|
存储 Java Linux
操作系统层面视角下 Java IO 的演进路径及核心技术变革解析
本文从操作系统层面深入解析Java IO的演进历程,涵盖BIO、NIO、多路复用器及Netty等核心技术。分析各阶段IO模型的原理、优缺点及系统调用机制,探讨Java如何通过底层优化提升并发性能与数据处理效率,全面呈现IO技术的变革路径与发展趋势。
91 2
|
4月前
|
安全 Java 微服务
Java 最新技术和框架实操:涵盖 JDK 21 新特性与 Spring Security 6.x 安全框架搭建
本文系统整理了Java最新技术与主流框架实操内容,涵盖Java 17+新特性(如模式匹配、文本块、记录类)、Spring Boot 3微服务开发、响应式编程(WebFlux)、容器化部署(Docker+K8s)、测试与CI/CD实践,附完整代码示例和学习资源推荐,助你构建现代Java全栈开发能力。
478 0