Java 基础学习(十七)多线程高级

1 多线程并发安全(续)

1.1 synchronized方法

1.1.1 synchronized方法

与同步代码块不同,同步方法将子线程要访问的代码放到一个方法中,在该方法的名称前面加上关键字synchronized即可,这里默认的锁为this,即当前对象。在使用时,需要确认多线程访问的是同一个实例的同步方法,才能实现同步效果。

同步方法的语法为:

访问修饰符  synchronized 返回类型  方法名(){
}

synchronized也可以用来修饰静态方法,即静态同步方法,此时锁定的是类对象。每个类都有唯一的一个类对象,可以通过类名.class获取。静态同步方法的语法为:

访问修饰符 synchronized static 返回类型  方法名(){
}

由于同步方法和静态同步方法均没有在代码中显式指定使用的锁对象,在实际使用中需要特别注意,仅在锁对象相同时,才能实现线程互斥。

1.1.2 【案例】synchronized方法示例

编写代码,测试synchronized方法。代码示意如下:

import java.util.concurrent.TimeUnit;
public class SynchronizedDemo2 {
    public static void main(String[] args) {
        MyRun1 run1 = new MyRun1();
        Thread t1 = new Thread(run1, "t1");
        Thread t2 = new Thread(run1, "t2");
        t1.start();
        t2.start();
    }
}
class MyRun1 implements Runnable {
    int num = 0;
    // 同步方法,本案例中的锁对象为main方法中的run1
    public synchronized void printNum() {
        // 在同步代码块中增加一次确认
        if (num > 10){
            return;
        }
        String name = Thread.currentThread().getName();
        System.out.println(name + ": " + num);
        num+=1;
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public void run() {
        while (num<11){
            printNum();
        }
    }
} 

1.1.3 synchronized实现原理

synchronized是通过对象的锁(也称为监视器monitor)来实现的。在Java中,任何一个对象都有一个Monitor与之关联,并提供了获取一个对象的监视器和释放一个对象的监视器的方法。

1、当一个线程想要进入synchronized代码块时,会先申请持有目标对象的锁;

2、如果该线程申请成功,则进入synchronized代码块并执行其中的内容;

3、此时如果其他线程想要进入synchronized代码块,会因无法持有目标对象的锁而进入阻塞状态;

4、当第一个线程执行完synchronized代码块中的内容时,会退出synchronized代码块并释放目标对象的锁;

5、之前申请该对象的锁的所有线程会争抢该锁,得到锁的线程结束阻塞进入synchronized代码块,其他线程继续保持阻塞状态。

整个过程如下图所示:

1.2 死锁

1.2.1 什么是死锁

死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),如果无外力作用,那么这些线程都将无法向前推进。线程死锁的示意如下图所示:

1.2.2 产生死锁的原因

死锁主要是由以下4个因素造成:

1、互斥条件:是指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。

2、不可被剥夺条件:是指线程获取到的资源在自己使用完之前不能被其他线程抢占。

3、请求并持有条件:是指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。

4、环路等待条件:是指在发生死锁时,必然存在一个(线程 — 资源)环形链,即线程集合 {T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,依次类推Tn正在等待已被T0占用的资源。环路等待的示意如下图所示:

1.2.3 【案例】死锁示例

编写代码,测试死锁。代码示意如下:

public class DeadLockDemo {
    public static void main(String[] args) {
        DeadDemo td1 = new DeadDemo();
        DeadDemo td2 = new DeadDemo();
        td1.flag = 1;
        td2.flag = 0;
        new Thread(td1,"td1").start();
        new Thread(td2,"td2").start();
    }
}
class DeadDemo implements Runnable {
    public int flag = 1;
    // 静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName+":flag = "+flag);
        if(flag == 1){
            synchronized (o1){
                System.out.println(threadName+":取得o1锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName+":申请o2锁");
                synchronized (o2){
                    System.out.println("1");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                System.out.println(threadName+":取得o2锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName+":申请o1锁");
                synchronized (o1){
                    System.out.println("0");
                }
            }
        }
    }
}

1.3 API的线程安全

1.3.1 API的线程安全概述

Java API的线程安全问题指的是在多线程环境下,使用Java标准库(Java API)中的一些类和方法可能会出现并发问题,导致程序运行出现不确定的结果或者抛出异常。

这并不是Java的设计问题,而是出于对效率和安全的考虑,Java提供了两类API:非线程安全API和线程安全API。

非线程安全API和线程安全API在功能和使用上往往非常相似,主要的区别是内部是否添加了保证线程安全的机制。开发者需要熟知API的线程安全性,并能够根据实际的场景进行正确的选择。

1.3.2 【案例】StringBuilder线程安全问题示例

StringBuilder是非线程安全的,以下通过一个案例演示它可能出现的问题。

import java.util.ArrayList;
import java.util.List;
public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder builder = new StringBuilder();
        int numThreads = 3;
        Runnable appendTask = () -> {
            for (int i = 0; i < 10000; i++) {
                builder.append("A");
            }
        };
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < numThreads; i++) {
            Thread thread = new Thread(appendTask);
            threads.add(thread);
            thread.start();
        }
        // 等待所有线程完成
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Final length of StringBuilder: " + builder.length());
    }
}

