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

news2024/9/22 10:45:24

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.coloradmin.cn/o/1335575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

国外加固Appdome环境检测与绕过

文章目录 前言第一部分&#xff1a;定位检测逻辑的通用思路1. 通过linux“一切皆文件”思路定位2. 分析现有检测软件猜测可能检测点3. 通过正向开发思路定位4. 通过activity及弹窗定位 第二部分&#xff1a;检测结果展示整体流程1. Jni反射调用doDispath完成广播发送2. NativeB…

Redis案例实战之Bitmap、Hyperloglog、GEO

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…

数据安全技术措施

目录 数据安全技术的控制点 数据完整性 数据保密性 数据备份与恢复 数据安全技术的控制点 数据完整性 数据保密性 数据备份与恢复 ~over~

开发利器——C语言必备实用第三方库

​ 对于广大C语言开发者来说&#xff0c;缺乏类似C STL和Boost的库会让开发受制于基础库的匮乏&#xff0c;也因此导致了开发效率的骤降。这也使得例如libevent这类事件库&#xff08;基础组件库&#xff09;一时间大红大紫。 今天&#xff0c;码哥给大家带来一款基础库&#…

Mybatis如何兼容各类日志?

文章目录 适配器模式日志模块代理模式1、静态代理模式2、JDK动态代理 JDBC Logger总结 Apache Commons Logging、Log4j、Log4j2、java.util.logging 等是 Java 开发中常用的几款日志框架&#xff0c;这些日志框架来源于不同的开源组织&#xff0c;给用户暴露的接口也有很多不同…

Ascon加解密算法分析

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

04|提示工程(上):用少样本FewShotTemplate和ExampleSelector创建应景文案

04&#xff5c;提示工程&#xff08;上&#xff09;&#xff1a;用少样本FewShotTemplate和ExampleSelector创建应景文案 当你用 print 语句打印出最终传递给大模型的提示时&#xff0c;一切就变得非常明了。 您是一位专业的鲜花店文案撰写员。 对于售价为 50 元的 玫瑰 &…

c++使用强制转换类型

对于c中的强制转换&#xff0c;这里主要是讲解的是父类与子类之间的类型强制转换。对于以下的代码中&#xff0c;主要是父类Monkey和子类Man之间的成员函数的调用。 // 这是父类 class Monkey { public&#xff1a; // 定义一个纯虚函数 virtual void printFunc() { qDeb…

udp多播/组播那些事

多播与组播 多播&#xff08;multicast&#xff09;和组播&#xff08;groupcast&#xff09;是相同的概念&#xff0c;用于描述在网络中一对多的通信方式。在网络通信中&#xff0c;单播&#xff08;unicast&#xff09;是一对一的通信方式&#xff0c;广播&#xff08;broad…

智能优化算法应用:基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码

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

算法学习系列(十一):KMP算法

目录 引言一、算法概念二、题目描述三、思路讲解三、代码实现四、测试 引言 这个KMP算法就是怎么说呢&#xff0c;就是不管算法竞赛还是找工作笔试面试&#xff0c;都是非常爱问爱考的&#xff0c;其实也是因为这个算法比较难懂&#xff0c;其实就是很难&#xff0c;所以非常个…

不同参数规模大语言模型在不同微调方法下所需要的显存总结

原文来自DataLearnerAI官方网站&#xff1a; 不同参数规模大语言模型在不同微调方法下所需要的显存总结 | 数据学习者官方网站(Datalearner)https://www.datalearner.com/blog/1051703254378255 大模型的微调是当前很多人都在做的事情。微调可以让大语言模型适应特定领域的任…

Vue在页面上添加水印

第一步&#xff1a;在自己的项目里创建一个js文件&#xff1b;如图所示我在在watermark文件中创建了一个名为waterMark.js文件。 waterMark.js /** 水印添加方法 */ let setWatermark (str1, str2) > {let id 1.23452384164.123412415if (document.getElementById(id) …

数据库01_增删改查

1、什么是数据&#xff1f;什么是数据库&#xff1f; 数据&#xff1a;描述事物的符号记录称为数据。数据是数据库中存储的基本对象。数据库&#xff1a;存放数据的仓库&#xff0c;数据库中可以保存文本型数据、二进制数据、多媒体数据等数据 2、数据库的发展 第一阶段&…

H266/VVC帧内预测编码技术概述

预测编码技术 预测编码&#xff08;Prediction Coding&#xff09;是指利用已编码的一个或多个样本值&#xff0c;根据某种模型或方法&#xff0c;对当前的样本值进行预测&#xff0c;并对样本真实值和预测值之间的差值进行编码。 视频中的每个像素看成一个信源符号&#xff…

MFC读取文件数据,添加信息到列表并保存到文件

打开并读取文件信息 添加&#xff1a; BOOL infoDlg::OnInitDialog() {CDialogEx::OnInitDialog();// TODO: 在此添加额外的初始化AfxMessageBox("欢迎查看学生信息");SetList();return TRUE; // return TRUE unless you set the focus to a control// 异常: OCX 属…

Node 源项目定制化、打包并使用全过程讲解

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是全栈工…

分布式面试题-理论部分(十二道)

文章目录 分布式面试题&#xff08;十二道&#xff09;分布式理论1. 说说CAP原理2. 为什么CAP不可兼得呢&#xff1f;3. CAP对应的模型和应用&#xff1f;4. BASE理论了解吗&#xff1f; 分布式锁5. 有哪些分布式锁的实现方案呢&#xff1f;**5.1 MySQL分布式锁如何实现呢&…

[2023-年度总结]凡是过往,皆为序章

原创/朱季谦 2023年12月初&#xff0c;傍晚&#xff0c;在深圳的小南山看了一场落日。 那晚我们坐在山顶的草地上&#xff0c;拍下了这张照片——仿佛在秋天的枝头上&#xff0c;结出一颗红透的夕阳。 这一天很快就会随着夜幕的降临&#xff0c;化作记忆的碎片&#xff0c;然…

PoE交换机传输距离是多少?100米?250米?

你们好&#xff0c;我的网工朋友。 今天和你聊聊PoE交换机&#xff0c;之前有系统地给你讲解过一篇&#xff0c;可以先回顾一下哈&#xff1a;《啥样的交换机才叫高级交换机&#xff1f;这张图告诉你》 为什么都说PoE交换机好&#xff1f;它最显著的特点就是&#xff1a; 可…