精华内容
下载资源
问答
  • 线程安全性文档

    千次阅读 2017-04-18 09:50:15
    首先说一个错误的观点是“只要是加了synchronized关键字的方法或者代码块就一定是线程安全的,而没有加这个关键字的代码就不是线程安全的”。这种观点认为“线程安全要么全有要么全无”,事实上这是错误的。因为线程...

    首先说一个错误的观点是“只要是加了synchronized关键字的方法或者代码块就一定是线程安全的,而没有加这个关键字的代码就不是线程安全的”。这种观点认为“线程安全要么全有要么全无”,事实上这是错误的。因为线程安全包含了几种级别:

    1. 不可变的(Immutable):类的实例不可变(不可变类),一定线程安全,如String、Long、BigInteger等。
    2. 无条件的线程安全(Unconditionally ThreadSafe):该类的实例是可变的,但是这个类有足够的的内部同步。所以,它的实例可以被并发使用,无需任何外部同步,如Random和ConcurrentHashMap。
    3. 有条件的线程安全(Conditionally ThreadSafe):某些方法需要为了安全的并发而在外部进行同步,其余方法与无条件的线程安全一致。如Collection.synchronized返回的集合,对它们进行迭代时就需要外部同步。如下代码,当对synchronizeColletcion返回的 collection进行迭代时,用户必须手工在返回的 collection 上进行同步,不遵从此建议将导致无法确定的行为。
        Collection c = Collections.synchronizedCollection(myCollection);
           ...
        synchronized(c) {
            Iterator i = c.iterator(); // Must be in the synchronized block
            while (i.hasNext())
               foo(i.next());
        }
    4. 非线程安全(UnThreadSafe):该类是实例可变的,如需安全地并发使用,必须外部手动同步。如HashMap和HashSet。
    5. 线程对立的(thread-hostile):即便所有的方法都被外部同步保卫,这个类仍不能安全的被多个线程并发使用。这种情况的类很少,不常用。

    以上是常用的5种线程安全性的级别,这些级别应该认真编写在类的线程安全注解中,以让用户清楚的知道某个类的线程安全性。synchronized关键字与这个文档毫无关系。

    Conditionally-ThreadSafe类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁。”

    如果正在编写的是无条件的线程安全类,就应该考虑使用私有的锁对象来代替同步方法,这样可以防止客户端程序和子类的不同步干扰。下面的代码体现了使用同步方法会造成的子类在无意之中妨碍基类的操作。

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 这个程序的运行结果是concurrent1和concurrent2随机出现,syn1都在syn2之后或者之前。
     * 由此可说明使用私有对象锁可以避免父子类方法的互相同步的干扰问题。
     * 因此,在Effective Java中说私有对象锁尤其适用于那些专门为继承而设计的类中。
     */
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
    
            final CountDownLatch start = new CountDownLatch(1);
            final CyclicBarrier firstSectionOver = new CyclicBarrier(2);
            final Son son = new Son();
            new Thread(new Runnable() {
                public void run() {
                    try {
                        start.await();
                        son.syn1();
                        firstSectionOver.await();
                        son.concurrent1();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                public void run() {
                    try {
                        start.await();
                        son.syn2();
                        firstSectionOver.await();
                        son.concurrent2();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            start.countDown();
    
        }
    }
    
    class Parent {
        private final Object parentLock = new Object();
    
        public synchronized void syn1() throws InterruptedException {
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("syn1");
            }
        }
    
        public void concurrent1() throws InterruptedException {
            synchronized (parentLock) {
                for (int i = 0; i < 10; i++) {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("concurrent1");
                }
            }
        }
    }
    
    class Son extends Parent {
        private final Object sonLock = new Object();
    
        public synchronized void syn2() throws InterruptedException {
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("syn2,has no concern about the Method syn1");
            }
        }
    
        public void concurrent2() throws InterruptedException {
            synchronized (sonLock) {
                for (int i = 0; i < 10; i++) {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("concurrent2,,has no concern about the Method concurrent1");
                }
            }
        }
    }


    在上面的代码中,syn2和concurrent2都想成为一个不干扰父类方法并且也不被父类方法干扰的同步方法。从运行的结果来看,concurrent2做到了,但syn2没做到。syn2和syn1由于使用synchronized(this)实现同步而对彼此造成干扰。



    展开全文
  • 线程安全性文档化。

    千次阅读 2018-08-05 10:59:45
    如果你没有在一个类的文档中描述其行为的并发情况,使用这个类的程序员将不得不做出某些假设。如果这些假设是错误的,这样得到的程序就可能缺少足够的同步,或者过度同步。无论属于这其中的哪种情况,都可能会发生...

    当一个了IDE实例或者静态方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立的约定的重要组成部分。如果你没有在一个类的文档中描述其行为的并发性情况,使用这个类的程序员将不得不做出某些假设。如果这些假设是错误的,这样得到的程序就可能缺少足够的同步,或者过度同步。无论属于这其中的哪种情况,都可能会发生严重的错误。

    你可能听过这样的说法:通过查看文档中是否出现synchronized修饰符,可以确定一个方法是否是线程安全的。这种说法从几个方面来说都是错误的。在正常的操作中,Javadoc并没有在它的输出中包含synchronized修饰符,这是有理由的。因为在一个方法声明中出现synchronized修饰符,这是个实现细节,并不是导出的API的一部分。她并不一定表明这个方法是线程安全的。

    而且,“出现了synchronized关键字就足以用文档说明线程安全性”的这种说法隐含了一个错误的观念,即认为线程安全性是一种“要么全有要么全无”的属性。实际上,线程安全性有多种级别。一个类为了可被多个线程安全的使用,必须在文档中清楚地说明他所支持的线程安全性级别。

    下面的列表概括了线程安全性的几种级别。这份列表并没有涵盖所有的可能,而只是些常见的情形:

    • 不可变的(immutable)——这个类的实例视不变的。所以,不需要外部的同步。这样的例子包括String、Long和BigInteger。
    • 无条件的线程安全(unconditionally thread-safe)——这个类的实例是可变的,但是这个类有足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。其例子包括Random和ConcurrentHashMap。
    • 有条件的线程安全(conditionally thread-safe)——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。这样的例子包括Collections.synchronized包装返回的集合,他们的迭代器要求外部同步。
    • 非线程安全(not thread-safe)——这个类的实例是可变的。为了并发的使用他们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如ArrayList和HashMap。
    • 线程对立的(thread-hostile)——这个类不能安全的被多个线程并发使用,即使所有方法调用都被外部同步包围。线程对立的根源通常在于,没有同步的修改静态数据。没有人会有意编写一个线程对立的类;这种类是因为没有考虑到并发性而产生的后果。幸运的是,在Java平台类库中,线程对立的类或方法非常少。System.runFinalizersOnExit方法是线程对立的,但已经被废除了。

    在文档中描述一个有条件的线程安全类要特别小心。你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁(极少的情况下是指哪几把锁)。通常情况下,这是指作用在实例自身上的那把锁。但也有例外。如果一个对象代表了另一个对象的一个视图(view),客户通常就必须在后台对象上同步,以防止其他线程直接修改后台对象。

    类的线程安全说明通常放在他的文档注释中,但是带有特殊线程安全属性的方法则应该在他们自己的文档注释中说明他们的属性。没有必要说明枚举类型的不可变性。除非从返回类型来看已经很明显,否则静态工厂必须在文档中说明被返回对象的线程安全性。

    当一个类承诺了“使用一个公有可访问的所对象”时,就意味着允许客户端以原子的方式执行一个方法调用序列,但是,这种灵活性是要付出代价的。并发集合使用的那种并发控制,并不能与高性能的内部并发控制相兼容。客户端还可以发起拒绝服务攻击,它只需超时的保持公有可访问锁即可。这有可能是无意的,也可能是有意的。

    为了避免这种拒绝服务攻击,应该使用一个私有锁对象来代替同步的方法(隐含着一个公有可访问锁)。

    重申一下,私有锁对象模式只能用在无条件的线程安全类上。有条件的线程安全类不能使用这种模式,因为他们必须在文档中说明:在执行某些方法调用序列时,他们的客户端程序必须获得哪把锁。

    私有锁对象模式特别适用于那些专门为继承而设计的类。如果这种类使用它的实例作为锁对象,子类可能很容易在无意中方案基类的操作,反之亦然。出于不同的目的而使用相同的锁,子类和基类可能会“相互绊住对方的脚”。这不只是一个理论意义上的问题。

    简而言之,每个类都应该利用字斟句酌的说明或者线程安全注解,清楚地在文档中说明他的线程安全属性。sychronized修饰符与这个文档毫无关系。有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁”。如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活的对并发控制采用更加复杂的方法。

    展开全文
  • shared_ptr的线程安全性boost官方文档对shared_ptr线程安全性的正式表述是:shared_ptr对象提供与内置类型相同级别的线程安全性。【shared_ptrobjects offer the same level of thread safety as built-in types.】...
  • java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...
  • 线程安全性

    千次阅读 2007-02-06 10:05:00
    线程安全性Li Chun-lin (译)06/16/2006 创建日期:06/16/2006 最后修订:06/16/2006 版本:1.0.0 作者:Peter C. Chapin 1. 概述当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历...

    线程安全性

    Li Chun-lin (译)
    06/16/2006
    • 创建日期:06/16/2006
    • 最后修订:06/16/2006
    • 版本:1.0.0
    • 作者:Peter C. Chapin

    1. 概述

    当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历若干非法的中间状态。这跟外科医生做手术有点象,尽管手术的目的是改善患者的健康,但医生把手术过程分成了几个步骤,每个步骤如果不是完全结束的话,都会严重损害患者的健康。想想看,如果一个医生切开患者的胸腔后要休三周假会怎么样?与此类似,调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定状态),等到操作完全结束,该对象才会重新回到完全可用的状态。如果其他线程企图访问一个处于不可用状态的对象,该对象将不能正确响应从而产生无法预料的结果,如何避免这种情况发生是线程安全性的核心问题。单线程的程序中是不存在这种问题的,因为在一个线程更新某对象的时候不会有其他线程也去操作同一个对象。(除非其中有异常,异常是可能导致上述问题的。当一个正在更新某对象的线程因异常而中断更新过程后,再去访问没有完全更新的对象,会出现同样的问题)

    2. 线程安全性的级别

    就线程安全性进行讨论的时候存在这样一个问题:线程的安全性是存在多种级别的,每个人谈论的级别其实并不相同,仅仅说某段代码不具备线程安全性并不能说明全部问题。然而许多人对线程的安全性有一些想当然的预期,有些时候这些预期是合理而合法的,但有些时候不是。下面给出一些此类的预期:

    • 通常认为多个线程读某对象时不会产生问题,问题只会在更新对象的时候出现,因为只有那时对象才会被修改, 从而有进入不稳定状态的危险。然而,某些对象具有内部状态,即使在读的时候内部状态也会被改变(比如某些对象有内部缓冲)。假如两个线程去读取这种对象,问题仍然会产生,除非该对象的读操作设计已经采用了合适的多线程处理方法。

       

    • 通常认为更新两个相互独立的对象时,即使它们的类型相同也不会有问题。一般假设相互独立的对象之间是互不相关的,一个对象的不稳定状态并不会对另一个对象产生影响。然而,一些对象在内部是存在数据共享的(如静态的类数据,全局数据等),这使它们即使看上去没有什么逻辑上的关系,内部却依然存在联系。这种情况下,修改两个“相互独立”的对象怎么都会产生问题。考虑下面的情况:
        void f( ) 
        { 
        		std::string x;
        		
        		// Modify x.
        
        }
        
        void g( )
        {
        		std::string y;
        		
        		// Modify y.
        
        }
      
      如果一个线程运行函数f()修改x,另一个线程运行函数g()修改y,这种情况下会出现问题吗?大部分人认为这是两个明显独立的对象,可以被同时改动而不会导致任何问题。但是,有一种可能性就是,两个对象内部存在共享数据,这完全依赖于std::string的实现,如果那样的话,同时改动便是有问题的了。实际上,如果两个对象内部存在共享数据的话,即使一个函数仅仅是读取对象,问题依然存在,因为改动另一个对象的函数可能触及内部的共享数据。

       

    • 通常认为即便使用一个通用的资源池,获取资源的函数也不存在线程安全性的问题。例如:
        void f()
        {
        		char *p = new char[512];
        		
        		// use the array p
        		
        }
        
        void g()
        {
        		char *p = new char[512];
        		
        		// use the array p
        		
        }
      
      如果一个线程执行函数f(),另一个线程执行函数g(),两个线程可能同时使用操作符new去分配内存。在多线程环境中,只有假设new操作符的实现已经考虑到这种情况,从同一个内存池中获取内存也设计了正确的处理方法,才可以认为安全性得到保证。实际中,new操作符在内部确实会同步这些线程,因而每次调用都能够得到独立的内存分配而不损坏共享的内存池。与此类似的操作还有文件的打开、网络连接的发起以及其他资源的分配。

       

    • 人们一般认为会引起问题的情形是一个线程要访问(读或更新)某对象时另一个线程正在更新它。全局对象尤其易于出现这种问题,局部对象出现问题的情况则少的多。比如:
        std::string x;
        
        void f()
        {
        		std::string y;
        		
        		// modify x and y.
        }
      
      如果两个线程同时进入函数f(),它们拿到的对象y是不相同的,这是由于不同的线程拥有各自不同的栈,局部变量都在线程自己的栈上分配,因而每个线程都会拿到自己独立的局部变量副本。所以说,在f()中对y进行操作不会产生问题(假定操作独立的对象有安全保证),然而,全局对象x仅有一份两个线程都会触及的拷贝,对x的如上操作便会产生问题。局部变量也不是完全不会产生问题,因为每个函数都能够启动新的线程并且把局部变量的指针作为该线程的一个输入参数,比如:
        void f ( )
        {
        		std : : string x ;
        		startthread (somefunction , &x);
        		startthread (somefunction , &x);
        }
      
      这里假设有一个名为startthread的库函数,它有两个参数,一个是线程入口函数的指针,一个是线程入口函数的参数的指针。在此情况下我们调用startthread启动两个线程,并且把x对象作为参数同时传给两个线程。如果somefunction()中会对x进行修改,则两个线程可能修改同一个对象,类似的问题便产生了。注意,这种情况是特别隐蔽的,因为somefunction()并没有什么特别的理由去考虑两次调用会传给它同一个对象,因而它不大可能做出应付这种情况的保护措施。

    3. 编写具备线程安全性的代码

    理论上讲,控制线程行为的唯一办法是使用同步原语,如互斥量和信号量。在某些语言中,线程属于语言本身的一部分,特定的同步原语也由语言本身来提供;此外还有其他的情形,比如在C语言中,同步原语是象POSIX API那样的库函数,和操作系统相关联。

    一般,所写的代码应该满足人们对线程安全性最通常的考量。要实现一个类,就要确保能够安全地同时对一个对象进行读操作,如果打算在调用者不可见的情况下更新内部数据,则可能要自己来保护这种更新行为。另外,要保证对相互独立的对象同时进行写操作时的安全性,若使用了共享数据,则可能要自己来保护对共享数据的更新。最后,如果要写一个为多个线程管理一个通用资源池中的共享资源的函数,可能得保护共享资源不会因为多个同时的资源请求而被损坏。但是,总的来说,可能没有什么必要去考虑保护单个对象的同时更新,这方面交给调用者来做好了,因为就运行时效率来讲,达到这种绝对的安全性通常需要付出很高的代价,而且一般没有必要,也不是很恰当。

    4. 异常安全性与线程安全性的对比

    线程安全性和异常安全性具有大量的共同点:都和处于不稳定状态的对象有关;都必须考虑资源问题(尽管考虑的方面不同,异常的安全与资源泄露有关,线程的安全与资源损坏有关);都有若干安全性级别,每个安全级别对应一种普遍预期,认为某些情况安全而某些情况不安全。

    然而,异常的安全和线程的安全有一个重要的不同之处:异常的发生是与程序执行同步的,而线程则是异步的,换句话讲,理论上异常只发生在某些特定的时刻,尽管我们并不是很清楚哪个操作会抛出异常哪个不会,但我们能够精确给出什么时候可能发生异常而什么时候不可能发生。因而常常可以通过找出异常来保证一个函数的异常安全性。相比较而言,却是无法控制两个线程何时发生冲突的,通过重新组织函数来确保它的线程安全性收效甚微。这点不同之处导致线程相关的错误很难再现且尤其不好管理。

    5. 多线程程序设计原则

    这部分打算给出一些构造多线程程序时需要注意的“拇指规则”。尽管使用多线程能够灵活而自然地解决一些程序设计中的问题,但也会引入竞争条件(race conditions)和其他不可思议而难以调试的问题,通过遵循以下一些规则可以避免许多这样的问题。

    5.1. 共享数据

    如前所述,两个线程访问同一个数据对象是有潜在问题的。通常,修改一个对象需要好几个步骤,在执行这些步骤的过程中对象一般会进入不正常的状态,若此时另一个线程来访问对象,它有可能会拿到损坏的信息,然后整个程序可能出现异常行为。这一点是必须避免的。

    5.1.1. 什么样的数据是共享的

    • 静态的数据(与程序生存期相同的数据)。包括全局数据和局部静态数据,其中局部静态数据的情况仅仅指在两个(或多个)线程同时执行一个含有局部静态数据的函数时。
    • 地址由静态变量保存的动态分配数据。例如, 一个函数使用malloc()或new分配了一个对象并将其地址置于一个可以为多个线程访问的变量中,则这个动态分配的对象也可以被多个线程访问了。
    • 类的成员变量。当一个类的实例的成员函数被不同的线程调用时需要考虑这一情况。

    5.1.2. 什么样的数据不是共享的

    • 局部变量。即使两个线程调用同一个函数,它们拿到的该函数的局部变量也是不同的拷贝,因为局部变量保存在栈上,而每个线程拥有独立的栈。
    • 函数参数。像C这样的编程语言,函数参数也是放在栈上的,每个线程同样拿到的是属于它自己的拷贝。

    由于局部变量和函数参数不是共享的,它们不受竞争条件制约,因而尽可能使用局部变量和函数参数,避免使用全局变量。

    5.1.3. 哪种并发访问会导致问题

    • 如果多个线程仅仅读取某个对象的话应该是没有问题的。但是,需要注意的是,对于某些复杂的对象,常常尽管从外部看来只是读取,但也同时对内部信息进行了更新。某些对象即使针对读操作也会维护一个缓冲或在内部跟踪统计使用率。对于此类对象的并发读取是不安全的。
    • 如果在一个线程写某对象的同时另一个线程访问一个完全独立的对象,这种情况下不会出现问题。然而需要提醒的是:许多函数和对象会在内部共享数据,表面上完全独立的两个对象实际可能使用一个相同的数据结构,这对外部却是不可见的。
    • 某些类型的对象是以不允许中断的方式更新的,对于此类对象的并发读和写都是安全的,因为在更新时此类对象是不可能进入不稳定状态的。这样的更新称作原子操作,可惜支持原子操作的数据类型通常非常简单(如int),而且没有什么好的办法可以确切的知道究竟什么类型支持原子操作。为此C标准提供了一个sig_atomic_t的类型,定义在<signal.h>中,是某种整型。对声明为volatile sig_atomic_t的对象进行并发访问是安全的,这种情况下不需要互斥量。

    5.2. 一般准则

    如果一个函数在其文档中没有特别注明具备线程安全性,则应该认为它不具备。许多库大量使用了内部的静态数据,除非它是为多线程应用所设计,否则要牢记其内部数据可能没有利用互斥量进行适当的保护。类似,如果类的成员函数在其文档中没有特别注明对于多线程应用是安全的话,则认为它不安全。两个线程去操作相同的对象会引起问题,这是显而易见的,然而,即使两个线程去操作不同的物体依然会引起问题。出于多种原因,许多类使用了内部静态数据或者在多个看上去明显不同的对象间共享实现细则,

    以下给出几个一般准则:

    • 操作系统提供的API具备线程安全性
    • POSIX线程标准要求C标准库中的大多数函数具备线程安全性,少数例外会在C标准中注明。
    • 对于Windows提供的C标准库,如果所使用的版本没有问题,而且进行了正确的初始化,他们都是安全的。
    • C++标准库的线程安全性不是很明确,它在很大程度上依赖于使用的编译器。标准模板库线程安全性的SGI准则作为实际中的标准取得很大进展,但并不是统一的标准。

    如果在一个函数中使用了不具备线程安全性的函数,那么这个函数也就跟着不具备线程安全性了,只是,如果确保一个函数不会被两个或更多的线程同时调用的话,也可以在多线程程序中使用不具备线程安全性的函数。我们可以选择只在一个线程中使用不具备线程安全性的函数,也可以选择使用互斥量来保护对这些函数的调用。要记住好多函数与其它函数在内部共享数据,如果要使用互斥量来保护对一个不安全的函数的调用,也必须使用同一个互斥量来保护所有与该函数有关的其它函数,做到这一点往往是很困难的。

     
    展开全文
  • 线程安全性详解(原子性、可见性、有序性)

    千次阅读 多人点赞 2019-10-22 10:00:52
    一、定义:什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用 何种调度方式 或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类...

    一、定义:什么是线程安全性

    当多个线程访问某个类时,不管运行时环境采用 何种调度方式 或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类就是线程安全的。

    二、线程安全性的三个体现

    • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock)
    • 可见性:一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)
    • 有序性:如果两个线程不能从 happens-before原则 观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(happens-before原则)

    三、线程安全性:原子性

    3.1、原子性 — Atomic包

    在Java jdk中里面提供了很多Atomic类

    • AtomicXXX:CAS、Unsafe.compareAndSwapInt
    • AtomicLong、LongAdder
    • AtomicReference、AtomicReferenceFieldUpdater
    • AtomicStampReference:CAS的ABA问题

    由于CAS原语的直接操作与计算机底层的联系很大,CAS原语有三个参数,内存地址期望值新值。我们在Java中一般不去直接写CAS相关的代码,JDK为我们封装在AtomicXXX中,因此,我们直接使用就可以了。

    我们在 java.util.concurrent.atomic目录中可以看到我们这些类,包下提供了AtomicBooleanAtomicLongAtomicInteger三种原子更新基本类型和一个比较好玩的类AtomicReference,这些类都有一个共同点,都支持CAS,以AtomicInteger为重点讲解。
    在这里插入图片描述

    3.1.1、AtomicInteger

    AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减

    以下是AtomicIntege基本包括的方法:

    public final int getAndSet(int newValue)       //给AtomicInteger设置newValue并返回加oldValue
    public final boolean compareAndSet(int expect, int update)    //如果输入的值和期望值相等就set并返回true/false
    public final int getAndIncrement()     //对AtomicInteger原子的加1并返回当前自增前的value
    public final int getAndDecrement()   //对AtomicInteger原子的减1并返回自减之前的的value
    public final int getAndAdd(int delta)   //对AtomicInteger原子的加上delta值并返加之前的value
    public final int incrementAndGet()   //对AtomicInteger原子的加1并返回加1后的值
    public final int decrementAndGet()    //对AtomicInteger原子的减1并返回减1后的值
    public final int addAndGet(int delta)   //给AtomicInteger原子的加上指定的delta值并返回加后的值
    

    示例:

    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.atomic.AtomicInteger;
    
    @Slf4j
    public class AtomicIntegerExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static AtomicInteger count = new AtomicInteger(0);
    
        public static void main(String[] args) throws Exception {
        	//获取线程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            //定义信号量
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count.get());
        }
    
        private static void add() {
            count.incrementAndGet();
        }
    }
    
    

    这里我们使用请求总数为:5000,同时执行的并发线程数为:200,我们最终需要得到结果为:5000,这个执行结果才算正确。

    查看返回结果:

    13:43:26.473 [main] INFO com.mmall.concurrency.example.atomic.AtomicIntegerExample - count:5000
    

    最后结果是 5000表示是线程安全的。

    我们来看看 AtomicInteger底层代码中到底为我们做了什么?首先我们来看 AtomicInteger.incrementAndGet()方法

    public class AtomicInteger extends Number implements java.io.Serializable{
    /**
         *  对AtomicInteger原子的加1并返回加1后的值
         * @return 更新的值
         */
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
    }
    

    AtomicInteger调用了java底层的 unsafe.getAndAddInt()方法,这里是实现CAS 的关键。

    incrementAndGet()是将自增后的值返回,还有一个方法getAndIncrement()是将自增前的值返回,分别对应++ii++操作。同样的decrementAndGet()getAndDecrement()则对--ii--操作。

    Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于
    Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作
    能力方面起了很大的作用。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。
    过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。通常我们最好也不
    要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。
    

    再来看 Unsafe.getAndAddInt()方法

    	/*
    	 * 其中getIntVolatile和compareAndSwapInt都是native方法
    	 * getIntVolatile是获取当前的期望值
    	 * compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值
    	 */
        public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
            return var5;
        }
        
        public native int getIntVolatile(Object var1, long var2);
        public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
           
    

    我们可以看到getAndAddInt(Object var1, long var2, int var4),传进来的第一个参数是当前的一个对象,也就是我们的:count.incrementAndGet(),在getAndAddInt()中,var1就是count,var2就是当前的值,比如当前循环中count值为 2,var4为每次递增1

    其次getAndAddInt()方法中涉及到的两个方法调用都定义为native,即java底层实现的本地方法

    • getIntVolatile():获取保存当前对象count的主存地址的引用(注意不是对象的值,是引用)。
    • compareAndSwapInt():比较当前对象的值和底层该对象的值是否相等,如果相等,则将当前对象值加1,如果不相等,则重新去获取底层该对象的值,这个方法的实现就是CPU的CAS(compare and swap)操作。

    我们知道volatile具有一致性的特征,但是它不具备原子性,为什么AtomicInteger却同时具备一致性和原子性,原来在AtomicInteger源码中实现了这样一串代码:private volatile int value;,在AtomicInteger内部实现就使用了volatile关键字,这就是为什么执行CAS(对CAS有疑问的点击此处)操作的时候,从底层获取的数据就是最新的数据:

    如果当前要保存的值和内存中最新的值不相等的话,说明在这个过程中被其他线程修改了,只
    能获取更新当前值为最新值,再那这个当前值再去和重新去内存获取的最新值比较,直到二者
    相等的时候,才完成+1的过程.
    

    使用AtomicInteger的好处在于,它不同于sychronized关键字或lock用锁的形式来实现原子性,加锁会影响性能,而是采用循环比较的形式来提高性能。

    3.1.2、AtomicLong

    AtomicLong是作用是对长整形进行原子操作,依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。

    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.atomic.AtomicLong;
    
    @Slf4j
    public class AtomicLongExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static AtomicLong count = new AtomicLong(0);
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count.get());
        }
    
        private static void add() {
            count.incrementAndGet();
            // count.getAndIncrement();
        }
    }
    
    

    执行结果:

    14:59:38.978 [main] INFO com.mmall.concurrency.example.atomic.AtomicLongExample - count:5000
    

    最后结果是 5000表示是线程安全的。

    3.1.3、AtomicBoolean

    AtomicBoolean位于java.util.concurrent.atomic包下,是java提供给的可以保证数据的原子性操作的一个类

    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    @Slf4j
    public class AtomicBooleanExample {
    
        private static AtomicBoolean isHappened = new AtomicBoolean(false);
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        test();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("isHappened:{}", isHappened.get());
        }
    
        private static void test() {
            if (isHappened.compareAndSet(false, true)) {
                log.info("execute");
            }
        }
    }
    
    

    返回结果:

    15:04:54.954 [pool-1-thread-2] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - execute
    15:04:54.971 [main] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - isHappened:true
    

    这里我们发现log.info("execute");,在代码中只执行了一次,并且isHappened:true的值为true,这是为啥呢?

    这里是因为当程序第一次compareAndSet()的时候,使得isHappend变为了true,因为原子性的关系,没有其他线程进行干扰,通过使用AtomicBoolean,我们使某段代码只执行一次。

    3.1.4、AtomicReference

    AtomicReferenceAtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

    多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏,要
    保持状态的一致性,就需要在单个原子操作中更新相关的状态变量。
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.atomic.AtomicReference;
    
    @Slf4j
    public class AtomicReferenceExample {
    
        private static AtomicReference<Integer> count = new AtomicReference<>(0);
    
        public static void main(String[] args) {
            count.compareAndSet(0, 2); 
            count.compareAndSet(0, 1);
            count.compareAndSet(1, 3); 
            count.compareAndSet(2, 4); 
            count.compareAndSet(3, 5); 
            log.info("count:{}", count.get());
        }
    }
    
    

    大家觉得我们输出的结果会是多少?

    返回结果:

    15:26:59.680 [main] INFO com.mmall.concurrency.example.atomic.AtomicReferenceExample - count:4
    

    为什么是4呢?
    首先我们 要说的是public final boolean compareAndSet(V expect, V update)这个方法,这个方法主要的作用是通过比对两个对象,然后更新为新的对象,这里的比对两个对象,比对的方式不是equals而是==,意味着比对的是内存的中地址。

    1、首先我们创建count的初始化为0
    2、在main方法中 count.compareAndSet(0, 2);,判断count为0时赋值为2
    3、在count.compareAndSet(0, 1);count.compareAndSet(1, 3);判断count是否为1或者0,因为上一步我们已经赋值为2了,所以判断不成立
    4、在count.compareAndSet(2, 4);判断count是否为2,等式成立
    5、最好输出结果为4

    count.compareAndSet(0, 2); //count=0?赋值 2,判断成立,此时count=0,更新后为2
    count.compareAndSet(0, 1); //count=0?赋值 1,判断不成立,此时count=2
    count.compareAndSet(1, 3); //count=1?赋值 3,判断不成立,此时count=2
    count.compareAndSet(2, 4); //count=2?赋值 4,判断成立,此时count=2,更新后count=4
    count.compareAndSet(3, 5); //count=3?赋值 5,判断不成立,此时count=4
    

    所以我们输出结果为:4

    3.1.5、CAS中ABA问题的解决

    CAS并非完美的,它会导致ABA问题,例如:当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。
    如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。
    在JDK中提供了AtomicStampedReference类来解决这个问题,这个类维护了一个int类型的标记stamp,每次更新数据的时候顺带更新一下stamp。

    3.2、原子性 — synchronized

    synchronized是一种同步锁,通过锁实现原子操作。
    1、修饰代码块:大括号括起来的代码,作用于调用的对象
    2、修饰方法:整个方法,作用于调用的对象
    3、修饰静态方法:整个静态方法,作用于所有对象
    4、修饰类:括号括起来的部分,作用于所有对象

    详细可以查看,我写的关于:synchronized的博客,因为写过所以就不做过多描述。

    3.3、原子性 — 对比

    • Atomic:竞争激烈时能维持常态,比Lock性能好, 只能同步一个值
    • synchronized:不可中断锁,适合竞争不激烈,可读性好的情况
    • Lock:可中断锁,多样化同步,竞争激烈时能维持常态

    四、线程安全性:可见性

    简介:一个线程对主内存的修改可以及时被其他线程观察到

    导致共享变量在线程间不可见的原因:
    1.线程交叉执行
    2.重新排序结合线程交叉执行
    3.共享变量更新后的值没有在工作内存中与主内存间及时更新

    4.1 可见性 — syncronized

    JMM关于syncronized的两条规定:

    • 线程解锁前,必须把共享变量的最新值刷新到主内存中
    • 线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁
      由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用

    4.2 可见性 — volatile

    通过加入内存屏障禁止重排序优化来实现可见性。
    具体实现过程:

    • volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
    • volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

    volatile不能保证操作的原子性,也就是不能保证线程安全性, 如果需要使用volatile必须满足以下两个条件:

    • 对变量的写操作不依赖与变量当前的值。
    • 该变量没有包含在具有其他变量的不变的式子中。

    所以volatile修饰的变量适合作为状态标记量。

    注:以下图片为资料中获取,如有雷同,纯属巧合
    在这里插入图片描述
    在这里插入图片描述

    示例:

    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class VolatileExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        public static volatile int count = 0;
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal ; i++) {
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("count:{}", count);
        }
    
        private static void add() {
            count++;
        }
    }
    

    返回结果:

    16:12:01.404 [main] INFO com.mmall.concurrency.example.count.VolatileExample4 - count:4986
    

    通过执行代码我们可以发现,返回结果并不是我们想看到的5000,说明这个是线程不安全的类

    主要是因为当我们执行conut++时分成了三步:
    1、取出当前内存count值,这时count值时最新的
    2、+1操作
    3、重新写回主存

    例如:有两个线程同时在执行count++,两个内存都执行了第一步,比如当前count值为99,它们都读到了这个count值,然后两个线程分别执行了+1,并写回主存,这样就丢掉了一次+1的操作。

    五、线程安全性:有序性

    • 在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
    • 通过volatile、synchronized、lock保证有序性

    5.1 happens-before原则

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作,也就是说只有先解锁才能对下面的线程进行加锁
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C
    • 线程启动规则Thread对象start()方法先行发生于此线程的每一个动作,一个线程只有执行了start()方法后才能做其他的操作
    • 线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生)
    • 线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

    六、线程安全性:总结

    • 原子性:Atomic包、CAS算法、synchronized、Lock
      原子性做了互斥方法,同一个线程只能有一个进行操作
    • 可见性:synchronized、volatile
      一个主内存的线程如果进行了修改,可以及时被其他线程观察到,介绍了volatile如何被观察到的
    • 有序性:happens-before原则
      happens-before原则,观察结果,如果两个线程不能偶从happens-before原则观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序
    展开全文
  • 本文定义了线程安全性,类要成为线程安全的,首先必须在单线程环境中有正确的行为。并在被多个线程访问时,不管运行时环境执行这些线程有什么样的时序安排,它必须有如上所述的正确行为,并且在调用的代码中没有任何...
  • 线程安全性

    2017-01-17 17:24:57
    多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类始终都能变现出正常的行为,那么就称这个类是线程安全的。 要编写线程安全...
  • shared_ptr线程安全性分析

    万次阅读 多人点赞 2012-12-13 22:34:40
    本文基于shared_ptr的源代码,提取了shared_ptr的类图和对象图,然后分析了shared_ptr如何保证文档所宣称的线程安全性。本文的分析基于boost 1.52版本,编译器是VC 2010。 shared_ptr的线程安全性 boost官方文档对...
  • Java 线程安全性(thread-safe)

    千次阅读 2016-04-06 08:52:30
    《Java并发编程实践》学习笔记之二:线程安全性(thread-safe)   1、什么是线程安全性   1.1 不可用状态   调用一个函数(假设该函数是正确的)操作某对象常常会使该对象暂时陷入不可用的状态(通常称为不稳定...
  • 索引文件的并发访问和线程安全性

    千次阅读 2014-12-15 15:21:08
    通常,Lucene的初学者们对Lucene.net索引文件的并发访问、IndexReader和IndexWriter的线程安全性存在一定的误解。而准确地理解这些内容是十分重要的。此文简单的论述下这两个问题。  并发访问的规则  Lucene提供...
  • 线程的相关讨论--(4)描绘线程安全性

    千次阅读 2007-11-15 14:02:00
    7月份我们的并发专家 Brian Goetz 将 Hashtable 和 ...但是,正如Brian 在本月的 Java 理论与实践中解释的,尽量在 Javadoc 中对类的线程安全性进行归类是非常重要的。请在附带的 讨论论坛中与作者和其他读者分享您关
  • hbase之htable线程安全性 HTablePool

    千次阅读 2012-08-03 14:30:47
    在单线程环境下使用hbase的htable是没有问题,但是突然高并发多线程情况下就出现问题了,然后细看htable的api说明   Java代码  * This class is not thread safe for updates; the underlying write buffer...
  • 线程安全

    千次阅读 2014-03-12 11:36:20
    线程安全
  • 劣质翻译系列 由于本人英文能力实在有限,不足之初敬请谅解,希望大家落脚同时能指出不足。...android 进程与线程 - 开发文档翻译 - 线程 其他系列的翻译 android activity开发文档翻译 - 1 - 基础篇
  • java中Writer的线程安全性

    千次阅读 2014-04-16 16:54:01
    以前负责一个项目,我负责从一个超大的文本文件中读取信息存入数据库再...与产生日志的部门沟通,他们说是多线程使用log4j写入,可能偶尔会有串行。 具体他们是否使用log4j的AsyncAppender我不太了解,暂时也没去看l
  • 纵观Qt文档,用“可重入”和“线程安全”来标识类和函数以彰显它们在多线程应用程序中是如何被使用的:  线程安全:一个线程安全的函数可以同时被多个线程调用,尽管这些调用使用了共享数据,因为所有指向共享数据...
  • Java 理论与实践: 描绘线程安全性

    千次阅读 2005-01-06 16:53:00
    线程安全不是一个非真即假的命题级别:中级Brian Goetz (brian@quiotix.com)首席顾问,Quiotix Corp2003年 11 ...一个类难道不是线程安全就是线程不安全的吗?不幸的是,线程安全并不是一个非真即假的命题,它的定义出
  • boost::shared_ptr的线程安全性

    千次阅读 2011-01-19 11:36:00
    boost::shared_ptr的win32实现中,没有使用类似mutex机制却能够实现线程安全。 <br /> 线程安全主要就是保证引用计数机制的线程安全 <br /> win32实现中关键在于使用了 BOOST_INTERLOCKED_...
  • SimpleDateFormat线程安全性 由于SimpleDateFormat比较常用,而且在一般情况下,一个应用中的时间显示模式都是一样的,所以很多人愿意使用如下方式定义SimpleDateFormat: public class Main { private static ...
  • 线程安全与线程不安全的理解

    千次阅读 2016-06-21 20:14:57
    线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据...
  • 1.关键点 PHP的线程安全与非线程安全版本,【暂时可认为】只存在与Windows版本中,即常是我们的本地开发环境【如果您使用mac,省略这句】,*nix中则不... 什么是线程安全? Thread Safety means that ...
  • 最新的详细测试 https://www.cnblogs.com/shangxiaofei/p/10465031.html ... String 字符串常量 StringBuffer 字符串变量(线程安全) StringBuilder 字符串变量(非线程安全) 简要的说, String 类型...
  • iOS线程安全问题

    千次阅读 2016-04-08 09:52:10
    iOS线程安全相关问题研究
  • QT的可重入线程安全

    千次阅读 2016-05-27 22:50:36
    翻译了QT官网上对于可重入和线程安全的解释。
  • ConcurrentHashMap是如何保证线程安全

    千次阅读 2019-09-24 09:55:40
    ConcurrentHashMap是如何保证线程安全的 之前分析过HashMap的一些实现细节,关于HashMap你需要知道的一些细节, 今天我们从源码角度来看看ConcurrentHashMap是如何实现线程安全的,其实网上这类文章分析特别多,秉着...
  • java 线程安全和不安全

    万次阅读 2016-07-24 11:30:41
    线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。(Vector,HashTab;le) 线程不...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 153,053
精华内容 61,221
关键字:

线程安全性文档作用