共享模型之管程(一)

news2024/10/5 20:26:24

1.共享带来的问题

1.1.线程安全问题

例如:

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?

@Slf4j
public class TestThread {
    //静态共享变量
    static int counter = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        log.info("{}", counter);
    }
}

在这里插入图片描述
可以多运行几次,每次运行结果都不一样,偶尔会出现结果为"0"的正常现象;

1.2.问题分析

以上的结果可能是正数、负数、零.为什么呢?因为Java中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析;

①.例如对于"i++"而言(i为静态变量),实际会产生如下的JVM字节码指令;

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

②.而对应"i–"也是类似;

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

③.而java的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换;
在这里插入图片描述

  • 如果是单线程以上8行代码是顺序执行(不会交错)没有问题;
    在这里插入图片描述
  • 但多线程下这8行代码可能交错运行;
  • 出现负数的情况:
    在这里插入图片描述
  • 出现正数的情况:
    在这里插入图片描述

1.3.临界区(Critical Section)

1>.一个程序运行多个线程本身是没有问题的;

2>.问题出在多个线程访问共享资源;

①.多个线程读共享资源其实也没有问题;
②.但是在多个线程对共享资源读写操作时发生指令交错,就会出现问题;

3>.一段代码块内如果存在对共享资源的多线程读写操作,那么就称这段代码块为临界区;

public class TestThread2 {

    static int counter = 0;

    static void increment()
    // 临界区
    {
        //操作共享资源(读和写)
        counter++;
    }

    static void decrement()
    // 临界区
    {
        //操作共享资源(读和写)
        counter--;
    }
}

1.4.竞态条件(Race Condition)

多个线程在临界区内执行(即多个线程执行了临界区代码),由于代码的执行序列不同(字节码指令交错执行)而导致结果无法预测,这样的情况就称之为发生了竞态条件;

2.Synchronized解决方案

1>.为了避免临界区的竞态条件发生,有多种手段可以达到目的:

①.阻塞式的解决方案: synchronized,Lock;
②.非阻塞式的解决方案: 原子变量;

2.1.Synchronized简介

1>.Synchronized,即俗称的"对象锁",它采用互斥的方式同一时刻至多只有一个线程能持有"对象锁"(独占锁),其它线程再想获取这个"对象锁"时就会被阻塞住.这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换;

注意: 虽然Java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

①.互斥是保证临界区的竞态条件发生时,同一时刻只能有一个线程执行临界区代码;
②.同步是由于线程执行的先后顺序不同,需要一个线程等待其它线程运行到某个点(才能继续运行);

2.2.Synchronized基本使用

2.2.1.语法

synchronized(对象) //线程1(running),线程2(blocked)
{
   临界区(当临界区代码执行完毕,释放对象锁,唤醒处于阻塞状态中的线程,线程才有机会获取到对象锁)
}

2.2.2.案例

@Slf4j
public class TestThread3 {

    static int counter = 0;

    //对象锁,必须要是(全局)唯一的,最好是不可变的!!!
    static final Object room = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    //临界区
                    counter++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (room) {
                    //临界区
                    counter--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.info("{}", counter);
    }
}

在这里插入图片描述
无论执行多少次,共享变量的值最终都是"0"!!!

分析:

在这里插入图片描述
可以做这样的类比
①.synchronized(对象)中的对象,可以想象为一个房间(room),有唯一入口(门),房间每次只能进入一人,线程t1,t2想象成两个人;

②.当线程t1执行到synchronized(room)时就好比t1进入了这个房间,并锁住了门拿走了钥匙,在门内执行"count++"代码;

③.这时候如果t2也运行到了synchronized(room),它发现门被锁住了,只能在门外等待,发生了上下文切换,线程被阻塞住了;

④.这中间即使线程t1的cpu时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,线程t1仍拿着钥匙,线程t2还在阻塞状态进不来,只有下次轮到t1自己再次获得时间片时才能开门进入;

