2.8 程序中的幽灵:隐蔽的错误

2.8.1 无提示的错误案例

  • 以求两个整数的平均值为例。请看下面代码:
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);
  • 输出如下:
v1 = 1073741827
v2 = 1431655768
ave = -894784850
  • 这是一个典型的溢出问题!显然,v1 + v2的结果已经导致了int的溢出。

2.8.2 并发下的ArrayList

  • ArrayList是一个线程不安全的容器。如果在多线程中使用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 < 10000000; 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());
}
}
  • 如果执行这段代码,可能会得到三种结果。

    • 第一,程序正常结束,ArrayList的最终大小确实2000000。这说明即使并行程序有问题,也未必会每次都表现出来。
    • 第二,程序抛出异常:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException:22
  • 这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另外一个线程访问到了不一致的内部状态,导致出现越界问题。

    • 第三,出现了一个非常隐蔽的错误,比如打印如下值作为ArrayList的大小:
1793758
  • 显然,这是由于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一位置进行赋值导致的。
  • 注意:改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

2.8.3 并发下诡异的HashMap

  • HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也可能会遇到意想不到的错误。不过和ArrayList不同,HashMap的问题似乎更加诡异。
public class HashMapMultiThread {
static Map<String, String> map = new HashMap<String, String>(); public static class AddThread implements Runnable {
int start = 0;
public AddThread(int start) {
this.start = start;
}
@Override
public void run() {
for (int i = start; i < 100000; i += 2) {
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
} public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(map.size());
}
}
  • 可能会得到以下三种情况:

    • 第一,程序正常结束,并且结果也是符合预期的。HashMap的大小为100000。
    • 第二,程序正常结束,但结果不符合预期,而是一个小于100000的数字,比如98868。
    • 第三,程序永远无法结束。
  • 对于前两种可能,和ArrayList的情况非常类似。
  • 使用jstack工具显示程序的线程信息,如下所示。其中jps可以显示当前系统中所有的Java进程。而jstack可以打印给定Java进程的内部线程及其堆栈。
C:\Users\geym >jps
14240 HashMapMultiThread
1192 Jps
C:\Users\geym >jstack 14240



  • 可以看到,主线程main正处于等待状态,并且这个等待是由于join()方法引起的,符合我们的预期。而t1和t2两个线程都处于Runnable状态,并且当前执行语句为HashMap.put()方法。
  • 查看put()方法的代码,如下所示:
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
  • 可以看到,当前两个线程正在遍历HashMap的内部数据。当前所处循环乍看之下是一个迭代遍历,就如同遍历一个链表一样。但在此时此刻,由于多线程的冲突,这个链表的结构已经遭到了破坏,链表成环了!当链表成环时,上述的迭代就等同于一个死循环,如图2.9所示,展示了最简单的一种环状结构,Key1和Key2互为对方的next元素。此时,通过next引用遍历,将形成死循环。

  • 这个死循环的问题,如果一旦发生,着实可以让你郁闷一把。但这个死循环的问题在JDK8中已经不存在了。由于JDK8对HashMap的内部实现了做了大规模的调整,因此规避了这个问题。但即使这样,贸 然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案就是使用ConcurrentHashMap代替HashMap。

2.8.4 初学者常见问题:错误的加锁

  • 现在,假设我们需要一个计数器,这个计数器会被多个线程同时访问。为了确保数据正确性,我们自然会需要对计数器加锁,因此,就有了以下代码:
public class BadLockOnInteger implements Runnable {
public static Integer i = 0;
static BadLockOnInteger instance = new BadLockOnInteger();
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized(i) {
i++;
}
}
} public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
  • 为了保证计数器i的正确性,每次对i自增前,都先获得i的锁,以此保证i是线程安全的。但我们得到一个比20000000小很多的数。

  • 要解释这个问题,得从Integer说起,在Java中,Integer属于不变对象。也就是对象一旦被创建,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它就永远表示1,你不可能修改Integer的值,使它为2.

  • 使用javap反编译这段代码的run()方法,我们可以看到:

  • 从结果中看出,实际上使用了Integer.valueOf()方法新建了一个新的Integer对象,并将它赋值给变量i。也就是说,i++在真实执行时变成了:

i = Integer.valueOf(i.intValue() + 1);
  • 进一步查看Integer.valueOf(),我们可以看到:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high) {
return IntegerCache.cache[i + (-IntegerCache.low)];
}
return new Integer(i);
}
  • Integer.valueOf()实际上是一个工厂方法,它会倾向于返回一个代表指定数值的Integer实例。因此,i++的本质是,创建一个新的Integer对象,并将它的引用赋值给i。
  • 由于在多个线程间,并不一定能够看到同一对象(因为i对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。
  • 修正这个问题也容易,只要将
sychronized(i) {
  • 改为:
synchronized(instance) {

第2章 Java并行程序基础(三)的更多相关文章

  1. 第2章 Java并行程序基础(二)

    2.3 volatile 与 Java 内存模型(JMM) volatile对于保证操作的原子性是由非常大的帮助的(可见性).但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的 ...

  2. 第2章 Java并行程序基础(一)

    2.1 有关线程你必须知道的事 进程是系统进行资源分配和调度的基本单位,是程序的基本执行实体. 线程就是轻量级进程,是程序执行的最小单位. 线程的生命周期,如图2.3所示. 线程的所有状态都在Thre ...

  3. Java并发程序设计(二)Java并行程序基础

    Java并行程序基础 一.线程的生命周期 其中blocked和waiting的区别: 作者:赵老师链接:https://www.zhihu.com/question/27654579/answer/1 ...

  4. JAVA并行程序基础

    JAVA并行程序基础 一.有关线程你必须知道的事 进程与线程 在等待面向线程设计的计算机结构中,进程是线程的容器.我们都知道,程序是对于指令.数据及其组织形式的描述,而进程是程序的实体. 线程是轻量级 ...

  5. JAVA并行程序基础二

    JAVA并行程序基础二 线程组 当一个系统中,如果线程较多并且功能分配比较明确,可以将相同功能的线程放入同一个线程组里. activeCount()可获得活动线程的总数,由于线程是动态的只能获取一个估 ...

  6. JAVA并行程序基础一

    JAVA并行程序基础一 线程的状态 初始线程:线程的基本操作 1. 新建线程 新建线程只需要使用new关键字创建一个线程对象,并且用start() ,线程start()之后会执行run()方法 不要直 ...

  7. Java并行程序基础。

    并发,就是用多个执行器(线程)来完成一个任务(大任务)来处理业务(提高效率)的方法.而在这个过程中,会涉及到一些问题,所以学的就是解决这些问题的方法. 线程的基本操作: 1.创建线程:只需要new一个 ...

  8. 到头来还是逃不开Java - Java13程序基础

    java程序基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来. ...

  9. Spring MVC + Spring + Mybitis开发Java Web程序基础

    Spring MVC + Spring + Mybitis是除了SSH外的另外一种常见的web框架组合. Java web开发和普通的Java应用程序开发是不太一样的,下面是一个Java web开发在 ...

随机推荐

  1. Qt5学习(1)

    1. In Qt, if you want to apply styles to the main window  itself, you must apply it to  its central ...

  2. static和final关键字

    static关键字 静态变量 静态变量:又称做类变量,也就是这个变量属于整个类,而不属于单个实例.类所有的实例共享静态变量,可以直接通过类名来访问它.静态变量在内存中只存在一份,当系统第一次加载类时, ...

  3. html 鼠标指针讲解

    html 鼠标指针 详情可以看https://www.w3school.com.cn/tiy/t.asp?f=csse_cursor 测试代码: <html> <body> & ...

  4. css3让元素自适应高度

    知识点: viewport:可视窗口,也就是浏览器.vw Viewport宽度, 1vw 等于viewport宽度的1%vh Viewport高度, 1vh 等于viewport高的的1% calc( ...

  5. docker 批量删除 镜像 容器

    我们在docker构建和测试时,经常会产生很多无用的镜像或者容器,我们可用如下两条命令一个一个删除. docker container rm 容器id #删除容器 可简写: docker rm 容器i ...

  6. 构造分组背包(CF)

    Ivan is a student at Berland State University (BSU). There are n days in Berland week, and each of t ...

  7. 替代not in 和 in 的办法

    在程序中,我们经常会习惯性的使用in和not in,在访问量比较小的时候是可以的,但是一旦数据量大了,我们就推荐使用not exists或者外连接来代替了.如果要实现一张表有而另外一张表没有的数据时, ...

  8. <密码学系列>—信息安全威胁

    懒惰等于将一个人活埋.--泰勒 本文已经收录至我的GitHub,欢迎大家踊跃star 和 issues. https://github.com/midou-tech/articles 点关注,不迷路! ...

  9. Mysql.复选条件的查询

    场景:有筛选条件 联盟:1.复联 2.正义联盟 3.猛禽小队,条件可多选,求查询结果. name league 飞人 复联,正义联盟 黑人 复联,正义联盟,猛禽小队 打手枪的男人 复联,猛禽小队 深井 ...

  10. ORM执行原生SQL语句

    # 1.connectionfrom django.db import connection, connections cursor = connection.cursor() # cursor = ...