JAVA内存模型

来源:互联网 发布:淘宝怎么看买家来源 编辑:程序博客网 时间:2024/06/06 13:14

         不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实JAVA的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的。JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm模型的基础上,如果解决多线程的可见性和有序性。

         那么,何谓可见性呢?多个线程之间是不能互相传递数据通信的,他们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作大小是有限制的。当线程操作某个对象时,执行顺序如下:
         (1)从主存复制变量到当前工作内存(readandload)

         (2)执行代码,改变共享变量值(useandassign)

         (3)用工作内存数据刷新主存相关内容(storeandwrite)

         JVM规范定义了线程对主存的操作指令:read、load、use、assign、store、write。当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改之后的值,这就是多线程的可见性问题。

那么什么是有序性呢?线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能从主内存中获取变量副本(read-load-use),也有可能直接引用原来的副本(use),也就是说read、load、use顺序可以由JVM实现系统决定。

线程不能直接为主内存中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store-write),至于何时同步过去,根据JVM实现系统决定。有该字段,则会从主内存中将该字段赋值到工作内存中,这个过程为read-load,完成后线程会引用该变量副本,当同一线程多次重复对字段赋值时,比如:

Java代码:

1.       for(inti=0;i<10;i++)

2.       a++;

线程有可能只对工作内存中的副本进行赋值,只到最后一次赋值后才同步到主存储区,所以assign、store、write顺序可以由JVM实现系统决定。假设有一个共享变量x,线程a执行x=x+1。从上面的描述中课可以知道x=x+1并不是一个原子操作,它的执行过程如下:

1.       从主存中读取变量x副本到工作内存

2.       给x加1

3.       讲x加1后的值写回主存

如果另外一个线程b执行x=x-1,执行过程如下:

1.       从主存中读取变量x副本到工作内存

2.       给x减1

3.       将x减1后的值写回主存

那么显然,最终的x的值是不可靠的。假设x现在为10,线程a加1,线程b减1,从表面上看,似乎最终x还是为10,但是多线程情况下会有这样的情况发生:

1.       线程a从主存读取x副本到工作内存,工作内存中x的值为10

2.       线程b从主存读取x副本到工作内存,工作内存中x的值为10

3.       线程a将工作内存中x加1,工作内存中x为11,

4.       线程a将x提交到主存中,主存中x为11

5.       线程b将工作内存中x减1,工作内存中x为9,

6.       线程b将x提交到主存中,主存中x为9

同样,x有可能为11,如果x是一个银行账户,线程a存款,线程b扣款,显然这样是有严重问题的,要解决这个问题,必须保证线程a和线程b是有序执行的,并且每个线程执行的加1和减1是一个原子操作。

publicclassAccount{

         privateintbalace;

         publicAccount(intbalance){

                   this.balance=balance;

}

publicintgetBalance(){

         returnbalance;

}

publicvoidadd(intnum){

         balance=balance+num;

}

publicvoidwithdraw(intnum){

         balance=balance-num;

}

publicstaticvoidmain(Stringargs[])throwsInterruptedException{

         Accountaccount=newAccount(1000);

         Threada=newThread(newAddThread(account,20),”add”);

         Threadb=newThread(newWithdrawThread(account,20),”withdraw”);

a.start();

b.start();

a.join();

b.join();

System.out.println(account.getBalance());

}

staticclassAddThreadimplementsRunnable{

         Accountaccount;

         intamount;

         publicAddThread(Accountaccount,intamount){

         this.account=account;

         this.amount=amount;

}

publicvoidrun(){

         for(inti=0;i<200000;i++){

                   account.add(amount);

}

}

}

}

staticclassWithdrawThreadimplementsRunnable{

         Accountaccount;

         intamount;

        

         publicWithdrawThread(Accountaccount;intamount){

         this.account=account;

         this.amount=amount;

}

publicvoidrun(){

         for(inti=0;i<100000;i++){

         account.withdraw(amount);

}

}

}

