多线程操作同一个对象时,容易引发线程安全问题。为了解决线程安全问题,Java多线程引入了同步监视器。
同步代码块
同步代码块语法格式如下:
synchronized(obj){
//此处的代码即为同步代码块
}
上面语法格式中synchronized后面括号的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然放弃了对同步监视器的锁定。
通常推荐使用可能被并发访问的共享资源当作同步监视器。
同步方法
同步方法就是用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无需显式指定同步监视器,同步方法的同步监视器是this,即当前对象本身。
需要注意的是,synchronized关键字可以修饰代码块,可以修饰方法,但是不能修饰构造器、属性等。
可变类的线程安全是以降低程序运行效率为代价的,为减少线程安全所带来的负面影响,可以使用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变共享资源的方法进行同步;
- 为可变类提供两种版本:线程安全版本和线程不安全版本,以适应不同的运行环境。
释放同步监视器的锁定
线程进入同步方法或同步代码块后无法主动释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
- 同步方法、同步代码块执行结束;
- 在执行同步方法、同步代码块时遇到break、return终止了同步代码块、同步方法的执行;
- 在执行同步方法、同步代码块是遇到了Error或Exception,发生了中断;
- 在执行同步方法、同步代码块时,执行了同步监视器对象的wait()方法,则当前线程暂停、并释放同步监视器。
在下面的情况下,程序不会释放同步监视器的锁定:
- 在执行同步方法、同步代码块时,程序调用了Thread.sleep()、Thread.yield()来暂停当前线程的执行;
- 在执行同步方法、同步代码块时,其他线程调用了当前线程的suspend方法将该线程挂起。
同步锁
同步锁通过显示定义同步锁对象来实现线程同步。这里同步锁使用Lock对象充当。
通常认为,同步锁提供了比同步代码块和同步方法更广泛的锁定操作。
synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。
Lock 实现提供了使用 同步方法和同步代码块所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock())、一个获取可中断锁的尝试 (lockInterruptibly()) 和一个获取超时失效锁的尝试(tryLock(long, TimeUnit))。
Lock类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。
Lock的实现类比较常用的是ReentrantLock。ReentrantLock锁具有同重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁。
还有一个接口ReadWriteLock。ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。 所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
死锁
当两个线程互相等待对方释放同步监视器就会发生死锁。Java虚拟机没有监测,也没有采用措施来处理死锁的情况。
预防死锁,需要防止线程在等待的情况占用资源。