1.3.3 StringBuffer

StringBuffer是Java中用于处理可变字符串的类,与StringBuilder非常相似。它们都继承自 AbstractStringBuilder,支持修改字符串内容,可以进行增删改查操作。

与StringBuilder不同的是,StringBuffer 是线程安全的。

StringBuffer 的关键方法都使用了 synchronized 关键字进行同步控制,确保在多线程环境下多个线程可以同时访问和修改同一个 StringBuffer 对象,而不会出现数据不一致或并发问题。

由于 StringBuffer 需要进行同步控制,使得它在性能上较 StringBuilder 稍有劣势。如果不需要考虑线程安全问题,推荐使用 StringBuilder,因为它没有线程安全的开销,性能更高。

如果需要保证多个线程安全地访问和修改同一个字符串缓冲区,应该使用 StringBuffer。

1.3.4 【案例】StringBuffer示例

import java.util.ArrayList;
import java.util.List;
public class StringBufferDemo {
    public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        int numThreads = 3;
        Runnable appendTask = () -> {
            for (int i = 0; i < 10000; i++) {
                buffer.append("A");
            }
        };
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < numThreads; i++) {
            Thread thread = new Thread(appendTask);
            threads.add(thread);
            thread.start();
        }
        // 等待所有线程完成
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Final length of StringBuilder: " + buffer.length());
    }
}

1.3.5 集合的线程安全概述

在 Java 中,集合类主要分为两类:线程安全的集合和非线程安全的集合。线程安全的集合是指在多线程环境下,多个线程可以同时访问和修改集合,而不会出现数据不一致或并发问题。非线程安全的集合是指在多线程环境下,多个线程同时修改集合可能会导致数据不一致或其他并发问题。

Java 中许多集合类都是非线程安全的,例如:ArrayList、LinkedList、HashSet、HashMap。

相应的,Java在java.util.concurrent 包下提供了一些专门设计用于多线程环境的线程安全集合,例如:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet等。

这部分内容将在后续的课程中展开介绍。

2 内存模型与并发问题

2.1 Java内存模型基础

2.1.1 Java内存模型的抽象结构

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量(Local Variables),方法定义参数(Formal Method Parameters)和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java线程之间的通信由Java内存模型(Java Memory Model, JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程已读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

假设有线程A和B:

线程A与线程B之间想要通信,必须经历下面2个步骤:

1、线程A把本地内存A中更新过的共享变量刷新到主内存中

2、线程B到内存中去读取线程A之前已更新过的共享变量

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java开发者提供内存可见性保证。

2.1.2 共享内存的并发问题

线程本地内存和主内存的设计可能带来并发问题。默认情况下,一个线程对主内存中数据的更新并不会通知另一个线程,另一个线程可能基于本地内存中之前缓存的数据进行操作,造成并发问题。如下图所示:

2.1.3【案例】共享内存并发问题示例

编写代码,测试共享内存的并发问题。代码示意如下:

public class SharedDataDemo1 {
    public static void main(String[] args) {
        // 创建保存共享数据的对象
        SharedData sharedData = new SharedData();
        // 启动一个线程修改sharedData对象的变量flag,将变量flag改为false
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                System.out.println("线程" + name + "正在执行");
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                sharedData.setFlagFalse();
                System.out.println("线程" + name + "更新后,flag的值为"+
                        sharedData.flag);
            }
        }
        ).start();
        // 确定主线程的副本是否会自动更新
        while (sharedData.flag) {
            // 当上面的线程将变量flag改为false后
            // 如果没有自动更新,就会一直在循环中执行
        }
        System.out.println("主线程运行终止");
    }
}
class SharedData {
    boolean flag = true;
    // 将变量flag的值改为false
    public void setFlagFalse(){
        this.flag = false;
    }
}