第一次执行的结果是10200,第二次执行的结果是1060,每次执行的结果都是不确定的,因为线程的执行顺序是不可预见的。这是java同步产生的根源。synchronized关键字保证了多个线程对于同步块是互斥的,synchronized作为一种同步手段,解决java多线程的执行的有序性和内存可见性,而volatile关键字只解决多线程的内存可见性问题。

synchronized关键字

         上面说了,java用synchronized关键字作为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标志了临界区。典型的用法如下:

synchronized(锁){

临界代码

}

为了保证银行账户的安全,可以操作账户的方法如下:

publicsynchronizedvoidadd(intnum){

         balance=balance+num;

}

publicsynchronizedvoidwithdraw(intnum){

         balance=balance-num;

}

刚才不是说了synchronized的用法是这样的吗:

synchronized(锁){

临界代码

}

那么对于publicsynchronizedvoidadd(intnum)这种情况,意味着什么呢?其实这种情况,锁就是这个方法所在的对象。同理,如果方法是publicsynchronizedvoidadd(intnum),那么锁就是这个方法所在的class。

         理论上,每个对象都可以作为锁,但一个对象作为锁时,应该被多个线程共享,这样显得有意义,在开发环境下,一个没有共享的对象作为锁是没有任何意义的。

例如:

         publicclassThreadTest{

                   publicvoidtest(){

         Objectlock=newObject();

         synchronized(lock){

         //dosomething

}

}

}

lock变量作为一个锁存在根本没有意义,因为它根本不是共享对象,每个线程进来都会执行Objectlock=newObject();每个线程都有自己的lock,根本不存在锁竞争。

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待cpu的调度。当一开始线程a第一次执行account.add方法时,jvm会检查锁对象account的就绪队列是否已经有线程在等待,如果有则表明了account的锁已经被占用了,由于是第一次执行,account的就绪队列为空,所以线程a获得了锁,执行account.add方法。如果恰好在这个时候,线程a要执行account.withdraw方法,因为线程a已经获得了锁还没有释放,所以线程b要进入account的就绪队列,等到得到锁后才可以执行。

一个线程执行临界区代码过程如下:

1.       获得同步锁

2.       清空工作内存

3.       从主存拷贝变量副本到工作内存

4.       对这些变量计算

5.       讲变量从工作内存写会到主存

6.       释放锁

可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

生产者/消费者

                   生产者/消费者模式其实是一种很经典的线程同步模型,很对时候,并不是光保证多个线程对某共享资源操作的互斥性就可以了,往往多个线程之间都是有协作的。

                   假设有这样一种情况,有一个桌子,桌子上面有一个盘子,盘子里只能放一颗鸡蛋,A专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没有鸡蛋,B专门从盘子里拿鸡蛋,如果盘子里没有鸡蛋,则等待直到盘子里有鸡蛋。其实盘子就是一个互斥区,每次往盘子里放鸡蛋应该都是互斥的,A的等到其实就是主动放弃锁,B等待时还要提醒A放鸡蛋。

                   很简单,调用锁的wait()方法就好。Wait方法是从Object来的,所以任意对象都有这个方法。

         Objectlock=newObject();

                   synchronized(lock){

         balance=balance-num;

//这里放弃了同步锁,好不容易得到,又放弃了

Lock.wait();

}

如果一个线程获得了锁,进入了同步块,执行lock.wait(),那么这个线程会进入到lock的阻塞队列。如果调用lock.notify()则会通知阻塞队列的某个线程进入就绪队列。

声明一个盘子只能放一个鸡蛋。

importjava.util.ArrayList;

importjava.util.List;

