热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

线程安全问题产生的原理解决线程安全问题_同步代码块同步技术的原理

线程安全产生的原因什么是线程安全在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug如果认为是因为这样的线程调度才导致代码产生了bug

线程安全产生的原因
什么是线程安全
在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的
这里的安全指代的是代码中有没有产生bug,与我们平常认为的安全是两种截然不同的概念,我们所熟知的安全是由黑客造成的,他们会不会侵入你的电脑,攻击你的计算机,这是我门不能够制止的,我们所要做的就是让代码不会产生bug.
使用两个线程,对同一个整型变量进行自增操作,每个线程自增五万次,看最后的结果,代码如下

package Demo01_Sleep;
class Counter{
int count = 0;
public void increase(){
count
++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1
= new Thread(()->{
for (int i = 0; i <50000; i++) {
counter.increase();
}
});
Thread thread2
= new Thread(()->{
for (int i = 0; i <50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.
out.println(counter.count);
}
}

 

 

 

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

 

根据案例简述︰

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作Java引入了线程同步机制。那么怎么去使用呢?有三种方式完成同步操作∶

1.同步代码块。

2.同步方法。

3.锁机制。

 

众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢?

 

一:首先了解jvm内存的运行时数据区

        1.堆区:存储对象实例(和实例变量),数组等

 

        2.java虚拟机栈(方法·栈),存放方法声明,局部变量,对象的引用变量,基本数据类型变量等

 

        3.本地方法栈:存储一些本地方法(native关键字修饰的方法,如hashCode()方法,clone方法,Thread类的star0()方法)

 

        4.方法区:存储类元数据,常量,静态变量等

 

        5.程序计数器:记录程序执行的位置,保证cpu切换上下文时,可以从上一次执行的位置开始执行

 

 二:内存空间的共享情况

        堆区与方法区都是线程共享的,而栈区如方法栈则是线程私有的

 

三:一个线程的大致组成结构

        1.每一个线程都有自己的线程栈,因此线程与线程之间是相互独立的

 

        2.一个线程栈里面有自己的程序计数器,方法栈等

 

        3.方法栈里面又是通过栈帧的形式存储局部变量表,操作数栈,方法出口等

 

        4.方法中的局部变量则是存储在局部变量表中,数据操作则是在操作数栈中进行

 

四:多线程引起线程安全原因(实质是造成了读写不一致)

        1.当多个线程操作共享空间中的变量时,就有可能造成线程安全问题(如一个线程更新变量之前,另一个线程读到了旧值并已经更新了,导致该线程再去更新时,更新的值相对来说就不正确了)

 

        2.结合内存空间的共享性,也就是说,当多个线程同时操作堆区中对象的成员变量,或者方法区中的静态变量时,就会造成线程安全问题

 

五:深入理解为什么线程之间会造成读写不一致

        首先线程并发导致安全问题的根本原因主要有3个

 

        1.原子性:线程切换会带来原子性问题,使用锁即可解决。java中只有简单的赋值操作,如i = 100是原子性操作,但是i = j则不是

 

        2.可见性:由于cpu高速缓存的存在,可能会导致线程对一个变量修改没有及时被其他线程所看见,使用volatile关键字即可解决

 

        3.有序性:jvm会对代码进行优化,从而会把代码进行重排序,使用volatile关键字可以禁止重排序

 

        注意:volatile只能保证可见性与有序性,不能保证原子性

 

       那么volatile是如何保证可见性与有序性的呢?

 

        首先说明为什么线程并发会导致可见性问题,以及可见性带来的影响

 

        java内存模型分为线程的工作内存(可以理解为线程栈)与主内存(可以理解为堆以及方法区)。主内存则会存放着一些共享变量;工作内存则是每一个线程独有的。当要操作主内存的变量时,线程会先从主内存中复制一份缓存到自己的工作内存,然后在自己的工作内存对值进行修改,之后再把值更新到主缓存中。因此当有一些线程事先缓存了变量或者线程修改的变量没有及时更新到主内存中,就会导致线程安全问题

 

        volatile则是可以保证线程修改变量后,马上更新到主内存,而且其他线程中即使缓存了该变量,也强制必须从主内存中获取值,从而解决了共享变量的可见性问题

 

        那为什么不能保证原子性呢?

 

        举个例子,比如 volatile i = 100,i++;首先一个线程读取到i = 100,然后阻塞了,另外一个线程进来,读取到 i = 100,再自增并且赋值,刷新主内存i = 101,此时其他线程对i的修改肯定是可见的,但是上一个阻塞的线程已经读到了 i = 100了,然后再++,依然是101,那么也就是说,volatile并没有保证原子性(i++有三个操作,读取,自增,赋值,java中只有简单的赋值才是原子性操作)  

 

五·:解决线程安全问题的思路(同时满足原子性,可见性与有序性)

        1.避免线程修改共享空间中变量的值

 

        2.使用无状态对象,即不共享状态(数据)给多个线程

 

        3.使用不可变对象,不可修改,就不会存在读写不一致的问题

 

        4.使用线程特有对象,如TheadLocal

 

        5.装饰者模式,即使用原子类,原子操作

 

        6.使用锁,保证线程同步,如Syconized,RetranceLock等

————————————————

版权声明:本文为CSDN博主「_小白不黑」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/m0_57713282/article/details/120573047

 

package Demo01_Sleep;
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station
= new BuyTicket();
new Thread(station,"小李").start();
new Thread(station,"王五").start();
new Thread(station,"李四").start();
}
}
class BuyTicket implements Runnable{
//
private int ticketNums = 10;
boolean floag
= true;
@Override
public void run() {
//买票
while (floag){
try {
buy();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//synchronized 同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
if(this.ticketNums <= 0) {
floag
= false;
return;
}
Thread.sleep(
100);
//买票
System.out.println(Thread.currentThread().getName() +"拿到了第"+ this.ticketNums--);
}
}

 

 

 

 

 

 

 

package Demo01_Sleep;
class Counter{
int count = 0;
public void increase(){
count++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i <50000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i <50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}

推荐阅读
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 检查在所有可能的“?”替换中,给定的二进制字符串中是否出现子字符串“10”带 1 或 0 ... [详细]
  • 属性类 `Properties` 是 `Hashtable` 类的子类,用于存储键值对形式的数据。该类在 Java 中广泛应用于配置文件的读取与写入,支持字符串类型的键和值。通过 `Properties` 类,开发者可以方便地进行配置信息的管理,确保应用程序的灵活性和可维护性。此外,`Properties` 类还提供了加载和保存属性文件的方法,使其在实际开发中具有较高的实用价值。 ... [详细]
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • 本文详细介绍了 Java 中遍历 Map 对象的几种常见方法及其应用场景。首先,通过 `entrySet` 方法结合增强型 for 循环进行遍历是最常用的方式,适用于需要同时访问键和值的场景。此外,还探讨了使用 `keySet` 和 `values` 方法分别遍历键和值的技巧,以及使用迭代器(Iterator)进行更灵活的遍历操作。每种方法都附有示例代码和具体的应用实例,帮助开发者更好地理解和选择合适的遍历策略。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 【刷题篇】Java 不用Math.sqrt() 如何求一个数的平方根
    题目:在不用Math.sqrt()方法中如何求解一个大于1的数的平方根题解一、牛顿迭代法计算x2n的解,令f(x)x2-n,相当于求解f( ... [详细]
  • 本教程详细介绍了如何使用 Spring Boot 创建一个简单的 Hello World 应用程序。适合初学者快速上手。 ... [详细]
  • 在 Java 中,`join()` 方法用于使当前线程暂停,直到指定的线程执行完毕后再继续执行。此外,`join(long millis)` 方法允许当前线程在指定的毫秒数后继续执行。 ... [详细]
  • 本题探讨如何编写程序来计算一个数值的整数次方,涉及多种情况的处理。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
author-avatar
手浪用户2602928705
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有