2.2 volatile关键字

2.2.1 volatile关键字概述

volatile关键字可以用来修饰字段(成员变量),即规定线程对该变量的访问均需要从共享内存中获取,对该变量的修改也必须同步刷新到共享内存中,以保证资源的可见性。

针对上一个案例的改变,如下图所示:

2.2.2【案例】volatile示例

编写代码,使用volatile关键字解决共享内存的并发问题。代码示意如下:

public class SharedDataDemo2 {
    public static void main(String[] args) {
        // 创建保存共享数据的对象
        SharedData2 sharedData = new SharedData2();
        // 启动一个线程修改sharedData对象的变量flag,将变量flag改为false
        new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                System.out.println("线程" + name + "正在执行");
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                sharedData.setFlagFalse();
                System.out.println("线程" + name + "更新后,flag的值为"+
                        sharedData.flag);
            }
        }
        ).start();
        // 确定主线程的副本是否会自动更新
        while (sharedData.flag) {
            // 当上面的线程将变量flag改为false后
            // 如果没有自动更新,就会一直在循环中执行
        }
        System.out.println("主线程运行终止");
    }
}
class SharedData2 {
    // 使用关键字volatile修饰变量flag
    volatile boolean flag = true;
    // 将变量flag的值改为false
    public void setFlagFalse(){
        this.flag = false;
    }
}

3 多线程协作

3.1 多线程协作概述

3.1.1 狭义的线程同步

广义的线程同步被定义为一种机制,用于确保两个或多个并发的线程不会同时进入临界区。从该定义来看,线程同步和线程互斥是相同的。

狭义的线程同步在线程互斥的基础上增加了对多个线程执行顺序的要求,即两个或多个并发的线程应按照特定的顺序进入临界区。

可以简单地总结为,狭义的线程同步是一种强调执行顺序的线程互斥,也称为多线程协作。

例如,在多个线程输出1-10案例中,仅要求同一时间仅能有一个线程执行printNum方法,即线程互斥,如果在案例中要求两个线程必须交替打印数字,不能出现一个线程连续打印连个数字的情况,就属于多线程协作的范畴。

3.1.2 为什么需要多线程协作

在现实生产中,我们经常会遇到多个人分工协作的场景,其中很多场景是强调工作的顺序的。例如,A同学负责编写代码,B同学负责测试代码,C同学负责修改代码中的问题。

在一个程序的运行过程中也会有很多相似的场景,例如在下载软件中,A、B、C三个线程负责分别下载某一段数据,D线程负责周期性的统计这3个线程的下载情况,显示最新下载进度,E线程负责在所有下载任务完成后关闭计算机。

3.2 线程同步

3.2.1 wait、notify和notifyAll

在线程的协作中,一种常用的方式是wait/notify等待通知方式。等待通知方式就是将处于等待状态的线程由其他线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。

Java提供了如下3个方法来实现线程之间的消息传递:

  • wait():导致当前线程等待,并释放持有的锁;直到其他持有相同锁的线程调用notify()方法或notifyAll()方法来唤醒该线程
  • notify():随机唤醒一个在此锁上等待的线程
  • notifyAll():唤醒所有在此锁上等待的线程

上述3个方法必须在同步代码块或同步方法中调用,否则会出现IllegalMonitorStateException异常。