publicclassPlate{

    List<Object>eggs=newArrayList<Object>();

   

    publicsynchronizedObjectgetEgg(){

        if(eggs.size()==0){

            try{

                wait();

            }catch(InterruptedExceptione){

               

            }

        }

       

        Objectegg=eggs.get(0);

        eggs.clear();          //清空盘了

        notify();               //唤醒阻塞队列的某线程到就绪队列

        System.out.println("拿到鸡蛋");

        returnegg;

    }

   

    publicsynchronizedvoidputEgg(Objectegg){

        if(eggs.size()>0){

            try{

                wait();

            }catch(InterruptedExceptione){

               

            }

        }

        eggs.add(egg);         //往盘子里放鸡蛋

        notify();               //唤醒阻塞队列的某线程到就绪队列

        System.out.println("放入鸡蛋");

    }

}

publicclassAddThreadextendsThread{

    privatePlateplate;

    privateObjectegg=newObject();

   

    publicAddThread(Plateplate){

        this.plate=plate;

    }

   

    publicvoidrun(){

        for(inti=0;i<5;i++){

            plate.putEgg(egg);

        }

    }

}

        

        

publicclassGetThreadextendsThread{

    privatePlateplate;

   

    publicGetThread(Plateplate){

        this.plate=plate;

    }

   

    publicvoidrun(){

        for(inti=0;i<5;i++){

            plate.getEgg();

        }

    }

   

    publicstaticvoidmain(String[]args){

        try{

            Plateplate=newPlate();

            Threadadd=newThread(newAddThread(plate));

            Threadget=newThread(newGetThread(plate));

            add.start();

            get.start();

            add.join();

            get.join();

        }catch(InterruptedExceptione){

            e.printStackTrace();

        }

        System.out.println("测试结束");

    }

}

执行结果:

放入鸡蛋

拿到鸡蛋

放入鸡蛋

拿到鸡蛋

放入鸡蛋

拿到鸡蛋

放入鸡蛋

拿到鸡蛋

放入鸡蛋

拿到鸡蛋

测试结束

   

    声明一个Plate对象为plate,被线程A和线程B共享,A专门放鸡蛋,B专门纳鸡蛋,假设:

    1开始,A调用plate.putEgg()方法,此时eggs.size()为0,因此顺利将鸡蛋放到盘子,还之行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有线程。

    2又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,自己进入了锁对象的阻塞线程

    3此时,来了一个B线程对象,调用plate.getEgg()方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,因此马上得到锁,开始往盘子里放鸡蛋,此时盘子是空的,因此放鸡蛋成功。

    4假设接着来了线程A,就重复2;假设来线程B,就重复3.

         volatile关键字

volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说?因为volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最彻底的同步要保证有序性和可见性,例如synchronized。任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都能及时写在主存。因此对于valatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改时有序的。什么意思呢?

publicclassVolatileTest{

         publicvolatileinta;

         publicvoidadd(intcount){

         a=a+count;

}

}

         当一个VolatileTest对象被多个线程共享,a的值不一定是正确的,因为a=a+count包含了好几部操作,而此时多个线程的执行是无序的,因为没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情况下可以使用volatile变量代替锁。要使用volatile变量提供理想的线程安全,必须同时满足下面两个条件:
1.对变量的写操作不依赖于当前值

2.该变量没有包含在具有其他变量的不变式中

volatile只保证了可见性,所以volatile适合直接赋值的场景。

publicclassVolatileTest{

         publicvolatileinta;

         publicvoidseta(inta){

         this.a=a;

}

}

在没有volatile声明时,多线程环境下,a的最终值不一定是正确的,因为this.a=a;涉及到给a赋值和将a同步回主存的步骤,这个顺序可能被打乱。如果用volatile声明了,读取主存副本到工作内存和同步a到主存的步骤,相当于是一个原子操作。所以简单来说,volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。这是一种很简单的同步场景,这时候使用volatile的开销将会非常小。

站内很多人都问我,所谓线程的“工作内存”到底是个什么东西?有的人认为是线程的栈,其实这种理解是不正确的。看看JLS(java语言规范)对线程工作内存的描述,线程的workingmemory只是cpu的寄存器和告诉缓存的抽象描述。

