A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

一.        线程安全等级
线程安全,一般常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个“非黑即白”的单项选择题。按照“线程安全”的安全程度由弱到强来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立。

1:不可变
                在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要在采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。
2:绝对线程安全
                绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。
3:相对线程安全
                相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。
                它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性
                在java语言中,大部分的线程安全类都是属于相对线程安全的,例如Vector,HashTable,Collections的synchronizedCollection()方法保证的集合。
4:线程兼容
                线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
                线程兼容是指对象本身并不是线程安全的,但是可以通过再调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如前面的Vector和hashTable相对应的集合类ArrayList和HashMap等。
5:线程对立
                线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。
                一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因为如此,这两个方法已经被废弃了。
二:线程安全的实现方法
        保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
        1:互斥同步
                互斥同步是最常见的一种并发正确性保证手段。同步是指在多线程并发访问共享数据
                时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区,互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
                        在java中,最基本的互斥手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
                        此外,reentrantlock也是通过互斥来实现同步。在基本用法上,reentrantlock与synchronized很相似,他们都具备一样的线性重入特性。
                        互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于属于一种悲观的并发策略。总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
                2:非阻塞同步
                        随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要吧线程挂起,因此这种同步操作称为非阻塞同步。
                        非阻塞的实现CAS:CAS指令需要有三个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示),旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧逾期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V值,都会返回V的旧值,上述的处理过程是一个原子操作。

                CAS缺点:
                        ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来为A,变成了B,又变成了A,那么使用CAS进行检查时就会发现它的值没有发生变化,但是实际上却变化了。
                        ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A—B—A,就变成了1A—2B—3C。JDK的atomic包里提供了一个类AtomicStampendReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是够等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
                3:无需同步方案
                        要保证线程安全,并不一定就要进行同步,两者之间没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1)        可重入代码
2)        线程本地存储

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马