不正之处,欢迎指教!!!
有了多线程之后,可以大大提高程序的工作效率,但是随之而来的也会带来一定的问题,比如程序中的多个线程可能在同一时刻要访问同一个文件,由于每一个线程的执行是不可控制的,所以很可能导致最终的结果与我们预期的不太一样。
例如现在有两个线程A,B,分别向一个Set集合中写数据,我们知道在写入的过程中是不允许有重复元素存在的,如果在某一个时刻,线程A读取了元素X,线程B也读取到了元素X,那么线程A就会去检差集合中是没有X的,此时还没有来得及插入的时候执行到了线程B,B也检查了集合中也是没有X的,并将X插入到了集合中。若此时线程切换到了A,因为A刚才已经检查了集合中是没有X的,所以接着执行插入操作,这显然是不符合的。
这就是线程安全问题,当多个线程在访问线程中共享的资源时会出现意想不到的结果,这里的资源必须是临界资源,是线程所共有的。特别申明,方法内部的变量是不属于临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
如果我们把临界资源比喻成一所房子的话,多个线程比喻成想要进入房子的人,如果每次进入的人只有一个的话,那么就可以保证每次只有一个线程去访问临界资源。也就是序列化的去访问资源,在Java中采用加锁机制来解决线程安全问题。在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
1.synchronized
先说一下互斥锁的概念,互斥锁就是可以达到互斥访问的锁,在多个线程要访问临界资源的请款下,如果一个线程在访问的时候,其他的线程只能等待。Java中,每一个对象都有一个锁标记,线程需要访问这个对象就必须要获得对象的锁(有了钥匙才可以进门),用synchronized关键字可以来标示一个方法或者代码块,当线程调用synchronized的方法或者代码块时,相应的线程就获得了该对象的锁。其他对象就无法访问这个对象,只有等该线程访问完了之后释放了对象锁,其他线程才可以访问这个对象。例如下面的代码:
class InsertData {
private ArrayList<Integer> list = new ArrayList<>();
public void Insert(Thread thread) {
for (int i = 0; i < 3; i++) {
list.add(i);
System.out.println(Thread.currentThread().getName() + "向列表中插入" + i);
}
}
}
public class Test implements Runnable {
private InsertData data = new InsertData();
@Override
public void run() {
data.Insert(Thread.currentThread());
}
public static void main(String[] args) {
Test t = new Test();
new Thread(t).start();
new Thread(t).start();
}
}
</span>
在运行期间会出现这样的情况:
两个线程同时在向列表中插入数据,说明此时这两个线程在同时的访问列表,当然在这里list中的数据是可以重复的,我们希望的是两个线程可以单独的访问这个list,所以此时可以在Insert方法中加入synchronized关键字,这样每次线程在访问list时候,就相当于获得了t对象的锁,其他的线程就只能等待不能同时访问这个临界资源。
public synchronized void Insert(Thread thread) {
for (int i = 0; i < 3; i++) {
list.add(i);
System.out.println(Thread.currentThread().getName() + "向列表中插入" + i);
}
}</span>
通过加上synchronized关键字之后,可以保证线程互斥访问列表。
synchronized(synObject) {
}</span>
synchronized代码块是在某一个对象中用synchronized关键字限制的一段代码块,当在某个线程中执行这段代码块,该线程就可以获得当前对象的锁,从而使其他的对象无法同时访问这段代码块,synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。所以下面的效果和直接在方法上使用synchronized的效果是一样的。
public void Insert(Thread thread) {
synchronized(this){
for (int i = 0; i < 3; i++) {
list.add(i);
System.out.println(Thread.currentThread().getName() + "向集合中插入" + i);
}
}
}</span>
synchronized代码块的使用效果比在方法上加synchronized要更加灵活,它可以只在方法的一部分上面使用,提高了执行效率,按需要来同步,因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。
2.Lock
Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock,Lock是一个借口,定义如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}</span>
在Lock接口中,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()都是用来获取锁的,而unLock()方法是用来释放锁的。前面讲到用synchronized关键字实现同步,那么什么时候线程会释放获得的对象锁呢?在线程执行完了该代码块之后会释放锁,在线程发生异常的时候,Java虚拟机会自动释放线程所拥有的对象锁。但是如果线程在获取对象锁之后,因为等待IO或者其他的原因被阻塞,此时其他的线程就只能等待,这样会造成很大的浪费,这个时候我么希望在等待了一定时间之后其他的线程也是可以执行的,这时候通过Lock就可以响应中断。因为Lock是通过具体的实现类来实现同步方法,所以在获得对象锁之后就必须要去手动的释放锁。使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
使用lock的形式一般如下所示:
Lock lock=.;
lock.lock(); //获取锁
try {
//
}catch(){
}finally {
lock.unlock(); //释放锁
}</span>
在fianlly语句中,释放了锁。
ReentrantLock,意思是“可重入锁”,Lock接口的具体实现类利用lock实现同步的过程如下:
class InsertData {
private Lock lock = new ReentrantLock();
private ArrayList<Integer> list = new ArrayList<>();
public void Insert(Thread thread) {
lock.lock();
try {
for (int i = 0; i < 3; i++) {
list.add(i);
System.out.println(Thread.currentThread().getName() + "向集合中插入" + i);
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
通过Lock和synchronized都可以实现同步操作,不同的是使用synchronized时候如果发生异常则系统会自动释放对象锁,而Lock则需要在finally块中释放锁,通过Lock的tryLock()方法的返回值可以知道有没有成功获取到锁,但是通过synchronized却不可以,Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
3.参考文章
http://www.cnblogs.com/dolphin0520/category/602384.html
《Java核心技术上卷》