当使用多线程来访问同一个数据时,就会很容易出现线程安全问题,最经典的问题就是银行取钱的问题。而如何处理线程安全问题呢,最常用的回答就是加锁,实现线程同步。那么如何加锁呢,拆分之后,无非是下面三种方式:同步代码块、同步方法、同步锁。
1 同步代码块
同步代码块的语法如下:
synchronized(obj){ //此处的代码就是同步代码块 }
上述语法格式中的synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
注意:阻止多线程对同一个共享资源进行并发访问,通常推荐使用可能被并发访问的共享资源充当同步监视器。
2 同步方法
与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
注意:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采取如下策略:
(1)不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
(2)如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。
JDK所提供的StringBuilder、StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBulider来保证较好的性能;当需要保证多线程安全时,就应该使用SringBuffer。
释放同步监视器的锁定:
针对同步代码块、同步方法,程序无法显式释放对同步监视器的锁定,那么线程在哪几种情况下会释放同步监视器的锁定呢?
(1)同步方法、同步代码块执行结束;
(2)同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行;
(3)同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
(4)程序执行了同步监视器对象的wait()方法。
下面的情况不会释放锁定:程序调用sleep()、yield()和suspend()方法。
3 同步锁
从Java5开始,Java还提供了一种功能强大的线程同步机制,通过显示定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
Lock是控制多线程对共享资源进行访问的工具,通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock,另外还为Lock提供了ReentrantLock(可重入锁)实现类。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:
class X { //定义锁对象 private final ReentrantLock lock = new ReentrantLock(); // ... public void m(){ //加锁 lock.lock(); try{ //需要保证线程安全的代码 // method body } //使用finally块来保证释放锁 finally{ lock.unlock(); } } }
注意:
(1)使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。
(2)使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。
(3)ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
4 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施处理死锁的情况,所以在多线程编程时,应该采取措施避免死锁出现。而一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态。