介绍在上一篇文章中,我们讨论了线程安全的原因,主要是由于多线程对共享内存的操作导致可见性或顺序的破坏,导致内存一致性。错误。那么如何设计并发代码来解决这个问题呢?我们一般会用到这些方法:线程关闭不可变对象同步释放和转义在此之前,我们先了解一下释放和转义的概念。发布是指使一个对象在当前作用域之外可用,比如将一个对象的引用传递给另一个类的方法,在一个方法中返回对它的引用等。在很多情况下,我们需要保证内部对象不可用出版。发布一些内部状态可能会破坏封装,允许用户随意改变它的状态,从而破坏线程安全。在某些情况下,我们需要发布一些内部对象,如果需要线程安全,则需要适当的同步。当一个对象在不该释放的时候被释放,这种情况称为逃逸。publicclassEscape{privateListusers=Lists.newArrayList();publicListgetUsers(){returnusers;}publicvoidsetUsers(Listusers){this.users=users;}}getUsers已经逃脱了它的角色字段,这个私有变量被发布,因为任何调用者都可以修改数组。同时,释放用户时,也间接释放了对User对象的引用。publicclassOuterEscape{privateStringstr="Outer'sstring";publicclassInner{publicvoidwrite(){System.out.println(OuterEscape.this.str);}}publicstaticvoidmain(String[]args){OuterEscapeout=newOuterEscape();OuterEscape.Innerin=out.newInner();in.write();}}在内部类中存储了对创建内部类的外部类的引用,因此内部类可以使用创建内部类的外部类的私有属性和方法。publicclassConstructorEscape{privateThreadt;publicConstructorEscape(){System.out.println(this);t=newThread(){publicvoidrun(){System.out.println(ConstructorEscape.this);}};t.start();}publicstaticvoidmain(String[]args){ConstructorEscapea=newConstructorEscape();}}这个引用是线程t共享的,所以线程t的释放会导致ConstructorEscape对象的释放。总结如何安全释放的步骤找出构成对象状态的所有变量找出约束状态变量的不变性条件建立对象状态的并发访问策略因为访问共享变量,如果能避免操作shared变量和每个线程访问自己的变量,不会有线程安全问题。这是实现线程安全的最简单方法。线程控制转义规则可以帮助你判断代码中对某些资源的访问是否是线程安全的。如果一个资源的创建、使用和销毁都在同一个线程中进行,并且永远不会脱离线程的控制,那么该资源的使用就是线程安全的。资源可以是对象、数组、文件、数据库连接、套接字等。在Java中,不需要主动销毁对象,所以“销毁”意味着不再有对对象的引用。即使对象本身是线程安全的,如果对象包含其他资源(文件、数据库连接),整个应用程序可能不再是线程安全的。例如,两个线程创建了自己的数据库连接。每个连接本身都是线程安全的,但它们所连接的同一个数据库可能不是线程安全的。下面看一下线程闭包的几种实现方式:栈闭包stackclosure是线程闭包的一种特例。在栈闭包中,只能通过局部变量访问对象,局部变量存放在线程自己的栈中。也就是说,局部变量永远不会被多个线程共享。因此,基本类型的局部变量是线程安全的。对象局部引用与底层类型的局部变量不同。尽管引用本身不是共享的,但它引用的对象并不存储在线程的堆栈中。所有对象都存储在共享堆中。如果在其中创建的对象不逃逸该方法,则该方法是线程安全的。事实上,即使这个对象作为参数传递给其他方法,只要其他线程无法获取到这个对象,它仍然是线程安全的。publicvoidsomeMethod(){LocalObjectlocalObject=newLocalObject();localObject.callMethod();method2(localObject);}publicvoidmethod2(LocalObjectlocalObject){localObject.setValue("value");}如上,LocalObject对象不返回方法,也不会传递给someMethod()方法之外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象并将其分配给localObject引用。所以,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使当LocalObject作为参数传递给同一个类的其他方法或其他类的方法时,它仍然是线程安全的。当然,如果通过一些方法将LocalObject传递给另一个线程,那么它就不再是线程安全的了。在特定线程上,这会降低此方法的可靠性。假设我们保证只有一个线程可以写入一个共享对象,那么这个对象的“读-修改-写”在任何情况下都不会出现竞争条件。如果我们给这个对象加上volatile修饰,就可以保证对象的可见性。任何线程都可以读取该对象,但只有一个线程可以写入它。这样,仅通过volatile修饰就可以很好地保证其安全性。与直接使用synchronized修改相比,虽然更合适,但实现起来稍微复杂一些。程序控制线程关闭不是一种具体的技术,而是一种设计思想。从设计的角度,把处理一个对象状态的所有代码都放在一个线程中,这样就避免了线程安全的问题。ThreadLocalThreadLocal机制本质上是程序控制线程的关闭,只是Java自己处理而已。再看Java的Thread类和ThreadLocal类:Thread线程类维护了一个ThreadLocalMap实例变量ThreadLocalMap是一个Map结构ThreadLocal的set方法获取当前线程,获取当前线程的threadLocalMap对象,然后以ThreadLocal对象为keyset中要放入的值作为value,放入MapThreadLocal的get方法中获取当前线程,获取当前线程的threadLocalMap对象,然后以ThreadLocal对象为key获取对应的valuepublicclassThreadimplementsRunnable{ThreadLocal.ThreadLocalMapthreadLocals=null;}publicclassThreadLocal{publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null)return(T)e.value;}returnsetInitialValue();}ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}}ThreadLocal的设计很简单,就是给线程对象设置一个内部Map,里面可以放一些数据。JVM从底层保证Thread对象不会看到对方的数据。使用ThreadLocal的前提是为每个ThreadLocal保存一个单独的对象。该对象不能在多个ThreadLocal之间共享。否则这个对象也是线程不安全的ThreadLocal内存泄漏。ThreadLocalMap使用ThreadLocal的弱引用作为key。如果一个ThreadLocal没有外部Strong引用引用它,那么在系统GC的时候,这个ThreadLocal势必会被回收,这样ThreadLocalMap中就会出现key为null的Entry,就没有办法访问到这些key为null的Entry的value,如果当前线程长时间没有结束,这些key为null的Entry的value总会有一个强引用链:ThreadRef->Thread->ThreaLocalMap->Entry->value永远无法回收,导致内存泄漏。其实在ThreadLocalMap的设计中就已经考虑到了这种情况,并增加了一些保护措施:当ThreadLocal的get()、set()、remove()时,会清除key为的线程ThreadLocalMap中的所有值无效的。所以每次使用ThreadLocal时,都会调用它的remove()方法来清除数据,从而避免这个问题。不可变对象如果一个对象在创建后不能被修改,则它被称为不可变对象。在并发编程中,一个普遍接受的原则是:尽可能使用不可变对象来创建简单可靠的代码。在并发编程中,不可变对象特别有用。由于创建后无法修改,因此不会出现操作共享变量导致的内存一致性错误,但程序员通常不热衷于使用不可变对象,因为他们担心每次创建新对象的开销。事实上,这种开销经常被高估,使用不可变对象带来的一些效率提升也抵消了这种开销。先来看一个使用同步解决线程安全的例子publicclassSynchronizedRGB{//Valuesmustbetween0and255.privateintred;privateintgreen;privateintblue;privateStringname;privatevoidcheck(intred,intgreen,intblue){if(red<0||red>255||green<0||green>255||blue<0||blue>255){thrownewIllegalArgumentException();}}publicSynchronizedRGB(intred,intgreen,intblue,Stringname){check(red,green,blue);this.red=red;this.green=green;this.blue=blue;this.name=name;}publicvoidset(intred,intgreen,intblue,Stringname){check(red,green,blue);synchronized(this){this.red=red;this.green=green;this.blue=blue;this.name=name;}}publicsynchronizedintgetRGB(){return((red<<16)|(green<<8)|blue);}publicsynchronizedStringgetName(){returnname;}}SynchronizedRGBcolor=newSynchronizedRGB(0,0,0,"PitchBlack");...intmyColorInt=color.getRGB();//1StringmyColorName=color.getName();//2//如果1执行后其他线程调用set方法,getName和getRGB的值将不匹配synchronized(color){我ntmyColorInt=color.getRGB();StringmyColorName=color.getName();}//需要让这两个语句同步执行。创建不可变对象的几个原则没有提供修改可变对象的方法(包括修改字段的方法和修改字段引用对象的方法)将类的所有字段定义为final和private。不允许子类重写方法。简单的方法是将类声明为final,更好的方法是将构造函数声明为private,通过工厂方法创建对象。如果类的字段是对可变对象的引用,则不允许修改引用的对象。不共享对可变对象的引用。当引用作为参数传递给构造函数,并且引用指向外部可变对象时,不能保存引用。如果需要保存,请创建可变对象的副本,然后保存对复制对象的引用。同理,如果需要返回内部变量对象,不要返回变量对象本身,而是返回其复制和修改后的例子if(red<0||red>255||green<0||green>255||blue<0||blue>255){thrownewIllegalArgumentException();}}publicImmutableRGB(intred,intgreen,intblue,Stringname){check(红,绿,蓝);this.red=red;this.green=green;this.blue=blue;this.name=name;}publicintgetRGB(){return((red<<16)|(green<<8)|blue);}publicStringgetName(){returnname;}}不可变对象如果对象本身是可变的,但在程序运行过程中没有变化的可能,则称为不可变对象。不需要额外的线程安全保护同步当我们不得不使用共享变量并且需要经常修改它们时,我们需要使用同步来实现线程安全。在Java中,我们可以使用Synchronized/LockvolatileCAS来实现同步。synchronized是排他锁,假设最坏的情况,只有在保证其他线程不会干扰的情况下才执行,导致所有其他需要锁的线程挂起,等待持有锁的线程释放锁。与锁相比,volatile变量是一种更轻量级的同步机制,因为使用这些变量时不会发生上下文切换、线程调度等操作,但volatile变量也有一些局限性:不能用于构建原子复合操作,所以a当变量依赖于旧值时,不能使用volatile变量。CAS是一种乐观锁。它不会每次都锁定,而是假设没有冲突来完成一个操作。如果由于冲突而失败,它将重试直到成功。同步解决了三个相互关联的问题:原子性:哪些指令必须是不可分割的可见性:一个线程执行的结果对另一个线程可见有序性:一个线程运行的结果对其他线程可见是一种无序的总结理解很重要线程安全的概念。所谓线程安全问题就是对象状态的处理问题。如果要处理的对象是无状态的(不变性),或者可以被多个线程共享(线程闭包),那么我们可以放心这个对象可能是线程安全的。当不可避免,必须共享对象状态以供多线程访问时,这时就会用到一系列的线程同步技术。这种理解被扩大到架构层面。我们在设计业务层代码的时候,业务层必须是无状态的,这样业务层才具有可扩展性,能够通过水平扩展平滑的应对高并发。所以我们可以从几个层面来处理线程安全:是否可以做成无状态的不变对象。无状态是最安全的。线程可以关闭吗?采用了什么样的同步技术(Synchronized/LockvolatiteCAS)