线程同步的风险
使用多线程可以让你充分利用多核处理器的资源,更简单的处理异步事件等。但是多线程有3个非常需要关注和解决的风险:
1.安全性风险:即每一次系统运行的最终结果都是与预期相符的,不会产生各种稀奇古怪无法预知的结果。
2.活动度风险:即预期会发生的事件最终会发生,而不是进入一种永远不会发生的状态。一个典型的事件就是死锁。
3.性能风险:良好的多线程设计能获得良好的性能,但是设计不好的系统反而可能使性能下降。多线程的性能开销主要来源于两个方面:1是线程同步带来的开销,2是线程上下文切换带来的开销。
而上述3大风险的解决或产生,无疑都与线程同步息息相关。安全性需要线程同步来解决,线程同步可能发生死锁导致活跃度失败的风险,即使解决了安全性和避免了活跃度失败的风险,这时候系统可以实现你的功能了,但是也并不意味着你线程同步没有问题,还有可能系统性能大大降低了。
可以说线程同步是并发编程设计中最核心的问题,下面讲讲几种常用的线程同步方式
volatile关键字
线程同步包含两方面的含义:一是可见性,保证获得的数据时最新的;二是原子性,原子性是不可分割的最小粒度的操作步骤。操作过程中只有一个线程在运行,不可被其他线程中断、更改。在没有同步的情况下,一个最小粒度的原子是一个机器指令。而线程同步可以使一组操作指令成为原子性。volatile关键字是一种比较弱的同步方式,他不会对它声明的对象加锁,也不会线程造成阻塞,它只保证可见性,不保证原子性。用volatile声明的属性只保证你读到的是最新的值,并不能保证a++是同步的。所以,为防止volatile被滥用,一般满足以下3个条件才能安全的使用volatile:
1,写入新的值时,并不依赖于当前的值;或者能保证只有一个线程能修改值。
2,变量不需要与其他的变量共同参与不变性约束。
3,访问变量时,没有其他的原因需要加锁。
原子类
java为大部分基本类型提供了原子类。比如AtomicInteger, AtomicLong和AtomicBoolean。这些类用法基本相似,他不当保证了get/set的同步,同时还为常用的自增自减操作提供了同步方法decrementAndGet,getAndDecrement,incrementAndGet,getAndIncrement
信号量Semaphore
当一个线程想要访问某个共享资源,首先,它必须获得semaphore。如果semaphore的内部计数器的值大于0,那么semaphore减少计数器的值并允许访问共享的资源。计数器的值大于0表示,有可以自由使用的资源,所以线程可以访问并使用它们。如果semaphore的计数器的值等于0,那么semaphore让线程进入休眠状态一直到计数器大于0。计数器的值等于0表示全部的共享资源都正被线程们使用,所以此线程想要访问就必须等到某个资源成为自由的。Semaphore对象的构造参数可以传信号上线,当上限为1时就相当于锁。semaphore.acquire()获取信号,该方法阻塞一定到其他线程释放信号。semaphore.release()执行完锁住的代码之后,需要释放信号,让其他等待的线程能够获取访问资格。
synchronized关键字
synchronized声明方法
只有一个执行线程将会访问一个对象中被synchronized关键字声明的方法。如果另一个线程试图访问同一个对象中任何被synchronized关键字声明的方法,它将被阻塞,直到第一个线程结束方法的执行。几点说明:1.只要有一个线程锁住了对象中的一个synchronized方法,那么其他线程将不能访问这个对象中的任何一个被声明为synchronized的方法。 对于synchronized声明的方法,锁是当前实例对象。
2.如果静态方法被声明为synchronized,此时锁是当前类的class对象。只有一个线程能够访问该静态方法。但是,其他线程可以访问该类的一个对象中的其他非静态的方法。 你必须非常小心这一点,因为两个线程可以访问两个不同的同步方法,如果其中一个是静态的而另一个不是。如果这两种方法改变相同的数据,你将会有数据不一致 的错误。
3.由于synchronized会锁住这个方法的整个代码,所以一般适合于比较简单、执行效率高的方法,否则将可能导致性能严重下降。
synchronized锁住一段代码
由于某些方法可能比较复杂,synchronized声明方法会使性能下降,所以可以使用synchronized包含需要同步的代码段,此时锁是synchronized括号里包含的对象。当你使用synchronized关键字来保护代码块时,必须通过一个对象的引用作为参数,比如经常用的是synchronized(this){code}。但是如果对象中有两个以上的属性需要独立上锁时,用同一个锁则会对性能造成影响。这时可以在对象中创建多个object对象,通过声明不同的synchronized(object1){code1},synchronized(object2){code2}来分别给不同的属性操作方法上锁。
使用Lock
Lock比synchronized关键字更加强大、灵活。很多用的比较多的锁,比如可重入锁,读写锁都在用Lock实现的。同时Lock还提供了tryLock方法来获得锁,并立即返回,不会使线程休眠。如果tryLock返回true,表示可以获取锁,如果返回false,则不能获得锁,开发人员可以根据场景对不同结果进行处理。