Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)
前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析。
一、问题引出
先看一段代码
package com.roocon.thread.t3;
public class Sequence {
private int value;
public int getNext(){
return value++;
}
public static void main(String[] args) {
Sequence sequence = new Sequence();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
运行结果:仔细发现,出现了两个84,但代码想要的结果是,每个线程每次执行,就在原来的基础上加一。因此,这里就是线程的安全问题。
Thread-0 0
Thread-1 1
Thread-2 2
...
Thread-2 81
Thread-1 82
Thread-0 83
Thread-2 84
Thread-1 84
Thread-0 85
Thread-2 86
解释原因:
return value++; 通过字节码分析,它其实不是原子操作,value = value + 1;首先,要先读取value的值,然后再对value的值加1,最后将value+1后的结果赋值给原来的value。
如果有线程1和线程2,假设value此时为83。
1.线程1读取value的值,为83。
2.线程1对value进行加1操作,得到值是84,但此时cpu被线程2抢走了,线程2还没来得及将计算后的值赋值给原来的value。
3.线程2读取value的值,仍然为83。
4.线程2对value进行加1操作,得到84,此时cpu被线程1抢走了,线程1继续执行赋值操作,将它计算得到的结果值84赋值给value,于是,线程1输出了84。
5.线程2此时再次抢到了cpu执行权,于是,将它计算得到的结果值84赋值给value,最后输出84。
下面来查看字节码文件验证:

继续往下查看字节码文件的getNext方法:

这些指令告诉我们,value++并不是原子操作。其中,getfield就代表读取value这个字段的值,iadd就表示对value值进行加1操作,而putfield就代表将jia1操作得到的值赋值给原来的value。
指令的含义可以查看:https://www.cnblogs.com/dougest/p/7067710.html
二、解决问题
那么,如何解决上面的问题呢?如何保证多线程的安全性问题呢?
最简单的办法就是,加同步锁。
package com.roocon.thread.t3;
public class Sequence {
private int value;
public synchronized int getNext(){
return value++;
}
public static void main(String[] args) {
Sequence sequence = new Sequence();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
运行结果:
Thread-0 0
Thread-1 1
Thread-2 2
...
Thread-0 81
Thread-1 82
Thread-2 83
Thread-0 84
Thread-1 85
Thread-2 86
Thread-0 87
解决线程安全性问题有很多解决方案,因为,如果所有的解决方案都是加同步锁,那么,所谓的多线程并发最后变成了串行了。那么,多线程就显得没意义了。
最后,总结下产生线程安全性问题三个条件:
1.多线程环境下。
2.多个线程共享一个资源。如servlet就不是线程安全的。在它的service方法中操作同一个实例变量,如果多个线程同时访问,由于多个线程共享该变量,因此存在线程安全问题。
3.对线程进行非原子性操作。
三、javap的理解
也许我们很少会使用到javap工具,因为现在有很多好的反编译工具,但是我在此介绍这个工具不是使用它进行反编译,而是查看java编译器为我们生成 的字节码,通过比较字节码和源代码,我们可以发现很多的问题,一个很重要的作用就是了解很多编译器内部的工作机制。
public class Main {
public static void main(String[] args) {
String s = "abc";
String ss = "ok"+s+"xyz"+5;
System.out.println(ss);
}
}
在反编译前你当然需要先编译这个类了:javac -g Main.java(使用-g参数是因为要得到下面javap -l时的输出需要使用此选项)
编译完成后,我们在使用不同的选项看看不同的效果:
1.先看看最简单的不带参数的情况:javap Main:

不带参数的情况将打印类的public信息,包括成员和方法
从上面的输出中我们确定了两个知识:如果类没有显示的从其它类派生那么它就是从Object派生;如果没有为类显示的申明构造方法,那么编译器将为之生成一个缺省构造方法(不带参数的构造方法)
2.javap -c Main

前面的和不带参数的输出一样,后面的显示了方法的具体的字节码,从这个输出里面我们又可以了解更多的内容.
从上面的代码很容易看出,虽然在源程序中使用了"+",但在编译时仍然将"+"转换成StringBuilder。因此,我们可以得出结论,在Java中无论使用何种方式进行字符串连接,实际上都使用的是StringBuilder类。
3.javap -l Main

-l参数将显示行号和局部变量表
4.javap -p Main

-p参数将额外的打印public成员和方法的信息,因为这个类没有因此输出相同
这几个参数几乎就可以构成javap的最常使用的集合,最常用的应该还是-c选项,因为可以打印字节码的信息,关于这些字节码的详细涵义在Java 虚拟机规范中定义,感兴趣的可以查看相关的信息!
5.javap -s Main

输出内部类型签名
6.javap -v Main

输出栈大小,方法参数的个数
四、为eclipse配置javap命令
javap命令经常使用来对java类文件来进行反编译,主要用来对java进行分析的工具,在学习Thinking in Java时,因为须要对类文件反编译。以查看jvm究竟对我们写的代码做了哪些优化和处理,比方我看的
使用+=对字符串进行拼接时。jvm的处理方式。
废话不多说。以下直接带上配置的教程:
点击菜单条 Run ---> External tools ---> External tools Configurations... 然后例如以下图点击New
输入:
Name: javap
Locations: 选择jdk的javap.exe文件所在的位置
Working Directory: ${workspace_loc}/${project_name}
Arguments: -classpath bin -c ${java_type_name}
说明:${workspace_loc}表示工作空间所在的路径;
${project_name}表示项目的名称;
${java_type_name}表示所选java文件的类名(全名);
上面的这些变量能够通过每一栏右下方的Variablesbutton去选择。
(关于其它的一些变量读者能够自行去了解)
Arguments的内容: -classpath表示javap命名搜索的类路径(bin表示是相对于项目的相对路径) -c表示这里将生成JVM字节码
例如以下图:
然后点击Run, 可能会出现例如以下的错误:
出现上面那个错误,说明你未选中java文件。然后选择一个java文件。点击javap,查看反编译后的结果。顺便说一下,你们可能不知道配置后的javap命令去那儿点击,看下图就知道去那儿点击javap了:
五、为Idea中添加javap命令
如果将javap命令添加到编译器中查看字节码文件会方便很多,下面介绍如何在idea中添加javap命令:
(1)打开setting菜单,
(2)找到工具中的扩展工具点击打开,
(3)点击左侧区域左上角的绿色加号按钮会弹出如下图这样的一个编辑框,按提示输入,
(4)完成后点击ok,点击setting窗口的apply然后ok,到这里就已经完成了javap命令的添加,
(5)查看已添加的命令并运行:在代码编辑区右键external tool的扩展选项里可以看到刚才添加的命令,点击执行即可。
参考资料:
龙果学院 《java并发编程与实战》
Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)的更多相关文章
- Java并发编程原理与实战七:线程带来的风险
在并发中有两种方式,一是多进程,二是多线程,但是线程相比进程花销更小且能共享资源.但使用多线程同时会带来相应的风险,本文将展开讨论. 一.引言 多线程将会带来几个问题: 1.安全性问题 线程安全性可能 ...
- Java并发编程原理与实战四:线程如何中断
如果你使用过杀毒软件,可能会发现全盘杀毒太耗时间了,这时你如果点击取消杀毒按钮,那么此时你正在中断一个运行的线程. java为我们提供了一种调用interrupt()方法来请求终止线程的方法,下面我们 ...
- Java并发编程原理与实战五:创建线程的多种方式
一.继承Thread类 public class Demo1 extends Thread { public Demo1(String name) { super(name); } @Override ...
- Java并发编程原理与实战三十一:Future&FutureTask 浅析
一.Futrue模式有什么用?------>正所谓技术来源与生活,这里举个栗子.在家里,我们都有煮菜的经验.(如果没有的话,你们还怎样来泡女朋友呢?你懂得).现在女票要你煮四菜一汤,这汤是鸡汤, ...
- Java并发编程原理与实战二十五:ThreadLocal线程局部变量的使用和原理
1.什么是ThreadLocal ThreadLocal顾名思义是线程局部变量.这种变量和普通的变量不同,这种变量在每个线程中通过get和set方法访问, 每个线程有自己独立的变量副本.线程局部变量不 ...
- Java并发编程原理与实战十:单例问题与线程安全性深入解析
单例模式我想这个设计模式大家都很熟悉,如果不熟悉的可以看我写的设计模式系列然后再来看本文.单例模式通常可以分为:饿汉式和懒汉式,那么分别和线程安全是否有关呢? 一.饿汉式 先看代码: package ...
- Java并发编程原理与实战九:synchronized的原理与使用
一.理论层面 内置锁与互斥锁 修饰普通方法.修饰静态方法.修饰代码块 package com.roocon.thread.t3; public class Sequence { private sta ...
- Java并发编程原理与实战四十二:锁与volatile的内存语义
锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...
- Java并发编程原理与实战三十三:同步容器与并发容器
1.什么叫容器? ----->数组,对象,集合等等都是容器. 2.什么叫同步容器? ----->Vector,ArrayList,HashMap等等. 3.在多线程环境下,为什么不 ...
随机推荐
- golang string转json的一些坑
先带来点冷知识,不知道大家知不知道,反正我刚知道... 大佬们都知道怎么在string中给string类型赋值带双引号的字符串,没错就是用反斜杠,如下: msg := "{\"na ...
- java编程的一些注意事项
下面是参考网络资源和总结一些在java编程中尽可能做到的一些地方 1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例 ...
- MapReduce编程之Semi Join多种应用场景与使用
Map Join 实现方式一 ● 使用场景:一个大表(整张表内存放不下,但表中的key内存放得下),一个超大表 ● 实现方式:分布式缓存 ● 用法: SemiJoin就是所谓的半连接,其实仔细一看就是 ...
- 初入React(一)
React:是2013年Facebook在github上的一个开源js库,它将用户界面抽象为一个个组件,再由开发者将其组合成页面.它不是完整的MVC/MVVM框架,专注于提供清晰.简洁的view层解决 ...
- windows下的C++ socket服务器(2)
int main(int ac, char *av[]) { ); ) { exit(); } thread t; ) { int socket_fd = accept(tcp_socket, nul ...
- 我所理解的Delphi中的数组类型
数组可以使Object Pascal所拥有的任何数据类型,数组是一些数值的简单集合. var MyArray: ..] of Integer; { 声明一个数组包括5个整数数值} b ...
- 【硬件】- 英特尔CPU命名中的产品线后缀
产品线后缀是CPU命名体系里最复杂最难懂的,在英特尔冗长的产品线中,CPU的后缀也是千变万化.不带后缀的CPU一般就是最普通的桌面级处理器,不管是性能还是价格都比较中庸,比如当前性价比较高的Core ...
- SQLSERVER STANDARD 版本不支持内存数据库
1. 自己负责的一个环境 安装了 SQLSERVER2014 的 标准版 发现有问题. 恢复了一个带内存数据库的性能测试库之后报错. 报错信息很不明了,但是 查了半天发现必须升级企业版才可以... 还 ...
- SGU438_The Glorious Karlutka River =)
好题,有一些人在河的一边,想通过河里的某些点跳到对岸去.每个点最多只能承受一定数量的人,每人跳跃一次需要消耗一个时间.求所有人都过河的最短时间. 看网上说是用了什么动态流的神奇东东.其实就是最大流吧, ...
- P3165 [CQOI2014]排序机械臂
题目描述 为了把工厂中高低不等的物品按从低到高排好序,工程师发明了一种排序机械臂.它遵循一个简单的排序规则,第一次操作找到高度最低的物品的位置 P1P_1P1 ,并把左起第一个物品至 P1P_1P1 ...