⑤.当线程t1执行完synchronized{}块内的代码,这时候才会从(room)房间出来并解开门上的锁,然后唤醒线程t2,把钥匙给它.线程t2这时才可以进入(room)房间,锁住了门拿上钥匙,执行它的"count–"代码;
当拥有对象锁的线程执行完synchronized{}代码代码块中的代码后,释放对象锁,然后唤醒所有的处于阻塞状态的线程,最后多个线程竞争CPU时间片,然后某个分配到CPU时间片的线程获取到对象锁,执行synchronized{}代码块中的代码;

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

思考:

①.synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断;


②.为了加深理解,请思考下面的问题:

  • 如果把synchronized(obj)放在for循环的外面,如何理解?-- 原子性
  • 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎样运作?-- 锁对象
  • 如果t1 synchronized(obj),而t2没有加会怎么样?如何理解?-- 锁对象

2.2.3.锁对象面向对象改造

1>.把需要保护的共享变量放入一个类(对象)中,在类的内部(操作方法)对共享资源的操作进行保护

@Slf4j
public class TestThread3 {

    public static void main(String[] args) throws InterruptedException {

        Room room = new Room();

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.info("count: {}", room.get());
    }
}


class Room {

    int value = 0;

    public void increment() {
        //对象锁为当前对象本身
        synchronized (this) {
            value++;
        }
    }

    public void decrement() {
        //对象锁为当前对象本身
        synchronized (this) {
            value--;
        }
    }

    public int get() {
        //对象锁被当前对象本身
        synchronized (this) {
            return value;
        }
    }
}

在这里插入图片描述

2.3.成员方法上的Synchronized

①.Synchronized关键字加在成员方法(即非static方法)上,对应的对象锁就是当前类的this对象(即当前对象);

②.Synchronized关键字加在static方法上,对应的对象锁就是当前类的字节码对象(类名.class);

1>.示例1:

class Test{
   public synchronized void test() {
   }
}

//等价于
class Test{
  public void test() {
     synchronized(this) {
       //...
     }
  }
}

2>.示例2

class Test{
   public synchronized static void test() {
   }
}

//等价于
class Test{
 public static void test() {
    synchronized(Test.class) {
       //...
    }
 }
}

2.4.变量的线程安全分析

2.4.1.成员变量和静态变量是否线程安全?

1>.如果它们没有被共享,则线程安全;

2>.如果它们被共享了(在多个方法中被使用),根据它们的状态是否能够改变,又分两种情况:

①.如果只有读操作,则线程安全;
②.如果有读写操作(值会发生改变),则这段代码是临界区,需要考虑线程安全;

2.4.2.局部变量是否线程安全?

1>.局部变量是线程安全的;

2>.局部变量引用的对象则未必安全:

①.如果该对象没有逃离方法的作用范围,它是线程安全的;
②.如果该对象逃离了方法的作用范围,需要考虑线程安全;

2.4.3.局部变量线程安全分析

1>.示例

public static void test1() {
 int i = 10;
 i++;
}

分析:

①.每个线程调用 test1()方法时,在每个线程的栈帧内存中都会创建/生成一份各自的局部变量i,是线程私有的,因此不存在共享;

public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=1, locals=1, args_size=0
  0: bipush 10
   2: istore_0
   3: iinc 0, 1
   6: return
LineNumberTable:
   line 10: 0
   line 11: 3
   line 12: 6
LocalVariableTable:
   Start Length Slot Name Signature
      3    4    0     i      I

在这里插入图片描述
局部变量的引用稍有不同(这里就不分析了)!

2.4.4.成员变量线程安全分析

1>.示例:

class ThreadUnsafe {
    //list对象定义在成员变量位置,会在堆内存中创建实例,只有一份,可以被所有线程共享
    ArrayList<String> list = new ArrayList<>();
    
public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
          // { 临界区, 会产生竞态条件
                method2();
                method3();
// } 临界区
        }
     }

     private void method2() {
         list.add("1");
      }

      private void method3() {
         list.remove(0);
      }
}

