java多线程3:原子性,可见性,有序性

简介:

java多线程3:原子性,可见性,有序性

概念
在了解线程安全问题之前,必须先知道为什么需要并发,并发给我们带来什么问题。

为什么需要并发,多线程?

时代的召唤,为了更充分的利用多核CPU的计算能力,多个线程程序可通过提高处理器的资源利用率来提升程序性能。
方便业务拆分,异步处理业务,提高应用性能。
多线程并发产生的问题?

大量的线程让CPU频繁上下文切换带来的系统开销。
临界资源线程安全问题(共享,可变)。
容易造成死锁。
注意:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性质,不会导致线程安全问题。

可见性
多线程访问同一个变量时,如果有一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。这是因为为了保证多个CPU之间的高速缓存是一致的,操作系统会有一个缓存一致性协议,volatile就是通过OS的缓存一致性协议策略来保证了共享变量在多个线程之间的可见性。

public class ThreadDemo2 {

private static boolean flag = false;

public void thread_1(){
    flag = true;
    System.out.println("线程1已对flag做出改变");
}

public void thread_2(){
    while (!flag){
    }
    System.out.println("线程2->flag已被修改,成功打断循环");
}

public static void main(String[] args) {
    ThreadDemo2 threadDemo2 = new ThreadDemo2();
    Thread thread2 = new Thread(()->{
        threadDemo2.thread_2();
    });
    Thread thread1= new Thread(()->{
        threadDemo2.thread_1();
    });
    thread2.start();
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread1.start();
}

}

执行结果

线程1已对flag做出改变
代码无论执行多少次,线程2的输出语句都不会被打印。为flag添加volatile修饰后执行,线程2执行的语句被打印

执行结果

线程1已对flag做出改变
线程2->flag已被修改,成功打断循环
局限:volatile只是保证共享变量的可见性,无法保证其原子性。多个线程并发时,执行共享变量i的i++操作<==> i = i + 1,这是分两步执行,并不是一个原子性操作。根据缓存一致性协议,多个线程读取i并对i进行改变时,其中一个线程抢先独占i进行修改,会通知其他CPU我已经对i进行修改,把你们高速缓存的值设为无效并重新读取,在并发情况下是可能出现数据丢失的情况的。

public class ThreadDemo3 {

private volatile static int count = 0;
public static void main(String[] args) {
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(()->{
            for (int j = 0; j < 1000; ++j){
                count++;
            }
        });
        thread.start();
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("count执行的结果为->" + count);
}

}

执行结果

count执行的结果为->9561
注意:这个结果是不固定的,有时10000,有时少于10000。

原子性
就像恋人一样同生共死,表现在多线程代码中程序一旦开始执行,就不会被其他线程干扰要嘛一起成功,要嘛一起失败,一个操作不可被中断。在上文的例子中,为什么执行结果不一定等于10000,就是因为在count++是多个操作,1.读取count值,2.对count进行加1操作,3.计算的结果再赋值给count。这几个操作无法构成原子操作的,在一个线程读取完count值时,另一个线程也读取他并给它赋值,根据缓存一致性协议通知其他线程把本次读取的值置为无效,所以本次循环操作是无效的,我们看到的值不一定等于10000,如何进行更正---->synchronized关键字

public class ThreadDemo3 {

private volatile static int count = 0;
private static Object object = new Object();
public static void main(String[] args) {
    for (int i = 0; i < 10; ++i){
        Thread thread = new Thread(()->{
            for (int j = 0; j < 1000; ++j){
                synchronized (object){
                    count++;
                }
            }
        });
        thread.start();
    }
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("count执行的结果为->" + count);
}

}

执行结果

count执行的结果为->10000
加锁后,线程在争夺执行权就必须获取到锁,当前线程就不会被其他线程所干扰,保证了count++的原子性,至于synchronized为什么能保证原子性,篇幅有限,下一篇在介绍。

有序性
jmm内存模型允许编译器和CPU在单线程执行结果不变的情况下,会对代码进行指令重排(遵守规则的前提下)。但在多线程的情况下却会影响到并发执行的正确性。

