Java多线程—线程同步
当多个线程访问一个数据时,很容易出现线程安全问题,很大原因是线程不同步造成的。
同步代码块
为了解决这个问题,Java使用同步监视器
来解决,方法就是使用同步代码块
。
synchronized(obj){//obj同步监视器
//同步代码块
}
线程开始执行同步代码块前,必须获得对同步监视器的锁定,任何时刻只能有一个可以获得对同步监视器的锁定,执行完后将会释放。例如使用synchronized
将run()
的方法体修改成同步代码块,任何线程在获取资源前对其加锁,保证并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区(临界区)
,所以同一时刻只有一个线程处于临界区
。
同步方法
多线程还提供了同步方法,同步方法就是用synchronized
修饰的方法。对于修饰的实例方法,无需显式指定同步监视器,因为监视器
就是this
,即调用该同步方法的对象
。提供同步方法可以实现安全的线程类,线程安全类的特点是:
- 该类的对象可以被多个线程访问
- 每个线程调用该对象的任意方法后都能得到正确结果,并且该对象状态保持合理状态
synchronized
关键字可以修饰方法
,代码块
,不能修饰构造器
和成员变量
!
对象的状态不可改变,比如不可变类,它的线程总是安全的,而可变类的线程需要其他方法保证其线程安全,例如在可变类中,把修改其成员变量的方法变成同步方法
,因此在线程执行体中可变类的对象调用同步方法则必须对该对象加锁。
上述是在可变类中定义该同步方法的,这很符合面向对象的做法规则,例如DDD(Domain Driven Design)领域驱动设计
,这种方式认为每个类都应该是完备的领域对象,有完备的方法保证对象的完整性、一致性。
同时可变类的线程安全以降低程序运行效率为代价,因此不要对线程安全的类的所有方法进行同步,只对那些会改变共享资源的方法同步。如果单线程和多线程可选,则在单线程中使用不安全线程保证性能,而在多线程中使用安全的线程。例如JDK提供的StringBuilder
,StringBuffer
类,单线程环境下可使用StringBuilder
保证性能,多线程环境下可以使用StringBuffer
来保证性能。
释放同步监视器
线程进入同步代码块或同步方法前会对同步监视器进行锁定,何时释放同步监视器呢?总结了以下情况会释放:
- 线程的同步方法和同步代码块执行结束。
- 在同步代码块、同步方法中
Break
,return
中止继续执行。 - 在同步代码块、同步方法中出现未处理的异常
Exception
或Error
- 在执行同步代码块、同步方法时,程序执行了同步监视器对象的
wait()
方法,当前线程暂停并释放同步监视器。
而以下情况则不会释放同步监视器:
- 调用了
Thread.sleep()
、Thread.yield()
方法暂停线程执行。 - 其他线程调用该线程的
suspend()
方法将该线程挂起也不会释放。
同步锁
Java不止如此还提供了更强大的线程同步机制,通过显式定义同步锁对象Lock对象
来实现同步。Lock
是控制多个线程对共享资源访问的工具,是Java5提供的其中一个根接口。Lock
提供了比synchronized
同步方法和代码块更多的操作,支持多个相关的condition
对象。锁
可以提供对共享资源的独占访问,每次只有一个线程对象对Lock对象
加锁,访问资源前要先获得Lock对象
。
还有一个根接口ReadWriteLock 读写锁
,允许对共享资源进行并发访问。而Java为ReadWriteLock 读写锁
提供了ReentrantReadWriteLock
可重入读写锁实现类,为Lock
提供了ReentrantLock
可重入锁实现类。可重入读写锁ReentrantReadWriteLock
为读写提供Writing
、ReadingOptimistic
、Reading
三种锁模式。在Java8新增了StampedLock
类,通常可代替过去的ReentrantReadWriteLock
。
而比较常用的是ReentrantLock
可重入锁,使用该Lock
对象可显式加锁、释放锁,建议使用finally
确保最终能释放锁。可重入锁具有可重入性
,就是线程可以对已被加锁的ReentrantLock
再次加锁,可重入锁对象有计数器来追踪lock()
方法的嵌套使用,线程在调用lock()
加锁后,必须显式使用unlock()
释放锁。
class x{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
public void foo(){ //定义保证线程安全的方法
lock.lock();
try{
//线程安全代码
}finally{
lock.unlock();
}
}
}
在使用Lock
对象时与同步方法有些类似,使用Lock
显式使用Lock
对象为同步锁;而使用同步方法时,系统将当前对象作为同步监视器并加锁。而且,使用Lock
对象时,每一个Lock
对象对应一个同步监视器,可以保证同一时刻只有一个线程进入临界区
。
同步代码块或同步方法使用与竞争资源相关的、隐式同步监视器,加锁和释放锁必须在一个块结构中。当获取多个锁时,还必须以相反顺序在所有锁被获取时相同范围内释放。所以看出同步代码块和同步方法的范围使使用同步锁时显得不灵活,因此有了Lock
提供了更灵活的方法运用锁,比如tryLock()
方法,获取超时失效锁的tryLock(long TimeUnit)
方法,以及获取可中断锁的lockInterruptibly()
方法。
死锁
当两个线程相互等待对方释放同步监视器时,就会产生死锁
,线程全部进入阻塞状态。线程在获得一个锁1的情况下再去申请另外一个锁2,也就是说在获得了锁1,并且没有释放锁1的情况下,又去申请获得锁2,这个是产生死锁的原因。两个锁的申请就没有发生交叉,就避免了死锁产生的可能。Thread
类提供的suspend
方法容易导致死锁产生,所以不建议使用该方法。
死锁产生的四个必要条件:
- 互斥使用:即当资源被一个线程占有时,别的线程不能使用。
- 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持:当资源请求者在请求其他资源时保持对原有资源的占有。
- 循环等待:即存在一个等待队列,A占有B的资源,B占有C的资源,C占有A的资源。
当上述四个条件都成立的时候,便形成死锁,当如果打破上述任何一个条件,便可让死锁消失。
所以如果我们能避免在对象的同步方法中调用其它对象的同步方法(同步锁),那么就可以避免死锁产生。在多线程编程时应注意避免发生死锁,尤其是有多个同步监视器的情况下。
公众号:菜鸡干Java
站点