public class TestThread{
      static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;

public static void main(String[] args) {
           ThreadUnsafe test = new ThreadUnsafe();
           for (int i = 0; i < THREAD_NUMBER; i++) {
                    new Thread(() -> {
                        test.method1(LOOP_NUMBER);
                    }, "Thread" + i).start();
            }
}
}

执行之后其中一种情况是,如果线程2 还未add,线程1 remove操作就会报错;

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748)

分析:

①.无论哪个线程中的method2(),引用的都是同一个对象中的list成员变量;
②.method3()与method2()分析相同;
在这里插入图片描述

2>.将list修改为局部变量

class ThreadSafe {
     public final void method1(int loopNumber) {
         //list变量定义在方法内部,每个调用该方法的线程都会创建一个(新的)不同的对象实例,该对象实例属于线程私有
         ArrayList<String> list = new ArrayList<>();
         for (int i = 0; i < loopNumber; i++) {
            method2(list);
              method3(list);
         }
      }

    private void method2(ArrayList<String> list) {
 list.add("1");
}

    private void method3(ArrayList<String> list) {
         list.remove(0);
    }
}

再次执行就不会出现上述的问题了;

分析:

①.此时list是局部变量,每个线程调用时会创建其不同实例,没有共享;

②.而method2()的参数是从method1()中传递过来的,与method1()中引用同一个对象;

③.method3()的参数分析与method2()相同;
在这里插入图片描述

3>.方法修饰符带来的思考: 如果把method2()和method3()的方法修改为public会不会带来线程安全问题?

①.情况1: 有其它线程调用method2()和method3();

  • 不会有线程安全问题;

②.情况2: 在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2()或method3()方法,即:

class ThreadSafe {
 public final void method1(int loopNumber) {
    ArrayList<String> list = new ArrayList<>();
    for (int i = 0; i < loopNumber; i++) {
        method2(list);
        method3(list);
   }
 }

 public void method2(ArrayList<String> list) {
    list.add("1");
 }

public void method3(ArrayList<String> list) {
   list.remove(0);
 }
}

class ThreadSafeSubClass extends ThreadSafe{

 @Override
 public void method3(ArrayList<String> list) {
    new Thread(() -> {
       //这里的list对象是之前的线程创建好的,也就是说之前> 线程创建的list对象被多个线程共享了(局部变量的引用暴> > 露给了其他线程),最终会出现线程安全问题;
       list.remove(0);
    }).start();
 }
}

结论:

方法的访问修饰符(private)在一定程度上可以保护方法的线程安全,因为它限制了子类不能够覆盖对应的方法(即方法的行为不被子类影响),因此子类中的同名方法和父类中的并不是同一个;

2.5.常见的线程安全类

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent包下的类

这里说它们是线程安全的是指: 多个线程调用它们同一个实例的某个方法时,是线程安全的.也可以理解为: 它们的每个方法(单独使用)是原子的,但是注意它们多个方法的组合使用可不是原子的!

例如:

Hashtable table = new Hashtable();

new Thread(()->{
 table.put("key", "value1");
}).start();

new Thread(()->{
 table.put("key", "value2");
}).start();

2.6.不可变类的线程安全性

一个类,其内部的属性只能读取,而不能修改,这样的类被称为不可变类;

1>.String、Integer等都是不可变类,因为其内部的状态(/属性)不可以改变,因此它们的方法都是线程安全的;
在这里插入图片描述

2.8.线程安全分析

1>.案例1

public abstract class Test {
 public void bar() {
    // 是否安全?
    // 答案:线程不安全,因为局部变量的引用会通过方法暴露给其他线程
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 局部变量的作用域发生了逃逸  
    foo(sdf);
    }

public abstract foo(SimpleDateFormat sdf);

public static void main(String[] args) {
      new Test().bar();
    }
}

子类方法:

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
 
for (int i = 0; i < 20; i++) {
      new Thread(() -> {
         try {
            sdf.parse(dateStr);
} catch (ParseException e) {
            e.printStackTrace();
         }
      }).start();
   }
}

