当多线程对同一个资源对象进行操作时,改变资源的值可能会造成线程不安全。为了保证线程安全,提高程序的可靠性,必须进行线程同步。
线程同步的方法有很多种,下面一一来介绍:
1、同步代码块儿
synchronized (obj)
{
<span style="white-space:pre"> </span>//同步代码块儿
}
首先用synchronized修饰代码块儿,表征同步。
其次,获取同步监视器obj,线程开始执行同步代码块儿之前,必须获得对同步监视器的锁定。
通常将可能被并发访问的共享资源充当同步监视器。同步代码块儿在执行之前,先获得对同步监视器的锁定,使得其他线程无法获得锁,于是其他线程就不能修改它。这种做法符合 “加锁、修改、解锁” 的思想。开始执行同步代码块儿之前加锁,执行过程中修改变量,同步代码块儿结束的时候释放锁。通过这种方式保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区,也就是同步代码块儿当中。
当前线程锁定了同步监视器之后,即使有其他的线程想要获得同步监视器对象,也无法实现,无法修改对象的值。必须等到当前线程释放了锁之后,其他线程才能进行操作。每个进来的线程都会首先获得对同步监视器的锁定,从而保证了修改同步监视器对象在某一个时刻的线程唯一性,保证了线程安全。
注意这种同步代码块儿一般指的就是线程执行体中的代码,也就是run方法中的代码块儿。
2、同步方法
用synchronized关键字修饰某个方法,称之为同步方法,不需要指定同步监视器,因为此时的同步监视器就是this(调用该方法的对象)。使用同步方法可以方便的实现线程安全的类。
一般线程中的不可变量都是线程安全的,线程不安全的是线程中的可变量,为了保证可变量的线程访问安全性,可以直接将修改可变量的方法设置成同步方法。
但是保证修改可变量的方法的线程安全性的代价是牺牲了效率。
3、同步锁
Java5中定义了一种更加强大的线程同步机制,通过显式定义同步锁对象来实现同步,同步锁用Lock标志。Lock和ReadWriteLock是Java5中提供的两个根接口。
Lock有一个实现类是可重入锁ReentrantLock(比较常用)。ReadWriteLock有一个实现类是ReentrantReadWriteLock。所谓可重入性是指一个线程可以对已经被加锁的ReentrantLock再次加锁,可以嵌套着使用,多次加锁。同理,也要显式的多次解锁。
使用锁的通用代码格式如下:
public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//...其他的一些变量定义
// 定义需要保证线程安全的方法
public void methord()
{
// 加锁
lock.lock();
try
{
//需要保证线程安全的代码段
}
finally
{
// 可变量修改完成,释放锁
lock.unlock();
}
}
}
通常需要使用finally块来确保在必要时释放锁。
死锁
既然降到了Lock锁,不可避免的要讲到死锁。所谓死锁就是指两个线程相互等待着对方释放同步监视器,致使所有线程都处于阻塞的状态。
介绍一种最简单的情况:有两个线程,线程一持有A对象的锁,还没有运行结束,还不能解锁,在运行过程中,需要获取B对象的锁,所以它就阻塞了,等待对B对象加锁。与此同时,线程二持有B对象的锁,同样还处在运行过程中,尚不能解锁。在运行的时候需要获取A对象的锁,于是它也阻塞了,等待对A对象加锁。
两个进程持有着对方需要的锁,相互等待着对方释放锁,于是就都处在了阻塞状态,也就是死锁。