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();
        }
    }
}



相关文章
|
6天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
31 7
|
6天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
23天前
|
监控 前端开发 Java
【技术开发】接口管理平台要用什么技术栈?推荐:Java+Vue3+Docker+MySQL
该文档介绍了基于Java后端和Vue3前端构建的管理系统的技术栈及功能模块,涵盖管理后台的访问、登录、首页概览、API接口管理、接口权限设置、接口监控、计费管理、账号管理、应用管理、数据库配置、站点配置及管理员个人设置等内容,并提供了访问地址及操作指南。
|
1月前
|
JSON 前端开发 JavaScript
java-ajax技术详解!!!
本文介绍了Ajax技术及其工作原理,包括其核心XMLHttpRequest对象的属性和方法。Ajax通过异步通信技术,实现在不重新加载整个页面的情况下更新部分网页内容。文章还详细描述了使用原生JavaScript实现Ajax的基本步骤,以及利用jQuery简化Ajax操作的方法。最后,介绍了JSON作为轻量级数据交换格式在Ajax应用中的使用,包括Java中JSON与对象的相互转换。
43 1
|
1月前
|
SQL 监控 Java
技术前沿:Java连接池技术的最新发展与应用
本文探讨了Java连接池技术的最新发展与应用,包括高性能与低延迟、智能化管理和监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,为开发者提供了一份详尽的技术指南。
33 7
|
1月前
|
移动开发 前端开发 Java
过时的Java技术盘点:避免在这些领域浪费时间
【10月更文挑战第14天】 在快速发展的Java生态系统中,新技术层出不穷,而一些旧技术则逐渐被淘汰。对于Java开发者来说,了解哪些技术已经过时是至关重要的,这可以帮助他们避免在这些领域浪费时间,并将精力集中在更有前景的技术上。本文将盘点一些已经或即将被淘汰的Java技术,为开发者提供指导。
85 7
|
1月前
|
SQL Java 数据库连接
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率
在Java应用中,数据库访问常成为性能瓶颈。连接池技术通过预建立并复用数据库连接,有效减少连接开销,提升访问效率。本文介绍了连接池的工作原理、优势及实现方法,并提供了HikariCP的示例代码。
48 3
|
1月前
|
SQL 监控 Java
Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面
本文探讨了Java连接池技术的最新发展,包括高性能与低延迟、智能化管理与监控、扩展性与兼容性等方面。同时,结合最佳实践,介绍了如何选择合适的连接池库、合理配置参数、使用监控工具及优化数据库操作,以实现高效稳定的数据库访问。示例代码展示了如何使用HikariCP连接池。
16 2
|
1月前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
30 4
|
1月前
|
Java 数据库连接 数据库
深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能
在Java应用开发中,数据库操作常成为性能瓶颈。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接、减少连接建立和断开的开销,从而显著提升系统性能。文章介绍了连接池的优势、选择和使用方法,以及优化配置的技巧。
31 1