其中foo()的行为是不确定的,可能导致不安全的发生,因此也被称之为外星方法;

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

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

相关文章

【Axure教程】拖动排序——扣款顺序

随着移动支付的发展&#xff0c;移动支付的途径和方式也越来越多&#xff0c;常见的有钱包余额支付、支付宝支付、微信支付、银行卡支付……随着绑定的账户越来越多&#xff0c;我们需要一个设置扣款顺序的功能页面。 所以今天作者就教大家如果做一个拖动排序的扣款顺序的原型…

机器学习的4种经典模型总结

机器学习&#xff08;Machine Learning&#xff09;是人工智能的一个分支&#xff0c;也是人工智能的一种实现方法。机器学习的核心是“使用算法解析数据&#xff0c;从中学习&#xff0c;然后对新数据做出决定或预测”&#xff0c;机器学习的概念就是通过输入海量训练数据对模…

【财务】FMS财务管理系统---质保金与预付款

在FMS财务管理系统中&#xff0c;如何对质保金和预付款进行管理&#xff0c;笔者做了详细的业务流程拆解。 上一篇主要说了财务应收管理&#xff0c;有一些朋友留言提出了很多建议&#xff0c;在这里必须谢谢。 关于应收分为ToC与ToB两部分&#xff0c;每一部分都与前端业务系…

新一代自动出价范式:在线强化学习SORL框架

丨目录&#xff1a; 摘要 动机&#xff1a;在离线不一致问题 问题建模 方法&#xff1a;SORL框架 实验结果 总结 关于我们 参考文献▐ 摘要近年来&#xff0c;自动出价已成为广告主提升投放效果的重要方式&#xff0c;在真实广告系统&#xff08;RAS&#xff09;中&#xff0c;…

C++ 数学与算法系列之高斯消元法求解线性方程组

1. 前言 什么是消元法&#xff1f; 消元法是指将多个方程式组成的方程组中的若干个变量通过有限次地变换&#xff0c;消去方程式中的变量&#xff0c;通过简化方程式&#xff0c;从而获取结果的一种解题方法。 消元法主要有代入消元法、加减消元法、整体消元法、换元消元法、…

【C/C++ SOCKET编程】实现服务器客户端的简单通信

什么是SOCKET Socket又称"套接字"&#xff0c;应用程序通常通过"套接字"向网络发出请求或者应答网络请求&#xff0c;使主机间或者一台计算机上的进程间可以通讯。 TCP/IP协议 从字面意义上讲&#xff0c;有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议…

Hive环境安装搭建

目录 Hive安装 MySQL安装 配置Hive元数据库到MySQL Hive安装 软件包 0积分免费下载&#xff1a; hive环境安装所需软件包-Hive文档类资源-CSDN下载 将软件包拖进虚拟机中 将jar包解压到目录 给目录文件夹名改为hive 配置环境变量 输入命令&#xff1a; vim /etc/profile …

数据滚动大屏:Stimulsoft Dashboards.WIN 2023.1.2

Stimulsoft Dashboards.WIN 是一组组件&#xff0c;您可以使用这些组件将分析添加到您的应用程序中。WinForms 和 WPF 的仪表板 Stimulsoft Dashboards.WIN 是一个功能齐全的工具&#xff0c;用于在仪表板上转换、分析、分组、过滤、排序和显示数据。它与 .NET Framework 4.5 及…

科创板智能家居第一股,萤石网络昨日上市

2022年12月28日&#xff0c;杭州萤石网络股份有限公司(以下简称“萤石网络”)成功登陆上海证券交易所科创板&#xff0c;证券代码为688475。值得一提的是&#xff0c;萤石网络是登陆科创板的第500家企业。 在上市仪式上&#xff0c;萤石网络董事长、总经理蒋海青表示&#xff0…

医药信息咨询公司排名TOP10是怎么进行收费的?

随着我国医药行业的飞速发展&#xff0c;市场竞争也是越发的激烈&#xff0c;作为产业链中的医药信息咨询公司作用也愈加明显&#xff0c;医药信息咨询公司以提供医药行业的战略咨询、项目尽调、产品立项评估、行业赛道/细分市场机会评估、专利服务、新产品上市服务、新药产品特…

