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并发编程与实战》

原文地址:https://www.cnblogs.com/pony1223/p/9363299.html

时间: 2024-10-13 10:45:40

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

Java并发编程原理与实战视频教程

14套java精品高级架构课,缓存架构,深入Jvm虚拟机,全文检索Elasticsearch,Dubbo分布式Restful 服务,并发原理编程,SpringBoot,SpringCloud,RocketMQ中间件,Mysql分布式集群,服务架构,运 维架构视频教程 14套精品课程介绍: 1.14套精 品是最新整理的课程,都是当下最火的技术,最火的课程,也是全网课程的精品: 2.14套资 源包含:全套完整高清视频.完整源码.配套文档: 3.知识也 是需要投资的,有投入才会有产出(保证投入产出比是

Java并发编程原理与实战

Java并发编程原理与实战网盘地址:https://pan.baidu.com/s/1c3mpC7A 密码: pe62备用地址(腾讯微云):https://share.weiyun.com/11ea938c7ad43783a934ed1d492eed8d 密码:ogHukS 原文地址:http://blog.51cto.com/13406637/2071116

Java并发编程学习笔记(一)线程安全性 1

什么是线程安全性: 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问."共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以发生变化. 一个对象是否需要线程安全的,取决于他是否被多个线程访问.这指的是在程序中访问对象的方式,而不是对象要实现的功能.要使得对象时线程安全的,需要采用同步机制来协同对对象可变状态的访问.如果无法实现协同,那么可能导致数据破坏以及其他不该出现的结果. 如果当多个线程访

Java并发编程原理与实战二十:线程安全性问题简单总结

一.出现线程安全性问题的条件 •在多线程的环境下 •必须有共享资源 •对共享资源进行非原子性操作 二.解决线程安全性问题的途径 •synchronized (偏向锁,轻量级锁,重量级锁) •volatile •JDK提供的原子类 •使用Lock(共享锁,排它锁) 三.认识的“*锁” •偏向锁 Java偏向锁(Biased Locking)是Java6引入的一项多线程优化. 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不

Java并发编程原理与实战十九:AQS 剖析

一.引言在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问.synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现.而从JDK1.5以后java界的一位大神—— Doug Lea 开发了AbstractQueuedSynchronizer(AQS)组件,使用原生java代码实现了synchronized语义.换句话说,Doug Lea没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,仅用一个普普通通的类就完成了代

Java并发编程原理与实战二十九:Exchanger

一.简介 前面三篇博客分别介绍了CyclicBarrier.CountDownLatch.Semaphore,现在介绍并发工具类中的最后一个Exchange.Exchange是最简单的也是最复杂的,简单在于API非常简单,就一个构造方法和两个exchange()方法,最复杂在于它的实现是最复杂的.在API是这么介绍的:可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象.Exchanger 可能被

Java并发编程原理与实战四十一:重排序 和 happens-before

一.概念理解 首先我们先来了解一下什么是重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段. 从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如下图所示 上述的1属于编译器重排序,2和3属于处理器重排序.这些重排序可能会导致多线程程序出现内存可见性问题.在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因):但在多线程程序中,对存在控制依赖的操作重排序,可能会改变

Java并发编程学习笔记(一)——线程安全性

1.当多个线程访问某个状态变量并且其中有一个献策灰姑娘执行写入操作时,必须采用同步机制来协同这些线程对变量的访问.Java中的主要同步机制是关键字synchronized,他提供了一种独占的加锁方式. 2.在任何情况下,只有当类中仅包含自己的状态时,线程安全类才是有意义的. 3.当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些献策灰姑娘讲如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 4.无状态对象一定是线程安全的

Java并发编程原理与实战六:主线程等待子线程解决方案

本文将研究的是主线程等待所有子线程执行完成之后再继续往下执行的解决方案 public class TestThread extends Thread { public void run() { System.out.println(this.getName() + "子线程开始"); try { // 子线程休眠五秒 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.