在Java程序中,用来管理线程间协调的主要工具是syschronized关键字。当缺少同步的情况下,jvm可以很轻松的对不同线程的执行操作进行计时和排序,这可以在大部分情况下提高性能,但是也给程序员带来了额外的负担,必须识别可能危机的程序正确性。
Syschronized真正意味着什么?
刚开始以为同步代码块、同步方法理解为使用互斥,原子操作。但是还有更复杂的应该清除。
syschronized语义确实保证了一次只有一个线程可以访问被保护的区段。*****但同时还包括同步线程在主存内相互作用的规则。*****在缺少同步的情况下,JVM允许两个线程在同一个内存地址看到不一样的值,而当用锁进入同步时,JVM立马要求线程工作缓存中的值失效,并在所释放前对她进行刷新并将修改过的值写会主存。不难看出为什么同步对程序影响这么大,频繁的刷新缓存代价很大。
选择一条好的运行线路
如果同步使用不恰当,对共享资源会造成数据混乱和争用情况,导致程序崩溃或者不可知的运行,更糟的是有些情况是偶然发生的,而我们得出程序正确的结论。另一方面,同步使用不当会造成性能降低或死锁,因此编写优秀的多线程程序需要使用好的运行线路,足够的同步使数据不发生紊乱,但不滥用到承担死锁或不必要的削减程序性能的风险。
同步的代价,不要争用
程序即使只包括一个在单处理器上运行的单线程,一个同步方法的调用比非同步调用也慢很多,如果同时发生锁争用,性能会降低更多。但是随着JVM的优化,对同步的代价不断降低,反而是争用情况会造成极大性能损失。那么减少争用需要考虑减小锁的粒度,减小同步块大小以及减小线程间共享资源的数量。
什么时候需要同步
简单说,存在共享数据时需要考虑,一般情况可使用volatile保护数据字段,其他情况,可在读写共享数据前请求加锁,一个好的建议是明确指出用什么锁来保护给定字段或对象,并在代码中记录。
另外,简单的同步存取器方法(或者声明下层字段为volatile)并不能保护一个共享字段。例:
private int foo;
public synchronized int getFoo() { return foo; }
public synchronized void setFoo(int f) { foo = f; }`
如果一个调用者想增加foo属性值,下面代码不是线程安全的:
setFoo(getFoo() + 1);
如果两个线程同时增加foo值,结果可能增加1或2,调用者需要同步一个锁,或者进行同步复合操作,才能防止这种争用情况。一个好的方法是在JavaDoc类中指定同步哪个锁,这样调用者就不用猜了。
如果情况不确定,考虑同步包装
当写一个类的时候,不确定时候用在共享环境,或者不清楚适合的锁粒度多大,可以通过同步包装来解决。例如Collections类,它是非同步的,但在框架定义中每个接口都有一个同步包装(例:Collctions.synchronizedMap(),它用一个同步的版本包装每个方法)。