QA | 关于高级硬件在环(HIL)想要了解的十个问题

HiL&#xff08;Hardware-in-the-Loop&#xff09;硬件在环仿真测试系统是采用实时处理器运行仿真模型来模拟受控对象&#xff08;比如&#xff1a;汽车、航空飞机等设备&#xff09;的运行状态&#xff0c;以此判断电控模块的性能。 Q1&#xff1a;什么是HIL&#xff1f; 硬…

分享20个Javascript中的数组方法,收藏

什么是数组&#xff1f;与其他编程语言中的数组一样&#xff0c;Array对象允许在一个变量名称下存储多个项的集合&#xff0c;并且具有用于执行常见数组操作的成员。 声明数组 我们可以用两种不同的方式声明数组。 使用新阵列 使用new Array&#xff0c;我们可以指定希望存在…

GitHub下载量10W,最新23版Java岗面试攻略,涵盖25个技术栈

年底失业&#xff0c;机会也不多&#xff0c;短时间内想找到合适工作是几乎不可能的。身体好点在家&#xff0c;主要建议大家就做两件事&#xff1a; 第一&#xff1a;整理工作经验&#xff0c;制定新年求职计划。等一些不错的公司放出新的hc&#xff0c;市场情况一回暖&#…

淘宝首页serverless升级后的质量保障方案

本文主要介绍了serverless 架构升级在淘宝首页的应用&#xff0c;新架构对底层所依赖的容器、环境资源等与之前相比差异较大&#xff0c;并且对应的预发、安全生产、生产等环境&#xff0c;与旧架构的完全隔离。背景阿里巴巴集团大淘宝技术全面推进云原生2.0战役——serverless…

wordpress企业主题推荐

WordPress制造企业主题推荐 国潮好物&#xff0c;配国产主题&#xff0c;为中国制造加油、助力&#xff0c;适合生产、加工、制造业官网的WordPress主题。 演示 https://www.jianzhanpress.com/?p4358 wordpress服务行业网站模板推荐 浅绿色小清新wordpress网站模板&#x…

软件测试期末复习(一)试题及答案

一、单项选择题&#xff08;每空 2 分&#xff0c;共 40 分&#xff09; 1&#xff0e;软件测试的目的:&#xff08; c &#xff09; A. 避免软件开发中出现的错误 B. 发现软件开发中出现的错误 C. 尽可能发现并排除软件中潜藏的错误&#xff0c;提高软件的可靠性 D. 修改软件…

Axure绘制流程图

相信大家在日常的工作中经常会绘制流程图&#xff0c;基本常见的绘制流程图的工具有Visio、亿图等。但是如果大家使用的是axure制作的产品prd的话&#xff0c;这些流程图的源文件全部需要进行存储&#xff0c;一旦丢失就需要重新画一遍&#xff0c;这样是很不方便。那么有没有一…

JDBC API详解

文章目录入门案例DriverManagerConnectionStatementResultSetPreparedStatement数据库连接池完整代码入门案例 package jdbc;import java.sql.*;public class connectionTest {public static void main(String[] args) throws ClassNotFoundException, SQLException {//1.注册…

Revit二次开发小技巧(十六)寻找最短路径

最近遇到一个需求&#xff0c;指定两个配电箱&#xff0c;然后找到两个配电箱之间最短的桥架路径。运用了Dijkstra算法去计算最短路径&#xff0c;以配电箱实体、三通、四通为节点&#xff0c;简化中间弯头计算的方式。 背景 选择起点和终点的配电箱&#xff0c;找到最短的桥架…

RingUI + JCEF开发IDEA插件

文章目录RingUI知识储备示例插件实现逻辑开发环境开发流程新建一个IDEA插件项目新建一个前端项目验证前端项目丰富前端项目丰富插件内容RingUI This collection of UI components aims to provide all the necessary building blocks for web-based products built inside JetB…