Java中ThreadLocal,成员变量和局部变量。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_21508727/article/details/83141570

一.成员变量和局部变量

1.程序例子:

public class HelloThreadTest
{
    public static void main(String[] args)
    {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class HelloThread implements Runnable
{
    int i;

    @Override
    public void run()
    {
        while (true)
        {
            System.out.println("Hello number: " + i++);

            try
            {
                Thread.sleep((long) Math.random() * 1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            if (50 == i)
            {
                break;
            }
        }
    }
}

        该例子中,HelloThread类实现了Runnable接口,其中run()方法的主要工作是输出"Hello number: "字符串加数字i,并且同时递增i,当i到达50时,退出循环。

  main()方法中生成了一个HelloThread类的对象r,并且利用这个一个对象生成了两个线程。

  程序的执行结果是:顺次打印了0到49的数字,共50个数字。

  这是因为,i是成员变量,则HelloThread的对象r只包含这一个i,两个Thread对象因为由r构造,所以共享了同一个i

  当我们改变代码如下时(原先的成员变量i被注释掉,增加了方法中的局部变量i):

public class HelloThreadTest
{
    public static void main(String[] args)
    {
        HelloThread r = new HelloThread();

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();

    }

}

class HelloThread implements Runnable
{
    // int i;
    // 若i是成员变量,则HelloThread的对象r只包含这一个i,两个Thread对象因为由r构造,所以共享了同一个i
    // 打印结果是0到49的数字
    @Override
    public void run()
    {
        int i = 0;
        // 每一个线程都会拥有自己的一份局部变量的拷贝
        // 线程之间互不影响
        // 所以会打印100个数字,0到49每个数字都是两遍
        while (true)
        {
            System.out.println("Hello number: " + i++);

            try
            {
                Thread.sleep((long) Math.random() * 1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }

            if (50 == i)
            {
                break;
            }
        }
    }
}

如注释中所述,由于局部变量对于每一个线程来说都有自己的拷贝,所以各个线程之间不再共享同一个变量,输出结果为100个数字,实际上是两组,每组都是0到49的50个数字,并且两组数字之间随意地穿插在一起。 

2.小结

  如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,它们对该成员变量是彼此影响的,也就是说一个线程对成员变量的改变会影响到另一个线程。

  如果一个变量是局部变量,那么每个线程都会有一个该局部变量的拷贝(即便是同一个对象中的方法的局部变量,也会对每一个线程有一个拷贝),一个线程对该局部变量的改变不会影响到其他线程。

局部变量定义:在方法内定义的变量称为“局部变量”或“临时变量”,方法结束后局部变量占用的内存将被释放。

成员变量定义:在类体的变量部分中定义的变量,也称为字段。

全局变量定义:java中没有全局变量的定义,全局变量,又称“外部变量”,它不是属于哪个方法,作用域从定义的地址开始到源文件结束。

注意事项:当局部变量与全局变量重名时,起作用的是局部变量。

二.ThreadLocal

1.ThreadLocal概述

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
  ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
  线程局部变量并不是Java的新发明,很多语言(如IBM XL、FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供语言级支持,而以一种变通的方法,通过ThreadLocal的类提供支持。所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,这也是为什么线程局部变量没有在Java开发者中得到很好普及的原因。

  学习JDK中的类,首先看下JDK API对此类的描述,描述如下:

  该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
  API表达了下面几种观点:

  1、ThreadLocal不是线程,是线程的一个变量,你可以先简单理解为线程类的属性变量。

  2、ThreadLocal在类中通常定义为静态变量。

  3、每个线程有自己的一个ThreadLocal,它是变量的一个“拷贝”,修改它不影响其他线程。

  既然定义为类变量,为何为每个线程维护一个副本(姑且称为“拷贝”容易理解),让每个线程独立访问?多线程编程的经验告诉我们,对于线程共享资源(你可以理解为属性),资源是否被所有线程共享,也就是说这个资源被一个线程修改是否影响另一个线程的运行,如果影响我们需要使用synchronized同步,让线程顺序访问。

  ThreadLocal适用于资源共享但不需要维护状态的情况,也就是一个线程对资源的修改,不影响另一个线程的运行;这种设计是‘空间换时间’,synchronized顺序执行是‘时间换取空间’。

2. ThreadLocal方法及使用示例

  ThreadLocal<T>类在Spring,Hibernate等框架中起到了很大的作用。为了解释ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类,包括内部类ThreadLocalMap,和线程类Thread。所有方法如下图:

四个核心方法说明如下:

                   T         get()                   返回此线程局部变量的当前线程副本中的值。

 protected  T         initialValue()       返回此线程局部变量的当前线程的“初始值”。

                   void remove()             移除此线程局部变量当前线程的值。

                   void set(T value)         将此线程局部变量的当前线程副本中的值设置为指定值。

三.ThreadLocal与局部变量

1.程序列子

public class ThreadLocalLearn { 

    static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
            protected IntHolder initialValue() { 
            return new IntHolder(); 
        } 
    }; 

    public static void main(String args[]) { 
        for(int i=0; i<5; i++) { 
            Thread th = new Thread(new ThreadTest(tl, i)); 
            th.start(); 
        } 
    } 
} 

class ThreadTest implements Runnable{ 

    ThreadLocal<IntHolder> tl ; //threadlocal变量 
    int i;  //线程局部变量 (ThreadTest的成员变量)
    int a = 3; //线程局部变量(ThreadTest的成员变量) 

    public ThreadTest(ThreadLocal<IntHolder> tl, int i) { 
        super(); 
        this.tl = tl; 
        this.i = i; 
    } 

    @Override 
    public void run() { 
        tl.get().increAndGet(); 
        a++; 
        System.out.println(tl.get().getA() + " "); 
        System.out.println("a : " + a); 
    } 
} 

class IntHolder{ 

    int a = 1; 

    public int getA() { 
        return a; 
    } 

    public void setA(int a) { 
        this.a = a; 
    }

    public int increAndGet() {
         return ++a; 
    } 
}

代码的运行结果如下:

2 
a : 4
2 
a : 4
2 
a : 4
2 
a : 4
2 
a : 4

          可以看到,局部变量和ThreadLocal起到的作用是一样的,保证了并发环境下数据的安全性。

2.小结

根据上述结论那就是说,完全可以用局部变量来代替ThreadLocal咯,这样想法对么?我们看一看官方对于ThreadLocal的描述:

    This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

翻译起来就是

    ThreadLocal提供的是一种线程局部变量。这些变量不同于其它变量的点在于每个线程在获取变量的时候,都拥有它自己相对独立的变量初始化拷贝。ThreadL:ocal的实例一般是私有静态的,可以做到与一个线程绑定某一种状态。PS:有更好的翻译请指教。

      所以就这段话而言,我们知道ThreadLocal不是为了满足多线程安全而开发出来的,因为局部变量已经足够安全。ThreadLocal是为了方便线程处理自己的某种状态。
      可以看到ThreadLocal实例化所处的位置,是一个线程共有区域。好比一个银行和个人,我们可以把钱存在银行,也可以把钱存在家。存在家里的钱是局部变量,仅供个人使用;存在银行里的钱也不是说可以让别人随便使用,只有我们以个人身份去获取才能得到。所以说ThreadLocal封装的变量我们是在外面某个区域保存了处于我们个人的一个状态,只允许我们自己去访问和修改的状态。
      ThreadLocal同时提供了初始化的机制,在实例化时重写initialValue()方法,便可实现变量的初始化工作

但注意安全性问题

//method 1 
static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
    protected IntHolder initialValue() { 
        return new IntHolder(); 
    } 
}; 

//method 2 
IntHolder intHolder = new IntHolder(); 
    static ThreadLocal<IntHolder> tl = new ThreadLocal<IntHolder>(){ 
        protected IntHolder initialValue() { 
        return intHolder; 
    } 
};

         方法一和方法二都可以实现初始化工作,但是方法二不能保证线程变量的安全性,因为引用拷贝指向的是同一个实例,对引用拷贝的修改,等同于对实例的修改。

当然,也可以在判断ThreadlLocal获取数据为空时,在线程内部为ThreadLocal实例化一个数据。如下

if(null == tl.get()) {
    tl.set(new IntHolder());
}

猜你喜欢

转载自blog.csdn.net/qq_21508727/article/details/83141570