public class ThreadDemo4 {

private static int x = 0,y = 0;
private static int a = 0,b = 0;
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
    for (;;){
        i++;
        x = 0;y = 0;
        a = 0;b = 0;
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                waitTime(10000);
                a = 1;
                x = b;
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("第" + i + "次执行结果(" + x + "," + y + ")");
        if (x == 0 && y == 0){
            System.out.println("在第" + i + "次发生指令重排,(" + x + "," + y + ")");
            break;
        }
    }
}
public static void waitTime(int time){
    long start = System.nanoTime();
    long end;
    do {
        end = System.nanoTime();
    }while (start + time >= end);
}

}

执行结果

第1次执行结果(0,1)
第2次执行结果(1,0)
....
第35012次执行结果(0,1)
第35013次执行结果(0,0)
在第35013次发生指令重排,(0,0)
如何解决上诉问题哪?volatile的另一个作用就是禁止指令重排优化,它的底层是内存屏障,其实就是一个CPU指令,一个标识,告诉CPU和编译器,禁止在这个标识前后的指令执行重排序优化。内存屏障的作用有两个,一个就是上文所讲的保证变量的内存可见性,第二个保证特定操作的执行顺序。

补充
指令重排序:Java语言规范规定JVM线程内部维持顺序化语义,程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以和代码顺序不一致。JVM根据处理器特性,适当的堆机器指令进行重排序,使机器指令更符号CPU的执行特性,最大限度发挥机器性能。

as-if-serial语义:不管怎么重排序,单线程程序的执行结果不能被改变,编译器和处理器都必须遵守这个原则。

happens-before原则:辅助保证程序执行的原子性,可见性和有序性的问题,判断数据是否存在竞争,线程是否安全的依据(JDK5)

1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说, 如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单 的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当 该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能 够看到该变量的最新值。

4. 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的 start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量 的修改对线程B可见

5. 传递性 A先于B ,B先于C 那么A必然先于C

6. 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前 执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法 成功返回后,线程B对共享变量的修改将对线程A可见。

7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中 断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

原文地址https://www.cnblogs.com/dslx/p/12690366.html

相关文章
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
127 0
|
3月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
225 5
|
3月前
|
监控 搜索推荐 Java
Java 多线程最新实操技术与应用场景全解析:从基础到进阶
本文深入探讨了Java多线程的现代并发编程技术,涵盖Java 8+新特性,如CompletableFuture异步处理、Stream并行流操作,以及Reactive编程中的Reactor框架。通过具体代码示例,讲解了异步任务组合、并行流优化及响应式编程的核心概念(Flux与Mono)。同时对比了同步、CompletableFuture和Reactor三种实现方式的性能,并总结了最佳实践,帮助开发者构建高效、扩展性强的应用。资源地址:[点击下载](https://pan.quark.cn/s/14fcf913bae6)。
240 3
|
4月前
|
算法 Java 调度
Java多线程基础
本文主要讲解多线程相关知识,分为两部分。第一部分涵盖多线程概念(并发与并行、进程与线程)、Java程序运行原理(JVM启动多线程特性)、实现多线程的两种方式(继承Thread类与实现Runnable接口)及其区别。第二部分涉及线程同步(同步锁的应用场景与代码示例)及线程间通信(wait()与notify()方法的使用)。通过多个Demo代码实例,深入浅出地解析多线程的核心知识点,帮助读者掌握其实现与应用技巧。
|
4月前
|
Java
java 多线程异常处理
本文介绍了Java中ThreadGroup的异常处理机制,重点讲解UncaughtExceptionHandler的使用。通过示例代码展示了当线程的run()方法抛出未捕获异常时,JVM如何依次查找并调用线程的异常处理器、线程组的uncaughtException方法或默认异常处理器。文章还提供了具体代码和输出结果,帮助理解不同处理器的优先级与执行逻辑。
121 1
|
2月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
7月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
213 20
|
7月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
9月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
153 1
|
11月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
135 1
C++ 多线程之初识多线程