可能很多人都觉得莫名其妙,说JVM的内存模型,怎么会扯到cpu上去呢?此时,我认为很有必要阐述下,免得很多人看的不明不白的。先抛开java虚拟机不谈,我们都知道,现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级是:寄存器—高速缓存—内存。线程消耗的是cpu,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写会内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特征:原子性,有序性,可见性。在《线程安全总结》这篇文章中,为了理解方便,我把原子性和有序性统一叫做“多线程执行有序性”。支持多线程的平台都会面临这种问题,运行在多线程平台上支持多线程的语言应该提供解决该问题的方案。

那么我们看看JVM,JVM是一个虚拟的计算机,它也会面临多线程并发问题,java程序运行在虚拟机平台上,java程序员不可能直接去控制底层线程对寄存器高速缓存内存之间的同步,那么java从语法层面,应该给开发人员提供一种解决方案,这个方案就是诸如synchronized,volatile,锁机制(如同步块,就绪队列,阻塞队列)等等。这些方案只是语法层面的,但我们要从本质上去理解它,不能仅仅知道一个synchronized可以保证同步就完了。在这里我说的是jvm的内存模型,是动态的,面向多线程并发的,沿袭JSL的”workingmemory”的说法,只是不想闲扯到太多底层细节,因为《线程安全总结》这篇文章在说明怎样从语法层而去理解java线程同步,知道各个关键字的使用场景。

今天有人问我,那java的线程不是有栈吗?难道栈不是工作内存吗?工作内存这四个字得放到具体的场景中区描述,方能体现它具体的意义,在描述JVM的线程同步时,工作内存指的是寄存器和高速缓存的抽象描述,具体请自行参阅JLS。上面讲的都是动态的内存模型,甚至已经超出了JVM的范围,那么JVM的内存静态存储是怎样划分的?今天还有人问我,jvm的内存模型不是有eden区吗?也不见得你提起。我跟他说,这是两个角度去看的,甚至是不同的范围,动态的线程同步的内存模型,涵盖了cpu,寄存器,高速缓存,内存;jvm的静态内存存储模型只是一种对内存的物理划分而已,它只局限在内存,而且只局限在jvm的内存。那么线程栈,eden区都仅仅在jvm内存。

说说jvm的线程栈和有个朋友反复跟我纠结的eden区吧。jvm的内存,被划分了很多的区域

1.       程序计数器

每一个java线程都有一个线程计数器来用于保存程序执行到当前方法的哪一个指令。

2.       线程栈

线程的每个方法被执行的时候,都会同时创建一个帧(Frame)

用于存储本地变脸表、操作栈、动态链接、方法出入口等信息。每个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,讲抛出StackOverflowError异常;如果VM栈可以动态扩展(VMSpec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

3.       本地方法栈

4.       堆

每个线程的栈都是该线程私有的,堆则是所有线程共享的。当我们new一个对象时,该对象就被分配到了堆中。但是堆,并不是一个简单的概念,堆区又划分了很多区域,问为什么堆划分了这么多区域,这是为了JVM的内存垃圾收集,似乎越扯越远了,扯到垃圾收集了,现在的jvm的gc都是按代收集,堆区大致被分为三大块:新生代、旧生代、持久代(虚拟的);新生代又分为eden区,s0区,s1区。新建一个对象时,基本小的对象,生命周期短的对象都会放在新生代的eden区中,eden区满时,有一个小范围的gc(minorgc),整个新生代满时,会有一个大范围的gc(majorgc),讲新生代里的部分对象转到旧生代里。

5.方法区

         其实就是永久代(PermanentGeneration),方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VMSpace描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不现实垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的”成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻

5.       常量池

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量表(constant_pooltable),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)

