多个执行线程共享一个资源的情景,是最常见的并发编程情景之一。在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或者数据库连接。
为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生。
为了解决这些问题,人们引入了临界区概念。临界区是一个可用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。
为了帮助编程人员实行这个临界区,java提供了同步机制。当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区。如果没有其他线程进入临界区,它就可以进入临界区;如果已经有线程进入了临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中一个,其余的将继续等待。
java语言提供了两种基本同步机制:
1.synchronized关键字机制
2.Lock接口及其实现机制
一、synchronized关键字机制
1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
对于实例的同步方法,使用this即当前实例对象。
对于静态的同步方法,使用当前类的字节码对象。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){}
Java中任意的对象都可以作为一个监听器(monitor),监听器可以被上锁和解锁,在线程同步中称为同步锁,且同步锁在同一时间只能被一个线程所持有。上面的obj对象就是一个同步锁,分析一下上面代码的执行过程:
1).一个线程执行到synchronized代码块,首先检查obj,如果obj为空,抛出NullPointerExpression异常;
2).如果obj不为空,线程尝试给监听器上锁,如果监听器已经被锁,则线程不能获取到锁,线程就被阻塞;
3).如果监听器没被锁,则线程将监听器上锁,并且持有该锁,然后执行代码块;
4).代码块正常执行结束或者非正常结束,监听器都将自动解锁;
线程同步锁对多个线程必须是互斥的,即多个线程需要使用同一个同步锁。代码中obj对象被多个线程共享,能够实现同步。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
两者的区别主要体现在同步锁上面。对于实例的同步方法,因为只能使用this来作为同步锁,如果一个类中需要使用到多个锁,为了避免锁的冲突,必然需要使用不同的对象,这时候同步方法不能满足需求,只能使用同步代码块(同步代码块可以传入任意对象);或者多个类中需要使用到同一个锁,这时候多个类的实例this显然是不同的,也只能使用同步代码块,传入同一个对象。
二、Lock接口及其实现机制
Lock接口及实现类提供了更多的好处:
1.支持更灵活的同步代码块结构。使用synchronized关键字时,只能在同一个synchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构,即控制的获取和释放不出现在同一个块结构中。
2.相比synchronized关键字,Lock接口提供了更多的功能。其中一个新功能是tryLock()方法的实现。这个方法试图获取锁,如果锁已被其他线程获取,它将返回false,并继续往下执行代码。使用synchronized关键字时,如果线程A试图执行一个同步代码块,而线程B已在执行这个同步代码块,则线程A就会被挂起直到线程B运行完这个同步代码块。使用锁tryLock()方法,通过返回值将得知是否有其他线程正在使用这个锁保护的代码块。
3.Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。
4.相比synchronized关键字,Lock接口具有更好的性能。
锁的公平性
ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数fair,它允许你控制着两个类的行为。默认fair值为false,它称为非公平模式。
在非公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,这个选择是没有任何约束的。
如果fair值为true,则称为公平模式。
在公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,而且选择的是等待时间最长的。
这两种模式只适用于lock()和unlock()方法。而Lock接口的tryLock()方法没有将线程置于休眠,fair属性并不影响这个方法。
常用的线程类:
1)、ReentrantLock类:是一个可重入的互斥锁,重入锁是一种递归无阻塞的同步机制。ReentrantLock由最近成功获取锁,还没有释放的线程所拥有,当锁被另一个线程拥有时,调用lock的线程可以成功获取锁。如果锁已经被当前线程拥有,当前线程会立即返回。
a、防止重复执行(忽略重复触发)
ReentrantLock lock = new ReentrantLock();
public void getObject(){
//如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
if (lock.tryLock()) {
try {
//操作
} finally {
lock.unlock();
}
}
}
b、同步执行,类似synchronized
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
//ReentrantLock lock = new ReentrantLock(true); //公平锁
public void getObject(){
//如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
lock.lock();
try {
//操作
} finally {
lock.unlock();
}
}
c、尝试等待执行
ReentrantLock lock = new ReentrantLock(true); //公平锁
public void getObject(){
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
//如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}
}
d、可中断锁的同步执行
ReentrantLock lock = new ReentrantLock(true); //公平锁
public void getObject(){
lock.lockInterruptibly();
try {
//操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
ReadWriteLock类:读写锁,维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
ReentrantReadWriteLock类:可重入读写锁,会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
1).没有其他线程的写锁,
2).没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
1).没有其他线程的读锁
2).没有其他线程的写锁
ReentrantReadWriteLock和ReentrantLock的区别,它和后者都是单独的实现,彼此之间没有继承或实现的关系:
(a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
(b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。
(c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
(d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
(e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
读写锁的例子:
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
Queue3 q3 = new Queue3();
for(int i=0;i<3;i++){
new Thread(){
public void run(){
while(true){
q3.get();
}
}
}.start();
}
for(int i=0;i<3;i++){
new Thread(){
public void run(){
while(true){
q3.put(new Random().nextInt(10000));
}
}
}.start();
}
}
}
class Queue3{
private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get(){
rwl.readLock().lock();//上读锁,其他线程只能读不能写
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
try {
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "have read data :" + data);
rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
}
public void put(Object data){
rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
try {
Thread.sleep((long)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
rwl.writeLock().unlock();//释放写锁
}
}
在锁中使用多条件
一个锁可能关联一个或多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起直到某个线程唤醒它们。Condition接口提供了挂起线程和唤起线程的机制。
Condition是在java1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
代码示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) {
final ReentrantLock reentrantLock = new ReentrantLock();
final Condition condition = reentrantLock.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "拿到锁了");
System.out.println(Thread.currentThread().getName() + "等待信号");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到信号");
reentrantLock.unlock();
}
}, "线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "拿到锁了");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "发出信号");
condition.signalAll();
reentrantLock.unlock();
}
}, "线程2").start();
}
}