线程安全是并发编程必须要关注的问题.多个线程同时访问共享资源,就会出现共享资源被修改情况,其他线程无法接受到修改后的结果,导致错误的运行结果.比如共享变量,共享文件的写操作等等.锁机制保证了一个共享数据同时只能被一个线程操作,其他线程只能等待当前线程释放锁,然后再获取锁,进而操作或者访问共享数据.
在JDK1.5之前都是使用synchronzied和volatile关键字来实现共享对象访问机制, lock并不是为了替代内置锁,而是提供内置锁具有的一些额外的功能.
synchronzied
synchronzied是java内置的关键字,是jvm层面的,对于synchronzied理解有三点:
(1)synchronzied可以修饰代码块,方法,静态方法和类.只有一个线程能访问到共享数据.线程A要请求一个被线程B占用的锁,线程A会一直等待或者阻塞直到线程B释放锁,也就是说B如果不释放锁的时候,A会永远的等待下去.不能中断那些等待获取锁的线程,并且在请求锁失败的情况下,会无限等待.
(2)synchronzied的锁不需要手动释放,有两种情况下,获取锁的线程会释放锁.其一是当被synchronzied修饰的代码执行完,线程会释放锁,其二是线程执行同步代码过程中发生异常,jvm会让线程释放锁.
(3)多个线程进行读取操作时相互之间不会冲突,而synchronized实现的同步,当一个线程进行读操作的时候,其他线程只能等待无法进行读取操作,这种情况下,效率比较低下.
由此看见,synchronzied实现的同步有很多的限制,lock就提供了比synchronized更多的功能,打破了这种限制,但是使用lock实现同步某种程度上比内部锁更加复杂.
Lock
Lock是java.util.concurrent.locks下面的一个接口,使用此接口及实现类实现同步,Lock接口的源码如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock()方法是比较常见的方法,用来获取锁,如果锁被其他的线程占用,线程将处于休眠状态.采用lock,必须手动释放锁,但也不排除出现异常的情况,所以lock以try{}finally{}的形式使用,在finally中进行锁的释放,保证发生异常时锁也会得到释放,避免了死锁的发生.使用方式如下:
Lock lock = ...;
...
lock.lock();
try {
//更新对象状态,捕获异常
}finally {
lock.unlock();//释放锁
}
- lockInterruptibly() throws InterruptedException方法表示的锁可用情况下,会立即获取锁.如果线程处于等待获取锁的状态 ,线程能够响应中断,即中断线程等待状态.如果锁被线程A占用,线程B使用lock.lockInterruptibly()获取锁的时候,会一直处于等待状态,此时其他线程可调用interrupt()方法能中断线程B的等待状态. 在获取锁过程线程被中断,抛出异常InterruptedException,使用方式try{}finally{}形式,或者在调用lockInterruptibly()方法外抛出异常.
public void threadDemo() throws InterruptedException {
Lock lock = ...;
...
lock.lockInterruptibly();
try {
.....
}
finally {
lock.unlock();
}
}
- tryLock()方法也是用来获取锁,锁可用情况下,获取锁,并且立即返回true,锁不可用情况下,会立即返回false.相比lock()方法,获取不到锁情况下不会一直等待.
- tryLock(long time,TimeUnit unit) throws InterruptedException.其中time是等待锁的最长时间,unit是参数的时间单位.方法表示锁在给定等待时间time内空闲,并且当前线程未被中断,获得了锁时,方法会立即返回true.在给定的时间time内获取不到锁,会返回false.如果将时间设置小于等于0,方法会立即返回.
Lock lock = ...;
...
lock.lock();
if(lock.tryLock()) {
try {
//
}finally {
lock.unlock();
}
}else {
//不能获取到锁的情况下.
}
- Condition是一个接口,作用是锁进行更加精确的控制,能够控制多线程的休眠和唤醒,对于一个锁可以创建多个Condition,不同情况下使用不同的Condition.Condition接口里面有与Object相对应的休眠和唤醒的方法.不同的是Object中唤醒和等待的方法经常与synchronzied搭配使用.
Condition Object 作用
await wait 线程休眠
signal notify 唤醒一个线程
signalAll notifyAll 唤醒所有线程
Lock使用案例
主要来看一下Lock的使用方式.Lock是接口,经常使用的是它的实现类ReentrantLock.ReentrantLock提供了与synchronzied相同的互斥和内存可见性.
1.lock()方法使用如下:
public class SellTicketTask implements Runnable {
private int ticket = 10;
private Lock lock = new ReentrantLock();
public void run() {
try {
if (ticket > 0) {
lock.lock();
System.out.println(Thread.currentThread().getName() + "拿到锁");
ticket--;
System.out.println("剩余票" + ticket);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
}
public class LockDemo2 {
public static void main(String[] args) {
SellTicketTask task = new SellTicketTask();
new Thread(task).start();
new Thread(task).start();
}
}
运行结果:
Thread-0拿到锁
剩余票9
Thread-0释放锁
Thread-1拿到锁
剩余票8
Thread-1释放锁
这个demo之前,有一个错误的使用Lock的案例,还是把代码贴在这里,防止犯同样的错误.如下:
public class LockDemo {
//private Lock lock = new ReentrantLock();//正确的方法
private int ticket = 10;
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
new Thread() {
public void run() {
lockDemo.sellTicket(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
lockDemo.sellTicket(Thread.currentThread());
}
}.start();
}
private void sellTicket(Thread currentThread) {
//Lock lock =new ReentrantLock();非正确方式.当把锁放在这里的时候,两个线程分别调用分别会产生两个锁
lock.lock();
try {
System.out.println(currentThread.getName()+"得到了锁");
ticket--;
System.out.println("剩余票数"+ticket);
}catch(Exception e) {
e.printStackTrace();
}finally {
System.out.println(currentThread.getName()+"释放了锁");
lock.unlock();
}
}
运行会产生这样的结果:
Thread-1得到了锁
Thread-0得到了锁
剩余票数9
剩余票数8
Thread-1释放了锁
Thread-0释放了锁
当把锁放在sellTicket()方法里面的时候,会产生上面得结果.之所产生这样的结果,稍微分析可以知道.两个线程分别调用sellTicket()方法的时候,会导致每个线程都会运行Lock lock= new ReetrantLock(),每个线程都产生一个锁.我们目的就是为了同时只有一个线程访问共享数据,所以要使用同一个锁.将lock变量声明为类的局部变量
2.tryLock()方法的使用案例
public class LockDemo {
private Lock lock = new ReentrantLock();
private int ticket = 10;
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
new Thread() {
public void run() {
lockDemo.sellTicket(Thread.currentThread());
}
}.start();
new Thread() {
public void run() {
lockDemo.sellTicket(Thread.currentThread());
}
}.start();
}
private void sellTicket(Thread currentThread) {
if(lock.tryLock()) {
try {
System.out.println(currentThread.getName()+"得到了锁");
ticket--;
System.out.println("剩余票数"+ticket);
}catch(Exception e) {
e.printStackTrace();
}finally {
System.out.println(currentThread.getName()+"释放了锁");
lock.unlock();
}
}else {
System.out.println(currentThread.getName()+"获取锁失败");
}
}
}
运行结果:
Thread-0得到了锁
剩余票数9
Thread-0释放了锁
Thread-1获取锁失败
3.使用LockInterruptebily()方法的案例
public class LockDemo {
private Lock lock = new ReentrantLock();
private int ticket = 10;
public static void main(String[] args) {
final LockDemo lockDemo = new LockDemo();
Thread thread1 = new Thread() {
public void run() {
try {
lockDemo.sellTicket(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "线程被中断");
}
}
};
Thread thread2 = new Thread() {
public void run() {
try {
lockDemo.sellTicket(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "线程被中断");
}
}
};
thread1.start();
thread2.start();
thread2.interrupt(); // 中断第二个线程
}
private void sellTicket(Thread currentThread) throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println(currentThread.getName() + "得到了锁");
ticket--;
System.out.println("剩余票数" + ticket);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(currentThread.getName() + "释放了锁");
lock.unlock();
}
}
}
运行结果:
Thread-1线程被中断
Thread-0得到了锁
剩余票数9
Thread-0释放了锁
synchronzied与Lock的区别
可以得到,synchronized与lock有几点区别:
- synchronzied关键字是java内置的,是jvm层面的,而lock是一个接口.
- synchronzied是无法判断是否获取了锁,但lock可以判断是否获取了锁,知道锁的状态.
- synchronzied不管是执行同步代码块或者发生异常释放锁,都会自动释放锁,不需要手动干预.而lock需要使用try{}finally{}形式,在finally保证锁被释放,否则可能发生死锁现象.
- synchronzied在同一时间只能有一个线程访问,lock在读取锁时,可以让多个线程同时访问.
- synchronzied没有中断线程的机制,获取不到锁时会一直等待下去.lock可以中断等待获取锁的线程状态.
所以在内部锁不满足需求的时候,或者说需要使用到可定时,可轮询与可中断锁获取操作,公平队列或者非结构的锁时,可以使用Lock,否则的还是使用更加简洁的synchronzied.