我的编程空间,编程开发者的网络收藏夹
学习永远不晚

JAVA多线程线程安全性基础

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

JAVA多线程线程安全性基础

线程安全性

一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能

什么是线程安全的代码

核心:对 共享的 和 可变的 状态的访问进行管理。防止对数据发生不受控的并发访问。

何为对象的状态?

状态是指存储在对象的状态变量(例如实例或静态域)中的数据。还可能包括 其他依赖对象 的域。

eg:某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中。

在这里插入图片描述

总而言之,在对象的状态中包含了任何可能影响其外部可见行为的数据。

何为共享的?

共享的 是指变量可同时被多个线程访问

何为可变的?

可变的 是指变量的值在其生命周期内可以发生变化。试想,如果一个共享变量的值在其生命周期内不会发生变化,那么在多个

线程访问它的时候,就不会出现数据不一致的现象,自然就不存在线程安全性问题了。

什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,达到预期的效果,那么就称这个类是线程安全的。

如下启动10个线程,每个线程对inc执行1000次递增,并添加一个计时线程,预期效果应为10000,而实际输出值为6880,是一个小于10000的值,并未达到预期效果,因此INS类不是线程安全的,整个程序也不是线程安全的。原因是递增操作不是原子操作,并且没有适当的同步机制


package hgh0808;
public class Test {
    public static void main(String[] args){
        for(int i = 0;i < 10;i++){
            Thread th = new Thread(new CThread());
            th.start();
        }
        TimeThread tt = new TimeThread();
        tt.start();
        try{
            Thread.sleep(21000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(INS.inc);
    }
}
---------------------------------------------------------------------
package hgh0808;
import java.util.concurrent.atomic.*;
public class TimeThread extends Thread{
    @Override
    public void run(){
        int count = 1;
        for(int i = 0;i < 20;i++){
            try{
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(count++);
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class CThread implements Runnable{
    @Override
    public void run(){
        for(int j = 0;j < 1000;j++){
            INS.increase();
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class INS{
    public static volatile int inc = 0;
    public static void increase(){
            inc++;
    }
}
=====================================================================

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880

通过synchronized加锁机制,对INS类实现同步,如下得到了正确的运行结果,很容易可以看出,主调代码中并没有任何额外的同步或协同,此时的INS类是线程安全的,整个程序也是线程安全的


package hgh0808;
public class INS{
    public static volatile int inc = 0;
    public static void increase(){
        synchronized (INS.class){
            inc++;
        }
    }
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000

如何编写线程安全的代码
------------------------------------------------------------------------------------------------
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,像上文中进行同步之前的代码
有三种方式可以修复这个问题:
*不在线程之间共享该状态变量
*将状态变量修改为不可变的变量
*在访问状态变量时使用同步
前两种方法是针对 共享 和 不变 这两个属性(见上文)解决问题,在有些情境下会违背程序设计的初衷(比如上文中INS类中的inc变量不可能不变,且在多核处理器的环境下为了提高程序性能,就需要多个线程同时处理,这样变量就必然要被多个线程共享)。
基于此,我们针对第三种方式------ 在访问状态变量时使用同步 展开讨论
在讨论第三种方式之前,我们先介绍几个简单的概念
原子性 :一个操作序列的所有操作要么不间断地全部被执行,要么一个也没有执行
竞态条件 :当某个计算的正确性取决于多个线程的的交替执行时序时,就会发生竞态条件。通俗的说,就是某个程序结果的正确性取决于运气时,就会发生竞态条件。(竞态条件并不总是会产生错误,还需要某种不恰当的执行时序)
常见的竞态条件类型:
*检查–执行(例如延迟初始化)
*读取–修改–写入(例如自增++操作)
针对以上两种常见的竞态条件类型,我们分别给出例子


延迟初始化(检查--执行)
--------------------------------------------------------------------
package hgh0808;
import java.util.ArrayList;
public class Test1 {
    public ArrayList<Ball> list;
    public ArrayList<Ball> getInstance(){
        if(list == null){
            list = new ArrayList<Ball>();
        }
        return list;
    }
}
class Ball{
}

大概逻辑是先判断list是否为空,若为空,创建一个新的ArrayList对象,若不为空,则直接使用已存在的ArrayList对象,这样可以保证在整个项目中list始终指向同一个对象。这在单线程环境中是完全没有问题的,但是如果在多线程环境中,list还未实例化时,A线程和B线程同时执行if语句,A和B线程都会认为list为null,A和B线程都会执行实例化语句,造成混乱。


自增++操作(读取--修改--写入)
------------------------------------------------------------------------
参考上文中为改进之前的代码(对INS类中inc的自增)

以上两个例子告诉我们,必须添加适当的同步策略,保证复合操作的原子性,防止竞态条件的出现

策略一:使用原子变量类,在java.util.concurrent.atomic包中包含了一些原子变量类

在这里插入图片描述


package hgh0808;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

public class INS{
    public static AtomicInteger inc = new AtomicInteger(0);
    public static void increase(){
        inc.incrementAndGet();
    }
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000

值得注意的是,只有一个状态变量时,可以通过原子变量类实现线程安全。但是如果有多个状态变量呢?

设想一个情景

多个线程不断产生1到10000的随机数并且发送到一个计算线程,计算线程每获取一个数字n,就计算sinx在[0,n]上的积分并打印到控制台上,为了提高程序性能,设计一个缓存机制,保存上次的数字n和积分结果(两个状态变量)。如果本次的数字和上次的数字相等,直接打印积分结果,避免重复计算。

看代码:


package hgh0808;
import java.util.concurrent.atomic.AtomicReference;

public class Calculate extends Thread{
    private final AtomicReference<Double> lastNumber  = new AtomicReference<Double>();  //缓存机制,原子变量类
    private final AtomicReference<Double> lastRes = new AtomicReference<Double>();      //缓存机制,原子变量类
    private static final double N = 100000;    //将区间[0,e]分成100000份,方便定积分运算
    public void service() throws Exception{
        getData();
        Thread.sleep(10000);   //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素
        while(true){
            Double e;
                if(!MyQueue.myIsEmpty()){
                     e = MyQueue.myRemove();
                }else{
                    return;
                }
            if(e.equals(lastNumber.get())){
                System.out.println(lastNumber.get()+" "+lastRes.get());
            }else{
                Double temp = integral(e);
                lastNumber.set(e);
                lastRes.set(temp);
                System.out.println(e+" "+temp);
            }
            Thread.sleep(2000);
        }
    }
    public void getData(){   //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素
        Thread1 th1 = new Thread1();
        Thread2 th2 = new Thread2();
        Thread3 th3 = new Thread3();
        Thread4 th4 = new Thread4();
        th1.start();
        th2.start();
        th3.start();
        th4.start();
    }
    public Double integral(double e){    //计算定积分
        double step = (e-0)/N;
        double left = 0,right = step;
        double sum = 0;
        while(right <= e){
            double mid = left+(right-left)/2;
            sum+=Math.sin(mid);
            left+=step;
            right+=step;
        }
        sum*=step;
        return sum;
    }
}
---------------------------------------------------------------------
package hgh0808;
import java.util.LinkedList;
public class MyQueue {      //由于LinkedList是线程不安全的,因此需要将其改写为线程安全类
    private static LinkedList<Double> queue = new LinkedList<>();
    synchronized public static void myAdd(Double e){
        queue.addLast(e);
    }
    synchronized public static void myClear(){
        queue.clear();
    }
    synchronized public static int mySize(){
        return queue.size();
    }
    synchronized public static boolean myIsEmpty(){
        return queue.isEmpty();
    }
    synchronized public static double myRemove(){
        return queue.removeFirst();
    }
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread1 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){     //由于从队列中取元素的速度低于四个线程向队列中加元素的速度,因此队列的长度是趋于扩张的,当达到一定程度时,清空队列
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread2 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread3 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread4 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

只看Calculate线程,不看其他线程和MyQueue中的锁机制,本问题的焦点在于Calculate线程中对多个状态变量的同步策略

存在问题:

尽管对lastNumber和lastRes的set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastRes;如果只修改了其中一个变量,那么在这两次修改操作之间,其它线程将发现不变性条件被破坏了。换句话说,就是没有足够的原子性

**当在不变性条件中涉及多个变量时,各个变量间并不是彼此独立的,而是某个变量的值会对其它变量的值产生约束。因此当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

改进 ================>加锁机制 内置锁 synchronized

之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象

synchronized修饰方法就是横跨整个方法体的同步代码块

非静态方法的锁-----方法调用所在的对象

静态方法的锁-----方法所在类的class对象


public class Calculate extends Thread{
    private final AtomicReference<Double> lastNumber  = new AtomicReference<Double>();  //缓存机制,原子变量类
    private final AtomicReference<Double> lastRes = new AtomicReference<Double>();      //缓存机制,原子变量类
    private static final double N = 100000;    //将区间[0,e]分成100000份,方便定积分运算
    public void service() throws Exception{
        getData();
        Thread.sleep(10000);   //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素
        while(true){
            Double e;
            synchronized (this){    //检查--执行 使用synchronized同步,防止出现竞态条件
                if(!MyQueue.myIsEmpty()){
                     e = MyQueue.myRemove();
                }else{
                    return;
                }
            }
            if(e.equals(lastNumber.get())){
                System.out.println(lastNumber.get()+" "+lastRes.get());
            }else{
                Double temp = integral(e);
                synchronized (this) {     //两个状态变量在同一个原子操作中更新
                    lastNumber.set(e);
                    lastRes.set(temp);
                }
                System.out.println(e+" "+temp);
            }
            Thread.sleep(2000);
        }
    }
    public void getData(){   //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素
        Thread1 th1 = new Thread1();
        Thread2 th2 = new Thread2();
        Thread3 th3 = new Thread3();
        Thread4 th4 = new Thread4();
        th1.start();
        th2.start();
        th3.start();
        th4.start();
    }
    public Double integral(double e){    //计算定积分
        double step = (e-0)/N;
        double left = 0,right = step;
        double sum = 0;
        while(right <= e){
            double mid = left+(right-left)/2;
            sum+=Math.sin(mid);
            left+=step;
            right+=step;
        }
        sum*=step;
        return sum;
    }
}

对于包含多个变量的不变性条件中,其中涉及的所有变量都需要由同一个锁来保护


synchronized (this) {     //两个状态变量在同一个原子操作中更新
                    lastNumber.set(e);
                    lastRes.set(temp);
                }

锁的重入

如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,“重入”意味着获取锁的操作的粒度是‘线程',而不是‘调用'。

重入的一种实现方式 :

为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。


如果内置锁不可重入,那么以下这段代码将发生死锁(每个doSomething方法在执行前都会获取Father上的内置锁)
----------------------------------------------------------------------
public class Father{
  public synchronized void doSomething(){
  }
}

public class Son extends Father{
   @Override
   public synchronized void doSomething(){
       System.out.println("重写");
       super.doSomething();
   }
}

线程安全性与性能和活跃性之间的平衡

活跃性:是否会发生死锁饥饿等现象
性能:线程的并发度
不良并发的应用程序:可同时调用的线程数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,可以平衡这个问题。
缩小作用范围的原则====>当执行时间较长的计算或者可能无法快速完成的操作时,一定不能持有锁!!!

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注编程网的更多内容!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

JAVA多线程线程安全性基础

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java线程安全基础概念解析

Java线程安全初步了解。JAVA线程安全从总体上来说,是指Java对象在多线程运行环境下的一种特性,表现为常规(区别于特殊调用情况)情况下每次调用都能得到正确的逻辑结果。从本质上来说,将对象的方法行为加上了同步控制逻辑,而调用者无须做其他
2023-05-31

网络安全编程:多线程编程基础知识

线程是进程中的一个执行单位(每个进程都必须有一个主线程),一个进程可以有多个线程,而一个线程只存在于一个进程中。在数据关系上,进程与线程是一对多的关系。

Java多线程 - 线程安全和线程同步解决线程安全问题

文章目录 线程安全问题线程同步方式一: 同步代码块方式二: 同步方法方式三: Lock锁 线程安全问题 线程安全问题指的是: 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 举例:
2023-08-20

Java多线程Thread基础学习

每一个正在执行的程序都是一个进程,资源只有一块,所以在同一时间段会有多个程序同时执行,但是在一个时间点上,只能由一个程序执行,多线程是在一个进程的基础之上的进一步划分,需要的朋友可以参考下
2023-05-17

如何在 Java 中保证多线程的线程安全?(java多线程怎么保证线程安全)

在Java开发中,多线程编程是一个常见且重要的技术。然而,多线程同时访问共享资源时可能会导致线程安全问题,如数据不一致、竞态条件等。本文将详细介绍在Java中如何保证多线程的线程安全。一、线程安全的概念线程安全是指在多线
如何在 Java 中保证多线程的线程安全?(java多线程怎么保证线程安全)
Java2024-12-15

python多线程基础

一、python多线程基础    python多线程主要涉及两个类:thread和threading,后者实际是对前者的封装,由于threading提供了更完善的锁机制,运用场景更多,重点学习了这个类的使用。threading.Thread
2023-01-31

java多线程怎么保证线程安全

Java中有多种方式可以保证线程安全,以下是一些常见的方法:1. 使用synchronized关键字:使用synchronized关键字可以将代码块或方法标记为同步的,只有一个线程能够进入同步块或方法执行,其他线程需要等待。这样可以确保同一
2023-09-13

怎样解析Java基础多线程

怎样解析Java基础多线程,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。 多线程是Java学习的非常重要的方面,是每个Java程序员必须掌握的基本技能。一、进程
2023-06-02

Java多线程编程安全如何退出线程

小编给大家分享一下Java多线程编程安全如何退出线程,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!线程停止Thread提供了一个stop()方法,但是stop()
2023-05-30

py基础---多线程、多进程、协程

目录 Python基础__线程、进程、协程 1、什么是线程(thread)? 2、什么是进程(process)? 3、进程和线程的区别 4、GIL
2023-01-31

java中多线程和线程安全是什么

这篇文章给大家分享的是有关java中多线程和线程安全是什么的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。什么是进程?电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。比如下图中
2023-06-25

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录