等待通知方式主要应用于如下场景:当一个线程获取锁后,发现自己不满足某些条件,不能执行锁住部分的代码,此时需要进入等待列表,直到满足条件时才会重新竞争线程。

3.2.2 【案例】两个线程交替打印数字示例

编写代码,用两个线程交替打印数字:

 代码示意如下:

public class WaitNotifyDemo {
    public static void main(String[] args) {
        Number number1 = new Number();
        Thread t1 = new Thread(number1);
        Thread t2 = new Thread(number1);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}
class Number implements Runnable {
    private int number = 1;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                // 唤醒等待池中的一个线程,该线程进入锁池,等待当前线程释放锁
                this. notify();
                String name = Thread.currentThread().getName();
                // 当前线程执行打印操作
                if (number <= 10) {
                    System.out.println( name + "打印" + number);
                    number++;
                } else{
                    break;
                }
                try {
                    // 当前线程进入等待池,并释放持有的锁
                    this. wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3.2.3 等待阻塞状态

当一个线程因wait()方法进入阻塞状态时,该线程处于等待阻塞状态。当一个处于等待阻塞的线程被notify()或notifyAll()方法唤醒时,该线程先进入同步阻塞状态,得到锁后进入可运行状态。

线程状态如下图所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/270637.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

短视频矩阵系统的崛起和影响

近年来&#xff0c;短视频矩阵系统已经成为了社交媒体中的一股新势力。这个新兴的社交媒体形式以其独特的魅力和吸引力&#xff0c;迅速吸引了大量的用户。这个系统简单来说就是将海量短视频整合在一个平台上&#xff0c;使用户可以方便地观看和分享好玩有趣的短视频。 短视频…

测试员有必要转测试开发吗?

为什么很多公司都在招测试开发&#xff1f; 质量保证和软件稳定性&#xff1a;测试开发人员在软件开发生命周期中扮演着关键的角色&#xff0c;他们负责编写和执行测试代码&#xff0c;以确保软件的质量和稳定性。他们可以帮助发现和修复潜在的问题和缺陷&#xff0c;提高软件…

短视频矩阵系统源码是如何运作的?

在当今数字化时代&#xff0c;短视频已经成为人们日常生活中的重要娱乐方式。而为了更好地满足用户需求以及提升使用体验&#xff0c;短视频平台需要依靠一个强大而高效的短视频矩阵系统。那么&#xff0c;这个系统又是如何运作的呢&#xff1f; 首先&#xff0c;短视频矩阵系…

[机器人-1]:开源MIT Min cheetah机械狗设计(一):系统方案及硬件解析

目录 一、MIT Min cheetah机械狗系统设计 1.1 指标需求 1.2 系统框图 1&#xff09;腿部硬件构成&#xff1a;机械狗每条腿 (共四条腿&#xff09; 2&#xff09; 数据通信转换板部分 2 STM32 * 2 CAN * (4 * 3)&#xff1a;FOC算法 3&#xff09;UP board 计算机板卡硬件…

【C++】explicit关键字

explicit关键字通常用于声明构造&#xff0c;是为了防止编译器进行隐式类型转换。 当加上explicit关键字后&#xff0c;只能显示的调用构造函数

Android---Kotlin 学习009

继承 在 java 里如果一个类没有被 final 关键字修饰&#xff0c;那么它都是可以被继承的。而在 kotlin 中&#xff0c;类默认都是封闭的&#xff0c;要让某个类开放继承&#xff0c;必须使用 open 关键字修饰它&#xff0c;否则会编译报错。此外在子类中&#xff0c;如果要复写…

09-为Java开疆拓土的ZGC深度剖析

文章目录 引言ZGC出现背景STW带来的问题手机系统(Android) 显示卡顿证券交易系统实时性要求大数据平台(Hadoop集群性能) 垃圾回收器的发展 ZGC介绍ZGC中JVM内存布局和设计为什么这么设计&#xff1f; ZGC支持NUMA&#xff08;了解即可&#xff09; ZGC的核心概念指针着色技术&a…

智能优化算法应用:基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于人工蜂鸟算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.人工蜂鸟算法4.实验参数设定5.算法结果6.…

Redis源码精读:字符串

文章目录 前言代码位置核心类型SDS结构获取sds字符串的元数据的宏获取字符串长度重新设置sds长度创建字符串感悟最后 前言 Redis中实现了sds&#xff08;simple dynamic string&#xff09;这种字符串&#xff0c;它比c语言标准库的char*字符串更加实用 代码位置 src/sdc.h …

Ascon加解密算法分析

参数定义 加密架构图 整个过程是在处理320bits的数据&#xff0c;所以在最开始需要对原始的数据进行一个初始化&#xff0c;获得320bits的数据块&#xff0c; 图里看到的pa和pb都是在做置换&#xff08;对320bits的数据进行一个置换&#xff09; 置换&#xff08;Permutation&…

不可能得到的最短骰子序列

说在前面 &#x1f388;不知道大家对于算法的学习是一个怎样的心态呢&#xff1f;为了面试还是因为兴趣&#xff1f;不管是出于什么原因&#xff0c;算法学习需要持续保持。 题目描述 给你一个长度为 n 的整数数组 rolls 和一个整数 k 。你扔一个 k 面的骰子 n 次&#xff0c;…

Vue 封装echarts饼状图(Pie)组件

目的&#xff1a;减少重复代码&#xff0c;便于维护 效果显示&#xff1a; 组件代码 <template><div class"ldw-data-content-box"><div class"ldw-chilren-box"><div class"title"><div>{{ title }}</div>…

【隐私保护】使用Python从文本中删除个人信息:第一部分

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

Flowable-升级为7.0.0.M2-第一节

目录 升级jdk升级springboot到3.1.3升级数据库连接池druid-spring-boot-3-starter到1.2.20升级mybatis-plus到3.5.3.2升级flowable到7.0.0.M2 最近有些读者一直问flowable如何升级到7.0.0.M2&#xff0c;接下来我就一步步的把flowable升级到7.0.0.M2 升级jdk flowable7.x采用的…

《PySpark大数据分析实战》-19.NumPy介绍ndarray介绍

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…

微信小程序合集更更更之echarts雷达图!

实现效果 写在最后&#x1f352; 更多相关内容&#xff0c;关注&#x1f365;苏苏的bug&#xff0c;&#x1f361;苏苏的github&#xff0c;&#x1f36a;苏苏的码云~

VMware17Pro虚拟机安装Linux CentOS 7.9(龙蜥)教程(超详细)

目录 1. 前言2. 下载所需文件3. 安装VMware3.1 安装3.2 启动并查看版本信息3.3 虚拟机默认位置配置 4. 安装Linux4.1 新建虚拟机4.2 安装操作系统4.2.1 选择 ISO 映像文件4.2.2 开启虚拟机4.2.3 选择语言4.2.4 软件选择4.2.5 禁用KDUMP4.2.6 安装位置配置4.2.7 网络和主机名配置…

OpenAI换血大震动始末:“ChatGPT之父”奥特曼,缘何被“扫地出门”?

近期&#xff0c;AI业界发生了一场“大地震”。作为聊天机器人ChatGPT的开发者&#xff0c;OpenAI无疑是最受关注的人工智能公司之一。就是这家公司的联合创始人、CEO、有“ChatGPT之父”之称的阿尔特曼在“疯狂的5天”里&#xff0c;经历了被闪电免职、加入微软、最终又官复原…

微服务架构<2>

在电商项目中&#xff0c;我们针对一些核心业务&#xff0c;比较复杂的业务需要做一些设计以及优化的过程首先我们针对于订单的模块拆分了2个子模块1.order-curr实时下单业务 2.order-his 做一些历史的订单归档我们的订单业务 >商品添加至购物车 >购物车结算--> 订单…

SpringBoot整合JWT+Spring Security+Redis实现登录拦截(二)权限认证

上篇博文中我们已经实现了登录拦截&#xff0c;接下来我们继续补充代码&#xff0c;实现权限的认证 一、RBAC权限模型 什么事RBAC权限模型&#xff1f; RBAC权限模型&#xff08;Role-Based Access Control&#xff09;即&#xff1a;基于角色的权限访问控制。在RBAC中&#x…
最新文章