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



相关文章
|
9天前
|
监控 Java 物联网
Java串口通信技术探究1:深入理解RXTX库
Java串口通信技术探究1:深入理解RXTX库
24 2
|
3天前
|
Kubernetes Java 调度
Java容器技术:Docker与Kubernetes
Java容器技术:Docker与Kubernetes
13 0
|
3天前
|
存储 安全 Java
深入理解Java字节码与反编译技术
深入理解Java字节码与反编译技术
11 0
|
3天前
|
监控 Java Maven
揭秘Java Agent技术:解锁Java工具开发的新境界
作为JDK提供的关键机制,Java Agent技术不仅为Java工具的开发者提供了一个强大的框架,还为性能监控、故障诊断和动态代码修改等领域带来了革命性的变革。本文旨在全面解析Java Agent技术的应用场景以及实现方式,特别是静态加载模式和动态加载模式这两种关键模式。
22 0
|
9天前
|
存储 缓存 前端开发
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
32 3
|
9天前
|
安全 IDE Java
Java串口通信技术探究2:RXTX库单例测试及应用
Java串口通信技术探究2:RXTX库单例测试及应用
25 4
|
9天前
|
存储 前端开发 安全
13:会话跟踪技术Session的深度应用与实践-Java Web
13:会话跟踪技术Session的深度应用与实践-Java Web
24 3
|
9天前
|
存储 前端开发 搜索推荐
12:会话跟踪技术Cookie的深度应用与实践-Java Web
12:会话跟踪技术Cookie的深度应用与实践-Java Web
22 4
|
11天前
|
供应链 Java API
Java 8新特性解析及应用区块链技术在供应链管理中的应用与挑战
【4月更文挑战第30天】本文将深入探讨Java 8的新特性,包括Lambda表达式、Stream API和Optional类等。通过对这些新特性的详细解析和应用实例,帮助读者更好地理解和掌握Java 8的新技术。
|
11天前
|
存储 安全 Java
【亮剑】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能
【4月更文挑战第30天】`ConcurrentHashMap`是Java中线程安全的哈希表,采用锁定分离技术提高并发性能。数据被分割成多个Segment,每个拥有独立锁,允许多线程并发访问不同Segment。当写操作发生时,计算键的哈希值定位Segment并获取其锁;读操作通常无需锁定。内部会根据负载动态调整Segment,减少锁竞争。虽然使用不公平锁,但Java 8及以上版本提供了公平锁选项。理解其工作原理对开发高性能并发应用至关重要。