tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

    • Java中创建线程的几种方式
    • 并发相关概念与体系图
    • 线程状态与操作系统的用户态、内核态
    • 线程中的声明与守护线程_基础
    • 程序中的幽灵错误_基础
      • 无提示的错误
      • 并发下的ArrayList
      • 错误的加锁
    • JDK并发包
    • 线程池相关
    • 并发中的安全集合
    • 生产者和消费者
    • 玩转单例模式
    • 一些工具类的原理
    • 并发包中的AQS
    • ThreadLocal与JMM
    • 锁的探究
  • JVM合集

  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • 并发合集
EffectTang
2024-04-26
目录

程序中的幽灵错误_基础

# 程序中的幽灵错误_基础

修复程序BUG应该说是基本的日常工作之一。作为Java程序员,也许你经常会被抛出的一大堆的异常堆栈所困扰,因为这可能预示着你又有工作可做了。但我这里想说的是,如果程序出错,你看到了异常堆栈,那你应该感到格外的高兴,因为这也意味着你极有可能可以在两分钟内修复这个问题(当然,并不是所有的异常都是错误)。最可怕的情况是:系统没有任何异常表现,没有日志,也没有堆栈,但是却给出了一个错误的执行结果,这种情况下,才真会让你抓狂。

# 无提示的错误

int v1=1073741827;
int v2=1431655768;
System.out.println("v1="+v1);
System.out.println("v2="+v2);
int ave=(v1+v2)/2;
System.out.println("ave="+ave);
1
2
3
4
5
6

结果竟然是ave=-894784850。这种不合理的结果,你稍微一思考其实就能明白原因,v1+v2的结果过大了导致了int的溢出。

这种错误就是明显的系统没有报错,但结果是错的。或许你觉得这种错误还好,但是如果这个问题发生在一个复杂的系统内部呢?在复杂的业务逻辑的掩盖下,再加上程序没有任何日志或异常,你是否还能这么轻易的发现问题呢?藏木于林是很厉害的。

这种幽灵错误——没有任何错误信息或异常,在并发的情况下,出现的频率更高。

# 并发下的ArrayList

public class ArrayListMultiThread {
    static ArrayList<Integer> al = new ArrayList<Integer>(10);
    public static class AddThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                al.add(i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new AddThread());
        Thread t2=new Thread(new AddThread());
        t1.start();
        t2.start();
        t1.join();t2.join();
        System.out.println(al.size());
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

因为ArrayList是线程不安全的容器,所以上述代码可能会出现以下几种结果:

  • 结果正常,ArrayList最终大小为 2000000
  • 最终结果小于,2000000,因为两个线程同时对一个位置进行赋值,并且这种情况没有任何错误提示,也是一个幽灵错误,同时不一定能复现。
  • 程序抛出异常
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 22
    at java.util.ArrayList.add(ArrayList.java:441)
    at geym.conc.ch2.notsafe.ArrayListMultiThread$AddThread.run
(ArrayListMultiThread.java:12)
    at java.lang.Thread.run(Thread.java:724)
1000015
1
2
3
4
5
6

这种原因是因为ArrayList在扩容的过程中,内部一致性被破坏,另一个线程访问到了不一致的内部状态,导致出现越界问题。

这种情况在线程不安全的集合中常常出现,HashMap就是其中之一。当然也有解决方案,其中最简单的就是使用ConcurrentHashMap代替HashMap。

# 错误的加锁

使用synchronized固然能帮助我们解决这种幽灵问题,但如果如果我们疏忽了,这种情况依然会发生,下面一种情况,是包装类型导致的,也是容易发生的:

public class Demo8 implements Runnable{
    public static Integer i = 0;
    static Demo8 badDemo = new Demo8();

    @Override
    public void run() {
        for (int j = 0; j < 10000; j++) {
            synchronized(i){
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(badDemo);
        Thread t2 = new Thread(badDemo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

实际结果比20000要小,使用了锁,但似乎没有效果。哪里出了问题呢?

要解释这个问题,得从Integer说起。在Java中,Integer属于不变对象。也就是对象一旦被创建,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它就永远表示1,你不可能修改Integer的值,使它为2。那如果你需要2怎么办呢?也很简单,新建一个Integer,并让它表示2即可。

在上述代码中,

i++;
// 实际变成了如下形式
i= Integer.valueOf(i.intValue()+1);
1
2
3

i++的本质是,创建一个新的Integer对象,并将它的引用赋值给i.

由于在多个线程间,并不一定能够看到同一个i对象(因为i对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。

既然问题找到了,那么也好解决,将锁的对象修改即可

synchronized(badDemo){
  
}
1
2
3
#并发
上次更新: 2025/04/23, 16:23:16
线程中的声明与守护线程_基础
JDK并发包

← 线程中的声明与守护线程_基础 JDK并发包→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式