简单阐述JAVA内存模型中工作内存"拷贝"的理解

上一篇博客说过了有关Android的HTTP API 的基础使用规则(包括一些基础类的讲解和项目中应该注意的问题)。这次仍然结合上一次的问题,在项目中碰见的另一个问题来说。
在项目中,向服务器发出请求的网络线程线程不止有一个,比如,用户在获取联系人列表的时候,回向服务器发送一个线程请求,但是从启动而言,总是有个一个轮询线程,这个线程每隔10s会请求一次服务器,然后把服务器的内容返回给客户端。现在问题来了,我们把请求服务器作为的方法作为一个静态方法,然后每个线程都调用这个方法来请求服务器,代码如下:

   private static InputStream getServetReponse(HttpClient mHttpCLient, HttpPost post) {
        try {
            HttpResponse mResponse = mHttpCLient.execute(post);
            //向服务器做请求连接
            mEntity = mResponse.getEntity();
            return (mEntity.getContent());
        } catch (SocketTimeoutException e) {
            //请求超时异常捕捉
            Log.i("lzw", "connection_timeout");
            e.printStackTrace();
            e.printStackTrace();
            Log.i("lzw", "IOException");
        }
        return (null);
    }
`

然而,当两个线程抢占一个HttpClient的时候,却出现了一个异常:Invalid use of SingleClientConnManager: connection still allocated。将这个异常百度之后,搜到一篇博客(http://blog.sina.com.cn/s/blog_5f2fecd901011o4f.html),大家如果碰到问题,都可以查看这个博客上说。照这篇博客所说,异常的抛出是由于两个线程一个请求未结束,另一个请求就已经开始的而导致的,解决的办法也很简单,在HttpClient的构造方法中,不设置默认的DeafultHttpClient()构造参数,使用这个默认构造方法,是SingleClientConnManager,不支持多线程,所以我们要让这个类支持多线程的话,使用以下的代码:

SchemeRegistry schReg = new SchemeRegistry();

                schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
                schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
                ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params, schReg);
                mHttpClient = new DefaultHttpClient(cm, params);
ThreadSafeClientManager 类支持多线环境,更换过代码之后,果然不会再出现那个问题了。

然而,虽然解决了问题,但是引起了我们的思考,JMM里面对线程的定义说:每个线程如果共享同一个变量,则是把这个变量拷贝到自己的工作内存中去的,这也是JMM中所谓的不可见性,即线程是如何交互的,那么,结合以上应用来看,这样的理论不是有悖我们的实践结果吗?两个线程同时拷贝HttpClient到自己的本地内存中吗?互不影响吧?怎么相互冲突呢?

这是一下的线程方法run(),在方法体中调用一个类的静态方法,这个静态方法就是使用HttpClient请求服务器。

public void run() {
String responseStr =
HttpUtils.requestHttpServer(this.requestURL, this.requestParams,
ComParameter.ENCODE_UTF_8, ComParameter.ENCODE_UTF_8);

  其实,让我们用内存模型的概念来分析,每个线程都有一个自己的工作主存,当我们要对堆空间的变量进行操作的时候,需要把这个变量拷贝到内存区域中去。对,是拷贝变量到自己的工作内存中去,但是到底是怎么一个拷贝呢?
 这里我讲下拷贝的理解:其实一个简单类型还是容易理解的,比如一个int,float类型,就是直接复制这个类型的数值就好。


![这里写图片描述](http://cdn1.infoqstatic.com/resource/articles/java-memory-model-1/zh/resources/11.png)
这里张图可以表明这个过程。

 但是如果是一个类呢?如果多线程共享一个类呢?其实也很简单,一个类在底层就是结构体,一个类的成员变量就是结构体的内部类型,试想:比如现在的两个线程共享一个类:是怎么拷贝的?

class Student{
private int id;
public String name;
public float grade;
public void setId(int id){
this.id=id;
}
public int getId(){
return(this.id);
}

       public static String requestSever(){
           //.........do somethind code// 
       }

}

   这个类怎么拷贝到工作内存中去的呢?其实也很简单:在底层,就是一个结构体,我们直接复制这个结构体就好,比如,这个结构体其实大小应该是20个字节(包括虚表指针一起),加上整形,字符串的地址值,8字节的浮点,直接把这些数值拷贝到自己的工作内存上去。那你还要问?那之中的方法呢?注意:方法是代码段:这里的方法分为非静态方法和静态方法。
      这里一定要搞清楚静态方法和非静态方法的理解:
      静态方法有一个隐含的传入参数,该参数是JVM给它的,和我们怎么写代码无关,
这个隐含的参数就是对象实例在stack中的地址指针。因此非静态方法(在stack中的指令代码)总是可以找到自己的专用数据(在heap 中的对象属性值)。
当然非静态方法也必须获得该隐含参数,因此非静态方法在调用前,必须先new一个对象实例,获得stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。

而静态方法无此隐含参数,因此也不需要new对象,只要class文件被ClassLoader load进入JVM的stack,该静态方法即可被调用。
当然此时静态方法是存取不到heap 中的对象属性的。

所以,非静态方法的代码是在栈上的,所以,这里不会拷贝方法的代码段到线程的主存中去,而是需要用到这个方法时候,根据Student 类new出的引用变量寻找其方法在内存中的地址中的地址值来寻找其代码段的地址值,然后在根据线程的程序计数器PC来执行代码指令。所以说,这里的非静态方法不会被拷贝,而是在执行的时候,直接根据地址值找到的。
      静态方法呢?静态属性是保存在stack中的(基本类型保存在stack中,对象类型地址保存在stack,值保存在heap 中),所以,上述的 public static String requestSever()这个静态方法,也是全局共享的。
 所以,静态方法体也不会被拷贝到线程的工作内存中去。
   那么,如果上述的student的类,里面有一个类类型的变量(引用)呢?而在多线程中又是如何的呢?比如这样:

class Student{

      public int[]  array;
      public  Teacher mTeacher;

}
“`
这样的类型要怎么分析呢?还是一样的,线程拷贝结构体,只是这个结构体都是地址值而已,那么访问的时候,对,也就说,我们只是拷贝了地址值,而不是把内存的堆空间上的地址值拷贝过来了,因此,如果多线程都共享了一个student的类变量,那么访问array或者mTeacher这个变量的时候,是直接去内存的堆空间中寻址的!
这样一来,我们终于把JMM中”拷贝”理解通透了,拷贝一些什么东西,哪些才是可以拷贝的,(代码是线程共享的)。
用以上理论来解释这个模型,就可以了,两个线程共享了HttpUtils类中的一个静态方法,也就是说,两个线程其实都没有拷贝变量的到自己工作内存中去,因为静态的代码是全局共享的,自然不会被拷贝,而静态的方法里面,HttpClient也是静态的,所以,两个线程共享了一个地址值,按照这个地址值去内存上寻址,自然会产生多线程冲突啦。

你可能感兴趣的:(内存)