synchronized1、用法和基本原理1.1、实例方法1.2、静态方法1.3、代码块2、理解synchronized2.1、可重入性2.2、内存可见性2.3、死锁3、同步容器及其注意事项3.1、复合操作3.2、伪同步3.3、迭代3.4、并发容器1、用法和基本原理synchronized可以用于修饰类的实例方法、静态方法和代码块。1.1、实例方法publicclassCounter{privateintcount;publicsynchronizedvoidincr(){count;}publicsynchronizedintgetCount(){returncount;}}Counter是一个简单的计数器类incr方法和getCount方法都加了synchronized修饰。加了synchronized后方法内的代码就变成了原子操作当多个线程并发更新同一个Counter对象的时候也不会出现问题。使用的代码如代码所示。publicclassCounterThreadextendsThread{Countercounter;publicCounterThread(Countercounter){this.countercounter;}Overridepublicvoidrun(){for(inti0;i1000;i){counter.incr();}}publicstaticvoidmain(String[]args)throwsInterruptedException{intnum1000;CountercounternewCounter();Thread[]threadsnewThread[num];for(inti0;inum;i){threads[i]newCounterThread(counter);threads[i].start();}for(inti0;inum;i){threads[i].join();}System.out.println(counter.getCount());}}我们创建了1000个线程传递了相同的counter对象每个线程主要就是调用Counter的incr方法1000次main线程等待子线程结束后输出counter的值这次不论运行多少次结果都是正确的100万。这里synchronized到底做了什么呢看上去synchronized使得同时只能有一个线程执行实例方法但这个理解是不确切的。多个线程是可以同时执行同一个synchronized实例方法的只要它们访问的对象是不同的即可比如Countercounter1newCounter();Countercounter2newCounter();Threadt1newCounterThread(counter1);Threadt2newCounterThread(counter2);t1.start();t2.start();这里t1和t2两个线程是可以同时执行Counter的incr方法的因为它们访问的是不同的Counter对象一个是counter1另一个是counter2。所以synchronized实例方法实际保护的是同一个对象的方法调用确保同时只能有一个线程执行。再具体来说synchronized实例方法保护的是当前实例对象即this, this对象有一个锁和一个等待队列锁只能被一个线程持有其他试图获得同样锁的线程需要等待。执行synchronized实例方法的过程大致如下尝试获得锁如果能够获得锁继续下一步否则加入等待队列阻塞并等待唤醒。执行实例方法体代码。释放锁如果等待队列上有等待的线程从中取一个并唤醒如果有多个等待的线程唤醒哪一个是不一定的不保证公平性。当前线程不能获得锁的时候它会加入等待队列等待线程的状态会变为BLOCKED。synchronized保护的是对象而非代码只要访问的是同一个对象的synchronized方法即使是不同的代码也会被同步顺序访问。比如对于Counter中的两个实例方法getCount和incr对同一个Counter对象一个线程执行getCount另一个执行incr它们是不能同时执行的会被synchronized同步顺序执行。此外需要说明的是synchronized方法不能防止非synchronized方法被同时执行。比如如果给Counter类增加一个非synchronized方法publicvoiddecr(){count--;}则该方法可以和synchronized的incr方法同时执行这通常会出现非期望的结果所以一般在保护变量时需要在所有访问该变量的方法上加上synchronized。1.2、静态方法synchronized同样可以用于静态方法如代码所示。publicclassStaticCounter{privatestaticintcount0;publicstaticsynchronizedvoidincr(){count;}publicstaticsynchronizedintgetCount(){returncount;}}synchronized保护的是对象对实例方法保护的是当前实例对象this对静态方法保护的是哪个对象呢是类对象这里是StaticCounter.class。实际上每个对象都有一个锁和一个等待队列类对象也不例外。synchronized静态方法和synchronized实例方法保护的是不同的对象不同的两个线程可以一个执行synchronized静态方法另一个执行synchronized实例方法。1.3、代码块除了用于修饰方法外synchronized还可以用于包装代码块publicclassCounter{privateintcount;publicvoidincr(){synchronized(this){count;}}publicintgetCount(){synchronized(this){returncount;}}}synchronized括号里面的就是保护的对象对于实例方法就是this, {}里面是同步执行的代码。对于前面的StaticCounter类等价的代码如代码所示。publicclassStaticCounter{privatestaticintcount0;publicstaticvoidincr(){synchronized(StaticCounter.class){count;}}publicstaticintgetCount(){synchronized(StaticCounter.class){returncount;}}}synchronized同步的对象可以是任意对象任意对象都有一个锁和等待队列或者说任何对象都可以作为锁对象。比如Counter类的等价代码还可以如代码所示。publicclassCounter{privateintcount;privateObjectlocknewObject();publicvoidincr(){synchronized(lock){count;}}publicintgetCount(){synchronized(lock){returncount;}}}2、理解synchronized2.1、可重入性synchronized有一个重要的特征它是可重入的也就是说对同一个执行线程它在获得了锁之后在调用其他需要同样锁的代码时可以直接调用。比如在一个syn-chronized实例方法内可以直接调用其他synchronized实例方法。可重入是一个非常自然的属性应该是很容易理解的之所以强调是因为并不是所有锁都是可重入的后续章节我们会看到不可重入的锁。可重入是通过记录锁的持有线程和持有数量来实现的当调用被synchronized保护的代码时检查对象是否已被锁如果是再检查是否被当前线程锁定如果是增加持有数量如果不是被当前线程锁定才加入等待队列当释放锁时减少持有数量当数量变为0时才释放整个锁。2.2、内存可见性对于复杂一些的操作synchronized可以实现原子操作避免出现竞态条件但对于明显的本来就是原子的操作方法也需要加synchronized吗比如下面的开关类Switcher只有一个boolean变量on和对应的setter/getter方法publicclassSwitcher{privatebooleanon;publicbooleanisOn(){returnon;}publicvoidsetOn(booleanon){this.onon;}}当多线程同时访问同一个Switcher对象时会有问题吗没有竞态条件问题但正如上节所说有内存可见性问题而加上synchronized可以解决这个问题。synchronized除了保证原子操作外它还有一个重要的作用就是保证内存可见性在释放锁时所有写入都会写回内存而获得锁后都会从内存中读最新数据。不过如果只是为了保证内存可见性使用synchronized的成本有点高有一个更轻量级的方式那就是给变量加修饰符volatile如下所示publicclassSwitcher{privatevolatilebooleanon;publicbooleanisOn(){returnon;}publicvoidsetOn(booleanon){this.onon;}}加了volatile之后Java会在操作对应变量时插入特殊的指令保证读写到内存最新值而非缓存的值。2.3、死锁使用synchronized或者其他锁要注意死锁。所谓死锁就是类似这种现象比如有a、b两个线程a持有锁A在等待锁B而b持有锁B在等待锁A, a和b陷入了互相等待最后谁都执行不下去如代码所示。publicclassDeadLockDemo{privatestaticObjectlockAnewObject();privatestaticObjectlockBnewObject();privatestaticvoidstartThreadA(){ThreadaThreadnewThread(){Overridepublicvoidrun(){synchronized(lockA){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}};aThread.start();}privatestaticvoidstartThreadB(){ThreadbThreadnewThread(){Overridepublicvoidrun(){synchronized(lockB){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}}}};bThread.start();}publicstaticvoidmain(String[]args){startThreadA();startThreadB();}}运行后aThread和bThread陷入了相互等待。怎么解决呢首先应该尽量避免在持有一个锁的同时去申请另一个锁如果确实需要多个锁所有代码都应该按照相同的顺序去申请锁。比如对于上面的例子可以约定都先申请lockA再申请lockB。不过在复杂的项目代码中这种约定可能难以做到。还有一种方法是使用后续章节介绍的显式锁接口Lock它支持尝试获取锁tryLock和带时间限制的获取锁方法使用这些方法可以在获取不到锁的时候释放已经持有的锁然后再次尝试获取锁或干脆放弃以避免死锁。如果还是出现了死锁怎么办呢Java不会主动处理不过借助一些工具我们可以发现运行中的死锁比如Java自带的jstack命令会报告发现的死锁。对于上面的程序。3、同步容器及其注意事项我们知道类Collection中有一些方法可以返回线程安全的同步容器比如publicstaticTCollectionTsynchronizedCollection(CollectionTc)publicstaticTListTsynchronizedList(ListTlist)publicstaticK,VMapK,VsynchronizedMap(MapK,Vm)它们是给所有容器方法都加上synchronized来实现安全的比如Synchronized-Collection其部分代码如下所示staticclassSynchronizedCollectionEimplementsCollectionE{finalCollectionEc;//Backing CollectionfinalObjectmutex;//Object on which to synchronizeSynchronizedCollection(CollectionEc){if(cnull)thrownewNullPointerException();this.cc;mutexthis;}publicintsize(){synchronized(mutex){returnc.size();}}publicbooleanadd(Ee){synchronized(mutex){returnc.add(e);}}publicbooleanremove(Objecto){synchronized(mutex){returnc.remove(o);}}//…}这里线程安全针对的是容器对象指的是当多个线程并发访问同一个容器对象时不需要额外的同步操作也不会出现错误的结果。加了synchronized所有方法调用变成了原子操作客户端在调用时是不是就绝对安全了呢不是的至少有以下情况需要注意复合操作比如先检查再更新。伪同步。迭代。3.1、复合操作publicclassEnhancedMapK,V{MapK,Vmap;publicEnhancedMap(MapK,Vmap){this.mapCollections.synchronizedMap(map);}publicVputIfAbsent(Kkey,Vvalue){Voldmap.get(key);if(old!null){returnold;}returnmap.put(key,value);}publicVput(Kkey,Vvalue){returnmap.put(key,value);}//…}EnhancedMap是一个装饰类接受一个Map对象调用synchronizedMap转换为了同步容器对象map增加了一个方法putIfAbsent该方法只有在原Map中没有对应键的时候才添加在Java 8之后Map接口增加了putIfAbsent默认方法这是针对Java 8之前的Map接口演示概念​。map的每个方法都是安全的但这个复合方法putIfAbsent是安全的吗显然是否定的这是一个检查然后再更新的复合操作在多线程的情况下可能有多个线程都执行完了检查这一步都发现Map中没有对应的键然后就会都调用put这就破坏了putIf-Absent方法期望保持的语义。3.2、伪同步那给该方法加上synchronized就能实现安全吗如下所示publicsynchronizedVputIfAbsent(Kkey,Vvalue){Voldmap.get(key);if(old!null){returnold;}returnmap.put(key,value);}答案是否定的为什么呢同步错对象了。putIfAbsent同步使用的是EnhancedMap对象而其他方法如代码中的put方法使用的是Collections.synchronizedMap返回的对象map两者是不同的对象。要解决这个问题所有方法必须使用相同的锁可以使用EnhancedMap的对象锁也可以使用map。使用EnhancedMap对象作为锁则Enhanced-Map中的所有方法都需要加上synchronized。使用map作为锁putIfAbsent方法可以改为publicVputIfAbsent(Kkey,Vvalue){synchronized(map){Voldmap.get(key);if(old!null){returnold;}returnmap.put(key,value);}}3.3、迭代对于同步容器对象虽然单个操作是安全的但迭代并不是。我们看个例子创建一个同步List对象一个线程修改List另一个遍历看看会发生什么如代码所示。privatestaticvoidstartModifyThread(finalListStringlist){ThreadmodifyThreadnewThread(newRunnable(){Overridepublicvoidrun(){for(inti0;i100;i){list.add(item i);try{Thread.sleep((int)(Math.random()10));}catch(InterruptedExceptione){}}}});modifyThread.start();}privatestaticvoidstartIteratorThread(finalListStringlist){ThreaditeratorThreadnewThread(newRunnable(){Overridepublicvoidrun(){while(true){for(Stringstr:list){}}}});iteratorThread.start();}publicstaticvoidmain(String[]args){finalListStringlistCollections.synchronizedList(newArrayListString());startIteratorThread(list);startModifyThread(list);}运行该程序程序抛出并发修改异常Exceptionin threadThread-0java.util.ConcurrentModificationExceptionatjava.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)atjava.util.ArrayList$Itr.next(ArrayList.java:831)我们之前介绍过这个异常如果在遍历的同时容器发生了结构性变化就会抛出该异常。同步容器并没有解决这个问题如果要避免这个异常需要在遍历的时候给整个容器对象加锁。比如上面的代码startIteratorThread可以改为privatestaticvoidstartIteratorThread(finalListStringlist){ThreaditeratorThreadnewThread(newRunnable(){Overridepublicvoidrun(){while(true){synchronized(list){for(Stringstr:list){}}}}});iteratorThread.start();}3.4、并发容器除了以上这些注意事项同步容器的性能也是比较低的当并发访问量比较大的时候性能比较差。所幸的是Java中还有很多专为并发设计的容器类比如CopyOnWriteArrayList。ConcurrentHashMap。ConcurrentLinkedQueue。ConcurrentSkipListSet。这些容器类都是线程安全的但都没有使用synchronized没有迭代问题直接支持一些复合操作性能也高得多。