Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

时间:2022-05-19 04:29:51

前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析。

一、问题引出

先看一段代码

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。

下面来查看字节码文件验证:

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

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

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

这些指令告诉我们,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:

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

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

2.javap -c Main

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

前面的和不带参数的输出一样,后面的显示了方法的具体的字节码,从这个输出里面我们又可以了解更多的内容.

从上面的代码很容易看出,虽然在源程序中使用了"+",但在编译时仍然将"+"转换成StringBuilder。因此,我们可以得出结论,在Java中无论使用何种方式进行字符串连接,实际上都使用的是StringBuilder类。

3.javap -l Main

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

-l参数将显示行号和局部变量表

4.javap -p Main

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

-p参数将额外的打印public成员和方法的信息,因为这个类没有因此输出相同

这几个参数几乎就可以构成javap的最常使用的集合,最常用的应该还是-c选项,因为可以打印字节码的信息,关于这些字节码的详细涵义在Java 虚拟机规范中定义,感兴趣的可以查看相关的信息!

5.javap -s Main

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

输出内部类型签名

6.javap -v Main

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

输出栈大小,方法参数的个数

四、为eclipse配置javap命令

javap命令经常使用来对java类文件来进行反编译,主要用来对java进行分析的工具,在学习Thinking in Java时,因为须要对类文件反编译。以查看jvm究竟对我们写的代码做了哪些优化和处理,比方我看的

使用+=对字符串进行拼接时。jvm的处理方式。

废话不多说。以下直接带上配置的教程:

点击菜单条 Run --->  External tools ---> External tools Configurations...    然后例如以下图点击New

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

输入:

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字节码

例如以下图:

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

然后点击Run, 可能会出现例如以下的错误:

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

出现上面那个错误,说明你未选中java文件。然后选择一个java文件。点击javap,查看反编译后的结果。顺便说一下,你们可能不知道配置后的javap命令去那儿点击,看下图就知道去那儿点击javap了:

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

五、为Idea中添加javap命令

如果将javap命令添加到编译器中查看字节码文件会方便很多,下面介绍如何在idea中添加javap命令:

(1)打开setting菜单,

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

(2)找到工具中的扩展工具点击打开,

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

(3)点击左侧区域左上角的绿色加号按钮会弹出如下图这样的一个编辑框,按提示输入,

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

(4)完成后点击ok,点击setting窗口的apply然后ok,到这里就已经完成了javap命令的添加,

(5)查看已添加的命令并运行:在代码编辑区右键external tool的扩展选项里可以看到刚才添加的命令,点击执行即可。

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

参考资料:

龙果学院 《java并发编程与实战》

Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)的更多相关文章

  1. Java并发编程原理与实战七:线程带来的风险

    在并发中有两种方式,一是多进程,二是多线程,但是线程相比进程花销更小且能共享资源.但使用多线程同时会带来相应的风险,本文将展开讨论. 一.引言 多线程将会带来几个问题: 1.安全性问题 线程安全性可能 ...

  2. Java并发编程原理与实战四:线程如何中断

    如果你使用过杀毒软件,可能会发现全盘杀毒太耗时间了,这时你如果点击取消杀毒按钮,那么此时你正在中断一个运行的线程. java为我们提供了一种调用interrupt()方法来请求终止线程的方法,下面我们 ...

  3. Java并发编程原理与实战五:创建线程的多种方式

    一.继承Thread类 public class Demo1 extends Thread { public Demo1(String name) { super(name); } @Override ...

  4. Java并发编程原理与实战三十一:Future&FutureTask 浅析

    一.Futrue模式有什么用?------>正所谓技术来源与生活,这里举个栗子.在家里,我们都有煮菜的经验.(如果没有的话,你们还怎样来泡女朋友呢?你懂得).现在女票要你煮四菜一汤,这汤是鸡汤, ...

  5. Java并发编程原理与实战二十五:ThreadLocal线程局部变量的使用和原理

    1.什么是ThreadLocal ThreadLocal顾名思义是线程局部变量.这种变量和普通的变量不同,这种变量在每个线程中通过get和set方法访问, 每个线程有自己独立的变量副本.线程局部变量不 ...

  6. Java并发编程原理与实战十:单例问题与线程安全性深入解析

    单例模式我想这个设计模式大家都很熟悉,如果不熟悉的可以看我写的设计模式系列然后再来看本文.单例模式通常可以分为:饿汉式和懒汉式,那么分别和线程安全是否有关呢? 一.饿汉式 先看代码: package ...

  7. Java并发编程原理与实战九:synchronized的原理与使用

    一.理论层面 内置锁与互斥锁 修饰普通方法.修饰静态方法.修饰代码块 package com.roocon.thread.t3; public class Sequence { private sta ...

  8. Java并发编程原理与实战四十二:锁与volatile的内存语义

    锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...

  9. Java并发编程原理与实战三十三:同步容器与并发容器

    1.什么叫容器? ----->数组,对象,集合等等都是容器.   2.什么叫同步容器? ----->Vector,ArrayList,HashMap等等.   3.在多线程环境下,为什么不 ...

随机推荐

  1. Java小游戏贪吃蛇

    package snake; import java.awt.BorderLayout;import java.awt.Canvas;import java.awt.Color;import java ...

  2. 深入理解计算机中的 csapp.h和csapp.c

    csapp.h其实就是一堆头文件的打包,在http://csapp.cs.cmu.edu/public/code.html 这里可以下载.这是<深入理解计算机系统>配套网站. 在头文件的# ...

  3. ios工程中ARC与非ARC的混合

    ARC与非ARC在一个项目中同时使用, 1,选择项目中的Targets,选中你所要操作的Target,2,选Build Phases,在其中Complie Sources中选择需要ARC的文件双击,并 ...

  4. ubuntu12&period;04 安装 chrome

    1.下载deb包 2. sudo apt-get remove google-chrome-stable sudo dpkg -i google-chrome-stable_current_amd64 ...

  5. 《Programming WPF》翻译 第7章 2&period;图形

    原文:<Programming WPF>翻译 第7章 2.图形 图形时绘图的基础,代表用户界面树的元素.WPF支持多种不同的形状,并为它们每一个都提供了元素类型. 7.2.1基本图形类 在 ...

  6. linux上安装Elasticsearch

    搭建环境centos7及 首先通过工具上传tar包到/usr/local/mypackage/elasticsearch 解压tar包 解压后进入config目录,编辑配置文件 vi elastics ...

  7. 如何转换pdf文档为word文档--先标记下,本周把这个问题知识掌握

    http://developer.51cto.com/art/201803/567539.htm

  8. Angular 通过注入 &dollar;location 获取与修改当前页面URL

    //1.获取当前完整的url路径 var absurl = $location.absUrl(); //http://172.16.0.88:8100/#/homePage?id=10&a=1 ...

  9. PHP和javascript中url编码解码详解

    在实际开发中,我们可能会遇到路径编码解码的问题,下面总结了一下: PHP中: 1.urlencode(编码),urldecode(解码) $a = urlencode('http://www.baid ...

  10. Django模版中的过滤器详细解析 Django filter大全

    就象本章前面提到的一样,模板过滤器是在变量被显示前修改它的值的一个简单方法. 过滤器看起来是这样的: {{ name|lower }} 显示的内容是变量 {{ name }} 被过滤器 lower 处 ...