第2章 Java并行程序基础(三)
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并行程序基础(三)的更多相关文章
- 第2章 Java并行程序基础(二)
2.3 volatile 与 Java 内存模型(JMM) volatile对于保证操作的原子性是由非常大的帮助的(可见性).但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的 ...
- 第2章 Java并行程序基础(一)
2.1 有关线程你必须知道的事 进程是系统进行资源分配和调度的基本单位,是程序的基本执行实体. 线程就是轻量级进程,是程序执行的最小单位. 线程的生命周期,如图2.3所示. 线程的所有状态都在Thre ...
- Java并发程序设计(二)Java并行程序基础
Java并行程序基础 一.线程的生命周期 其中blocked和waiting的区别: 作者:赵老师链接:https://www.zhihu.com/question/27654579/answer/1 ...
- JAVA并行程序基础
JAVA并行程序基础 一.有关线程你必须知道的事 进程与线程 在等待面向线程设计的计算机结构中,进程是线程的容器.我们都知道,程序是对于指令.数据及其组织形式的描述,而进程是程序的实体. 线程是轻量级 ...
- JAVA并行程序基础二
JAVA并行程序基础二 线程组 当一个系统中,如果线程较多并且功能分配比较明确,可以将相同功能的线程放入同一个线程组里. activeCount()可获得活动线程的总数,由于线程是动态的只能获取一个估 ...
- JAVA并行程序基础一
JAVA并行程序基础一 线程的状态 初始线程:线程的基本操作 1. 新建线程 新建线程只需要使用new关键字创建一个线程对象,并且用start() ,线程start()之后会执行run()方法 不要直 ...
- Java并行程序基础。
并发,就是用多个执行器(线程)来完成一个任务(大任务)来处理业务(提高效率)的方法.而在这个过程中,会涉及到一些问题,所以学的就是解决这些问题的方法. 线程的基本操作: 1.创建线程:只需要new一个 ...
- 到头来还是逃不开Java - Java13程序基础
java程序基础 没有特殊说明,我的所有学习笔记都是从廖老师那里摘抄过来的,侵删 引言 兜兜转转到了大四,学过了C,C++,C#,Java,Python,学一门丢一门,到了最后还是要把Java捡起来. ...
- Spring MVC + Spring + Mybitis开发Java Web程序基础
Spring MVC + Spring + Mybitis是除了SSH外的另外一种常见的web框架组合. Java web开发和普通的Java应用程序开发是不太一样的,下面是一个Java web开发在 ...
随机推荐
- MySQL之性能优化
查看执行计划explain 1.1 Explain命令:它可以对select语句进行分析,并输出select执行的详细信息,以对开发人员针对性优化 1.2 Explain的用法:在selec ...
- 【转】Zookeeper原理
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护和命名服务等.Zookeeper是hadoop的一个子项目,其 ...
- RabbitMQ远程调用测试用例
RabbitMQ远程调用测试,使用外部机器192.168.174.132上的RabbitMQ,使用之前需要对远程调用进行配置,操作过程见博文“解决RabbitMQ远程不能访问的问题”. SendTes ...
- Date类(java.util)和SimpleDateFormat类(java.text)
在程序开发中,经常需要处理日期和时间的相关数据,此时我们可以使用 java.util 包中的 Date 类.这个类最主要的作用就是获取当前时间,我们来看下 Date 类的使用: 使用 Date 类的默 ...
- Java项目之家庭收支记账软件
模拟实现基于文本界面的家庭记账软件,该软件能够记录家庭的收入支出,并能够打印收支明细表. 项目采用分级菜单方式.主菜单如下: 假设家庭起始的生活基本金为10000元. 每次登记收入(菜单2)后,收入的 ...
- Linux之shell编程的基本使用
1.Shell shell是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便运行程序的系统级程序 2.shell编程打印hello world 2.1 代码部分 #!/bin/ba ...
- java小心机(5)| 浅谈类成员初始化顺序
类成员什么时候会被初始化呢?一般来说:"类的代码在初次使用时才被加载",加载过程包括了初始化. 比如说new A()调用构造函数时,类中全部成员都会被初始化. 但对于static域 ...
- manually Invoking Model Binding / Model Binding /Pro asp.net mvc 5
限制绑定器 数据源
- POJ Protecting the Flowers
点击打开题目 题目大意 奶牛要吃花,FJ来赶牛,将第i头牛赶走要2*ti分钟,奶牛每分钟吃di个单位花,求花的最小损失 先赶吃花多的,Wrong Answer QAQ 我们可以算一算损失 设sum=d ...
- airtest通过包名直接打开app的方法
工具提供直接打开APP的函数 #输入微信包名,打开微信 start_app("com.tencent.mm")