Java多线程——线程安全、synchronized、volatile关键字以及多线程案例

news2024/11/26 10:16:47

文章目录

  • 前言
  • 一、线程安全—多线程不可避免的风险!
    • 1、线程不安全的示例
    • 2、线程不安全的原因
  • 二、synchronized关键字
    • 1.synchronized的特性
      • 1)互斥
      • 2)刷新内存
      • 3)可重入
    • 2、synchronized使用示例
    • 3、Java标准库中的线程安全类
  • 三、volatile关键字
    • 1、volatile能保证内存可见性
    • 2、volatile不保证原子性
    • 3、synchronized和volatile的对比。
  • 四、wait和notify
    • 1、wait()方法
    • 2、notify()方法
    • 3、notifyAll()方法
    • 4、wait和sleep对比
  • 总结


前言

多线程是java编程中很重要的一个知识点,上一篇文章大致介绍了多线程的基础,本篇紧接上篇,将对线程安全以及解决线程安全的手段进行总结,然后介绍几个多线程案例。


一、线程安全—多线程不可避免的风险!

线程安全的定义:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

1、线程不安全的示例

先观察下面这段代码:

public class Demo23 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
                for (int i = 0; i < 5_0000; i++) {
                    count++;
                }
        });
        Thread t2 = new Thread(()->{
                for (int i = 0; i < 5_0000; i++) {
                    count++;
                }
        });
        t1.start();
        t2.start();
        //打印count值的时候, 需要等待t1 和t2 执行完
        t1.join();
        t2.join();
        System.out.println(count);

    }
}

在这里插入图片描述

如果放在单线程的情况下,那么两个for循环分别对count变量进行50_000次自增,所以结果应该是100_000,但结果并不像我们想的那样,这就是一个线程不安全的案例。运行结果为什么是这样?究其原因,是因为上面这个代码,两个线程在对同一个静态变量count进行了修改,此时这个count是一个多个线程都能访问到的“共享数据”,问题就出在这里!

count++,其实就是count=count+1,这行代码对应了三个指令指令是原子性的,不可分割,要么成功要么失败,不存在中间态。

  • 从内存读取count值 ——LOAD
  • 对数据进行+1操作 ——ADD
  • 将修改后的数据写回主内存 ——SAVE

线程的调度是随机的,因此线程2得到CPU资源时,可能线程1正在对count进行ADD操作但还没有存回主内存,可能发生在任意时刻。这就导致了有可能两个线程都对count进行了+1,但是count只增加了1。导致线程不安全的根本原因,就是线程的随机性!

画张图解释一下:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

通过synchronized加锁,将count++这个操作变成原子性的操作,也就把两个线程对数据的操作变成了串行。如果将锁从run方法开头锁到run方法结束,其实这里的多线程就“名存实亡”了。

2、线程不安全的原因

  • 线程调度的随机性(这是根本原因,但我们解决不了,无可奈何)
  • 多个线程对同一个变量进行修改操作(也不一定能解决,上面的例子可以解决)
  • 原子性:如果对共享变量的操作不是原子性的,就会有安全问题
  • 内存可见性一个线程对数据的操作,可能是其他线程是无感知的,所以对数据进行了更改其他线程没有及时从主内存中读取数据
  • 指令重排序:编译器会对代码进行优化,可能会打乱代码的执行顺序。

既然前两个原因不好解决,后面将要讲到的解决方案,就是针对后面三个的。

二、synchronized关键字

1.synchronized的特性

1)互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待(进入Blocked状态)。

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的。

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队
在这里插入图片描述
理解 “阻塞等待”.
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。

注意

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则(非公平锁)
  • synchronized的底层是使用操作系统的mutex lock实现的。

2)刷新内存

synchronized的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以synchronized也能保证内存可见性,可以对比下面的volatile部分。

3)可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

理解 “把自己锁死”

一个线程没有释放锁, 然后又尝试再次加锁

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁,
在这里插入图片描述
这样的锁称为不可重入锁,Java中的synchronized是可重入锁,不会有死锁的问题。

可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

2、synchronized使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用。

1)直接修饰普通方法: 锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
	public synchronized void methond() {
	}
}

2) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
	public synchronized static void method() {
	}
}

3) 修饰代码块: 明确指定锁哪个对象.

锁当前对象:

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
		}
	}
}

锁类对象:

public class SynchronizedDemo {
	public void method() {
		synchronized (SynchronizedDemo.class) {
		}
	}
}

重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.

3、Java标准库中的线程安全类

Java标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施。

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

在这里插入图片描述