什么是线程安全?

  如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

  线程安全问题都是由全局变量及静态变量引起的。

  

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

  比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。

  在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;

  而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。

那好,现在我们来看看ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。

线程安全性

  类要成为线程安全的,首先必须在单线程环境中有正确的行为。如果一个类实现正确(这是说它符合规格说明的另一种方式),那么没有一种对这个类的对象的操作序列(读或者写公共字段以及调用公共方法)可以让对象处于无效状态,观察到对象处于无效状态、或者违反类的任何不可变量、前置条件或者后置条件的情况。

  此外,一个类要成为线程安全的,在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排或者交错,它必须仍然有如上所述的正确行为,并且在调用的代码中没有任何额外的同步。其效果就是,在所有线程看来,对于线程安全对象的操作是以固定的、全局一致的顺序发生的。

  正确性与线程安全性之间的关系非常类似于在描述 ACID(原子性、一致性、独立性和持久性)事务时使用的一致性与独立性之间的关系:从特定线程的角度看,由不同线程所执行的对象操作是先后(虽然顺序不定)而不是并行执行的。

  线程安全性不是一个非真即假的命题。 Vector 的方法都是同步的,并且 Vector 明确地设计为在多线程环境中工作。但是它的线程安全性是有限制的,即在某些方法之间有状态依赖(类似地,如果在迭代过程中Vector 被其他线程修改,那么由 Vector.iterator() 返回的 iterator会抛出ConcurrentModifiicationException)。

  对于 Java 类中常见的线程安全性级别,没有一种分类系统可被广泛接受,不过重要的是在编写类时尽量记录下它们的线程安全行为。

  Bloch 给出了描述五类线程安全性的分类方法:不可变、线程安全、有条件线程安全、线程兼容和线程对立。只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。这种系统有其局限性 -- 各类之间的界线不是百分之百地明确,而且有些情况它没照顾到 -- 但是这套系统是一个很好的起点。这种分类系统的核心是调用者是否可以或者必须用外部同步包围操作(或者一系列操作)。下面几节分别描述了线程安全性的这五种类别。

不可变

  不可变的对象一定是线程安全的,并且永远也不需要额外的同步[1]。因为一个不可变的对象只要构建正确,其外部可见状态永远也不会改变,永远也不会看到它处于不一致的状态。Java 类库中大多数基本数值类如Integer 、 String 和 BigInteger都是不可变的。

线程安全

  线程安全的对象具有在上面“线程安全”一节中描述的属性 -- 由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不需要任何额外的同步。这种线程安全性保证是很严格的 -- 许多类,如 Hashtable 或者 Vector 都不能满足这种严格的定义。

有条件的线程安全

  有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器 --由这些类返回的 fail-fast 迭代器假定在迭代器进行遍历的时候底层集合不会有变化。为了保证其他线程不会在遍历的时候改变集合,进行迭代的线程应该确保它是独占性地访问集合以实现遍历的完整性。通常,独占性的访问是由对锁的同步保证的 -- 并且类的文档应该说明是哪个锁(通常是对象的内部监视器(intrinsicmonitor))。

  如果对一个有条件线程安全类进行记录,那么您应该不仅要记录它是有条件线程安全的,而且还要记录必须防止哪些操作序列的并发访问。用户可以合理地假设其他操作序列不需要任何额外的同步。

线程兼容

  线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。这可能意味着用一个 synchronized 块包围每一个方法调用,或者创建一个包装器对象,其中每一个方法都是同步的(就像 Collections.synchronizedList() 一样)。也可能意味着用 synchronized 块包围某些操作序列。为了最大程度地利用线程兼容类,如果所有调用都使用同一个块,那么就不应该要求调用者对该块同步。这样做会使线程兼容的对象作为变量实例包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。

  许多常见的类是线程兼容的,如集合类 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 类 Connection 和 ResultSet 。

线程对立

线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。线程对立类的一个例子是调用 System.setOut() 的类。

0 0