aas 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。(线程安全的回答)
asdsadasdasdasdsadasdasdasdsadassdasdsasdsadsdasdasdsadasdasdsadasdas————《Java虚拟机规范》
按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:
aas
aaasdass①、不可变 ②、绝对线程安全 ③、相对线程安全 ④、线程兼容 ⑤、线程对立
aas
不可变:
aaasdass不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。
aa
aaasdass【注】:
aaasdasadss①、“final关键字带来的可见性”时曾经提到过这一点:只要一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态
aaasdasadss②、Java语言中,如果多线程共享的数据是一个 基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的(对于对象,还没有相应的支持方式,API中符合不可变要求的类型有String之外,常用的还有枚举类型及java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。但同为Number子类型的原子类AtomicInteger和AtomicLong则是可变的)。
aaasdasadss③、“不可变”带来的安全性是最直接、最纯粹的。
aas
绝对线程安全:
aaasdass绝对的线程安全能够完全满足我们文章开头定义的线程安全概念(也就是说更加严格 )。(调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,甚至不切实际的代价)
aa
aaasdass【注】:
aaasdasadss①、在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
aaasdasadss②、比如java.util.Vector
是一个线程安全的容器(因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高,但保证了具备原子性、可见性和有序性。),但是在多线程环境中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全(不是绝对线程安全)的。(假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。)
aas
相对线程安全 :
aaasdass相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
aa
aaasdass【注】:在Java语言中,大部分声称线程安全的类都属于这种类型,例如:Vector、HashTable、Collections的synchronizedCollection()方法包装的集合等。
aas
线程兼容:
aaasdass线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
aa
aaasdass【注】:我们平常说一个类不是线程安全的,通常就是指这种情况。Java类库API中大部分的类都是线程兼容的。
aas
线程对立:
aaasdass线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
aa
aaasdass【注】:Thread类的suspend()和resume()方法,调用这两个方法的线程存在很大的死锁风险,如今已经废弃。
线程安全的实现方法:
aas
互斥同步:
aaasdass互斥同步是一种最常见也是最主要的并发正确性保障手段。
aaasdass互斥是实现同步的一种手段,临界区、互斥量和信号量 都是常见的互斥实实现方式
aaasdass互斥是因,同步是果;互斥是方法,同步是目的。
aa
aaasdass【注】:最基本的互斥同步手段就是synchronized关键字、J.U.C包、重入锁ReentrantLock。
aas
aaasdaasdsss①、等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
aas
aaasdaasdsss②、公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
aaasdaasdsss③、指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可。
aas
非阻塞同步:
aaasdass互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步。
aaasd
aaasdass互斥同步属于一种悲观的并发策略,而随着硬件指令集的发展,我们已经有了另外一个选择:基于冲突检测的乐观并发策略。通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步,使用这种措施的代码也常被称为无锁编程。
aa
aaasdass【注】:
aaasdass①、为什么要说随着硬件指令集的发展? 因为我们必须 要求操作和冲突检测 这两个步骤具备原子性。
aaasdass②、靠什么来保证原子性?这里再使用互斥同步来保证就完全失去意义了,所以我们只能靠硬件来实现这件事情,硬件保证某些从语义上看起来需要多次操作的行为可以 只通过一条处理器指令 就能完成。
aaasdass常用的指令:
aaassaddass⒈、测试并设置(Test-and-Set)
aaassaddass⒉、获取并增加(Fetch-and-Increment)
aaassaddass⒊、交换(Swap)
aaassaddass⒋、比较并交换(CAS);
aaassaddass⒌、加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)。
aaasdass后面的两条是现代处理器新增的,功能也类似。(在IA64、x86指令集中有用 cmpxchg 指令完成的CAS功能,在SPARC-TSO中也有用casa指令实现的,而在ARM和PowerPC架构下,则需要使用一对ldrex/strex指令来完成LL/SC的功能。)
aas
无同步方案:
aaasdass要保证线程安全,也并非一定要进行阻塞或非阻塞同步,同步与线程安全两者没有必然的联系。
aaa
aaasdass同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。两类:
aaasdsaasda①、可重入代码: 又称纯代码,指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
aaasdass【注】:
aaasasdsadass⒈所有可重入的代码都是线程安全的,但并非所有的线程安全的代码都是可重入的。
aaasasdsadass⒉可重入代码有一些共同的特征:不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等
aaasasdsadass⒊如何判断代码具备可重入性?如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
aaasdsaasda②、线程本地存储 (ThreadLocal): 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
aaasdass【注】:可以通过java.lang.ThreadLocal
类来实现线程本地存储的功能,每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量(具体可以看高并发模块)。