StringBuffer 的核心方法都带有 synchronized

加锁保证安全性,但不加锁普遍效率要高于加锁(StringBuilder效率就高于StringBuffer,但后者是线程安全的)。

三、volatile关键字

1、volatile能保证内存可见性

volatile修饰的变量,能够保证“内存可见性”。
在这里插入图片描述

代码在写入volatile修饰的变量时

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取volatile修饰的变量的时候

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

代码示例:

在这个代码中有两个线程(创建的t1和main线程),t1包含一个循环,这个循环以flag==0为循环条件。在主线程中从键盘读入一个整数,并把这个整数赋值给flag。预期当用户输入非零的值时,t1线程结束。

public class Demo28 {
    static  int flag = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            System.out.println("t1 开始循环...");
            while (flag == 0){
              //什么都不做
            }
            System.out.println("t1 循环结束...");
        });
        t1.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入flag的值:");
        flag = scanner.nextInt();
    }
}

执行效果:

在这里插入图片描述

与预想的不一样,用户输入非0值时,t1线程没有结束。因为这里的t1读取的是自己工作内存中的内容,当主线程中对flag变量进行修改,t1感知不到flag的变化。

给flag加上volatile关键字后:

在这里插入图片描述

2、volatile不保证原子性

volatile和synchronized有着本质的区别,synchronized能够保证原子性,volatile保证的是内存可见性。比如上一篇文章提到的两个线程分别对count进行5w次自增的例子,我们使用volatile修饰count,最终结果仍然不是预期的100000.

在这里插入图片描述

3、synchronized和volatile的对比。

严格来说,synchronized和volatile并没有什么联系,但是因为二者都是Java关键字且用来解决线程安全问题,因此很容易扯在一起。

synchronizedvolatile
通过加锁解锁的方式,将代码绑定在一起,保证原子性不能保证原子性
通过加锁解锁的方式,保证了内存可见性通过强制从主内存读取数据,保证了内存可见性
对指令重排序有一定的约束禁止指令重排序

既然synchronized在大多数情况都可以保证线程安全,是不是无脑使用synchronized就行呢?当然不是。

synchronized是要付出一定代价的,synchronized是通过加锁,解锁的方式来保证的,所以,其他线程抢不到锁的时候,线程会阻塞线程就会放弃CPU,放弃之后,什么时候才能重新被调用,是不确定的。使用了synchronized,就代表着,你在一定程度上,放弃了高性能多线程是为了提高效率,使用synchronized又会放弃一定的效率来保证安全,volatile虽然也会对性能产生一定影响,但不会造成线程阻塞,影响也没有synchronized大。因此有些情况下,使用volatile更合适。

四、wait和notify

1、wait()方法

wait做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常。

代码示例:

public static void main(String[] args) throws InterruptedException {
	Object object = new Object();
	synchronized (object) {
		System.out.println("等待中");
		object.wait();
		System.out.println("等待结束");
	}
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。

2、notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

代码示例: 使用notify()方法唤醒线程

  • 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
  • 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.
static class WaitTask implements Runnable {
	private Object locker;
	public WaitTask(Object locker) {
		this.locker = locker;
		}
	@Override
	public void run() {
		synchronized (locker) {
			while (true) {
				try {
					System.out.println("wait 开始");
					locker.wait();
					System.out.println("wait 结束");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
--------------------------------------------------------------
static class NotifyTask implements Runnable {
	private Object locker;
	public NotifyTask(Object locker) {
		this.locker = locker;
	}
	@Override
	public void run() {
		synchronized (locker) {
			System.out.println("notify 开始");
			locker.notify();
			System.out.println("notify 结束");
		}
	}
}
public static void main(String[] args) throws InterruptedException {
		Object locker = new Object();
		Thread t1 = new Thread(new WaitTask(locker));
		Thread t2 = new Thread(new NotifyTask(locker));
		t1.start();
		Thread.sleep(1000);
		t2.start();
	}

3、notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

值得注意的是:虽然是同时唤醒多个线程,但是这些线程还是需要重新竞争锁,并不是同时执行,仍然是有先后的执行。

理解 notify 和 notifyAll

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着
在这里插入图片描述
notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
在这里插入图片描述

4、wait和sleep对比

其实理论上wait和sleep是完全没有可比性的,因为一个是用于线程之间通信的,一个是让线程阻塞一段时间。

唯一的相同点是都可以让线程放弃执行一段时间。

区别:

  • 进入的阻塞状态不同

sleep()→TIMED_WAITTING
wait()→WAITTING
wait(long time)→TIMED_WAITTING

  • 使用方面:wait需要搭配synchronized一起使用,sleep不需要

  • 类的方面:wait是Object的方法,sleep是Thread的静态方法

总结

保证线程安全的思路:

  1. 使用没有共享资源的模型

  2. 使用共享资源只读不写的模型

1)不需要写共享资源的模型
2)使用不可变对象

  1. 直面线程安全(重点)

1)保证原子性
2)保证内存可见性
3)保证指令顺序性

本篇文章,着重讲解了多线程下的安全问题,以及详细介绍了解决线程安全问题的方法和思路,对synchronized关键字和volatile关键字以及wait、notify方法做了介绍。至此,多线程的初阶知识已经大概学完了,下周开始进阶!!!

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

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

相关文章

【巨人的肩膀】JAVA面试总结(四)

&#x1f4aa;、JVM 目录&#x1f4aa;、JVM1、说一下JVM的主要组成部分及其作用2、什么是JVM内存结构&#xff08;谈谈对运行时数据区的理解&#xff09;3、堆和栈的区别是什么4、堆中存什么&#xff1f;栈中存什么&#xff1f;5、为什么不把基本类型放堆中呢&#xff1f;6、为…

理论上BI软件适配任何行业,但为什么有些行业做不了?

BI商业智能是一种通用的数据类技术解决方案。理论上来说&#xff0c;BI商业智能的核心是数据。只要企业有数据累积&#xff0c;就可以在BI软件上展开一系列的数据开发&#xff0c;获取决策所需的数据依据。但在现实中&#xff0c;却会发现有些BI软件对特定的行业束手无策&#…

【站外SEO】如何利用外部链接来提高你的网站排名

随着互联网的快速发展&#xff0c;越来越多的企业开始注重SEO优化&#xff0c;以提升自己的网站排名&#xff0c;增加流量和曝光度。 而站外SEO作为SEO的重要组成部分&#xff0c;对于提升网站排名具有不可忽视的作用。 站外SEO主要是通过外部链接来提高网站的排名。而GPB外链…

4EVERLAND 的 IPFS Pinning 服务:4EVER Pin

我们很高兴地宣布 4EVERLAND Storage 的一个令人兴奋的补充&#xff0c;即 4EVER Pin。什么是 4EVER Pin&#xff1f;您可能已经知道星际文件系统或IPFS是一个分布式存储网络&#xff0c;来自世界各地的计算机组成节点共享数据。通常&#xff0c;在IPFS中获取一条数据时&#x…

3/3操作系统作业

目录 1.前趋图和程序执行 &#xff08;1&#xff09;前驱图 &#xff08;2&#xff09;程序的顺序执行 &#xff08;3&#xff09;程序的并发执行 2.进程的描述 &#xff08;1&#xff09;进程的定义与特征 ​编辑​编辑&#xff08;2&#xff09;进程控制块​编辑 &…

ESP32设备驱动-GUVA-S12SD紫外线检测传感器驱动

GUVA-S12SD紫外线检测传感器驱动 文章目录 GUVA-S12SD紫外线检测传感器驱动1、GUVA-S12SD介绍2、硬件准备3、软件准备4、驱动实现1、GUVA-S12SD介绍 GUVA-S12SD 紫外线传感器芯片适用于检测太阳光中的紫外线辐射。 它可用于任何需要监控紫外线量的应用,并且可以简单地连接到任…

DebugView在Vs + Qt 应用程序中的使用

1.准备头文件mylog.h #pragma once #ifndef _MYLOG_H_ #define _MYLOG_H_#include <windows.h> #include <tchar.h>#define DP0(fmt) {TCHAR sOut[256];_stprintf_s(sOut,_T(fmt));OutputDebugString(sOut);} #define DP1(fmt,var) {TCHAR sOut[256];_stprintf_s(…

【Java】CompletableFuture 并发顺序调度

前言 Java CompletableFuture 提供了一种异步编程的方式&#xff0c;可以在一个线程中执行长时间的任务&#xff0c;而不会堵塞主线程。 和Future相比&#xff0c;CompletableFuture不仅实现了Future接口&#xff0c;也实现了 CompletionStage接口。Future接口不用多说&#…

react renderProps学习记录

react renderProps学习记录1.引入2.改一下呢3.再改一下呢4.总结一下如何向组件内部动态传入带内容的结构(标签)?children propsrender props1.引入 上代码&#xff1a; import React, { Component } from react import ./index.css export default class Parent extends Com…

【云原生】K8S调度约束

一、调度约束 Kubernetes 是通过 List-Watch&#xff08;监控&#xff09; 的机制进行每个组件的协作&#xff0c;保持数据同步的&#xff0c;每个组件之间的设计实现了解耦。 用户是通过 kubectl 根据配置文件&#xff0c;向 APIServer 发送命令&#xff0c;在 Node 节点上面…

ANR系列(二)——ANR监听方案之SyncBarrier

前言 在项目中经常遇到了手机假死问题&#xff0c;无规律的偶现问题&#xff0c;大量频繁随机操作后&#xff0c;便会出现假死&#xff0c;整个应用无法操作&#xff0c;不会响应事件&#xff0c;会发生各种奇怪的ANR&#xff0c;且trace不固定。而SyncBarrier是其中的罪魁祸首…

RabbitMq(具体怎么用,看这一篇即可)

RabbitMq汇总1.RabbitMq的传统实现方式2.SpringAMQP简化RabbitMq开发2.1 基本消息队列&#xff08;BasicQueue&#xff09;2.2 工作消息队列&#xff08;WorkQueue&#xff09;2.3 发布订阅 -- 广播&#xff08;Fanout&#xff09;2.4 发布订阅 -- 路由&#xff08;Direct&…

2024级浙江大学MBA提面申请流程参考

近年来浙大MBA项目的招生一直都有提前批面试的环节&#xff0c;而且每年在申请政策方面也会做出一些微调&#xff0c;但大的方面不会做调整&#xff0c;2024年MBA提面申请即将开始&#xff0c;对此杭州达立易考教育结合2023年的情况为大家梳理出来基本的申请流程和批次参考&…

【Spring Boot】Spring Boot以Repository方式整合Redis

1 简介 Redis是高性能的NoSQL数据库&#xff0c;经常作为缓存流行于各大互联网架构中。本文将介绍如何在Springboot中整合Spring Data Redis&#xff0c;使用Repository的方式操作。 代码结构如下&#xff1a; 2 整合过程 2.1 安装Redis数据库 为了节省时间&#xff0c;就直…

为什么很多计算机专业大学生毕业后还会参加培训?

基于IT互联网行业越来越卷的现状&#xff0c;就算是科班出身&#xff0c;很多也是达不到用人单位的要求。面对这样的现实情况&#xff0c;有的同学会选择继续深造&#xff0c;比如考个研&#xff0c;去年考研人数457万人次&#xff0c;可见越来越的同学是倾向考研提升学历来达到…

管道。环境变量和常用命令

文章目录管道与文件重定向的区别举例环境变量使用自己编写的程序像命令符一样常用命令grepagwctree . -acutsortmorehistory管道 管道类似于重定向&#xff0c;但是又不太一样&#xff0c;管道可以连接好几个&#xff0c;把第一个的输出当成第二个的输入&#xff0c;第二个的输…

实践Spring5 响应式编程框架WebFlux

WebFlux 以 Reactor 库为基础, 基于异步和事件驱动&#xff0c;可以让我们在不扩充硬件资源的前提下&#xff0c;提升系统的吞吐量和伸缩性。一、什么是 Spring WebFlux了解 WebFlux ,首先了解下什么是 Reactive Streams。Reactive Streams 是 JVM 中面向流的库标准和规范&…

论文推荐:ScoreGrad,基于能量模型的时间序列预测

能量模型&#xff08;Energy-based model&#xff09;是一种以自监督方式执行的生成式模型&#xff0c;近年来受到了很多关注。本文将介绍ScoreGrad&#xff1a;基于连续能量生成模型的多变量概率时间序列预测。如果你对时间序列预测感兴趣&#xff0c;推荐继续阅读本文。 为什…

Qt实现系统桌面目录下文件搜索的GUI:功能一:文件查找与现实

⭐️我叫恒心&#xff0c;一名喜欢书写博客的研究生在读生。 原创不易~转载麻烦注明出处&#xff0c;并告知作者&#xff0c;谢谢&#xff01;&#xff01;&#xff01; 这是一篇近期会不断更新的博客欧~~~ 有什么问题的小伙伴 欢迎留言提问欧。 功能点一&#xff1a;文件查找与…

《MongoDB入门教程》第27篇 创建索引

本文将会介绍 MongoDB 中的索引概念&#xff0c;以及如何利用 createIndex() 方法创建索引。 索引简介 假设存在一本包含介绍各种电影的图书。 如果想要查找一部名为“Pirates of Silicon Valley”的电影&#xff0c;我们需要翻阅每一页&#xff0c;直到发现该电影的介绍为止…