synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁会有三种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
3)这个主要是在等待唤醒机制里面的wait()方法,//在等待的时候立即释放锁,方便其他的线程使用锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此我们需要不论程序的代码块执行的如何最终都将锁对象进行释放,方便其他线程的执行。(此处后面有一个简单的demo起始就是将锁对象人工的释放而且是在finally里面的执行)
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,同时为了更好地释放锁。
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
3)在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,
但是ReetrantLock的性能能维持常态;
自锁案例:
MyLock
public class MyLock {
// 创建两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}
// 发生死锁的线程
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
//死锁的测试demo
/*
* 同步的弊端:
* A:效率低
* B:容易产生死锁
*
* 死锁:
* 两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。
*
* 举例:
* 小明和小强的自行车都有两把锁一人一把钥匙案例。
* 正常情况:
* 小明: 两把锁的钥匙都有;
* 小强: 两把锁的钥匙都有。
* 现在:
* 小明:有其中一把锁的两把钥匙;
* 小强:有另一把锁的两把钥匙。
* 结局两个人都不能打开锁。。。。一直等待朔夜起不到自行车
*/
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
二、Lock锁的简单使用
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket implements Runnable {
// 定义票
private int tickets = 100;
// 定义锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// 加锁
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
} finally {
// 释放锁
lock.unlock();
}
}
}
}
下面是测试类
public class RunnableDemo {
public static void main(String[] args) {
SellTicketsRunnable str = new SellTicketsRunnable();
Thread tr1 = new Thread(str, "窗口1");
Thread tr2 = new Thread(str, "窗口2");
Thread tr3 = new Thread(str, "窗口3");
//
tr1.start();
tr2.start();
tr3.start();
}
}
Lock 的简介
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法:
//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
//释放锁
void unlock();
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
Lock l = ...; //根据不同的实现Lock接口类的构造函数得到一个锁对象
l.lock(); //获取锁位于try块的外面
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
注意,加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。
try {
l.lockInterruptibly();//获取锁失败时不会执行finally块中的unlock语句
try{
// access the resource protected by this lock
}finally{
l.unlock();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
实现Lock接口的基本思想
需要实现锁的功能,两个必备元素:
1、一个是表示(锁)状态的变量(我们假设0表示没有线程获取锁,1表示已有线程占有锁)
2、队列,队列中的节点表示因未能获取锁而阻塞的线程。
线程获取锁的大致过程
1. 读取表示锁状态的变量
2. 如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成),当多个线程同时将表示状态的变量值由0设置成1时,仅一个线程能成功,其它线程都会失败
2.1 若成功,表示获取了锁,
2.1.1 如果该线程(或者说节点)已位于在队列中,则将其出列(并将下一个节点则变成了队列的头节点)
2.1.2 如果该线程未入列,则不用对队列进行维护
然后当前线程从lock方法中返回,对共享资源进行访问。
2.2 若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)。
3. 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已。
线程释放锁的大致过程
1. 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码
2. 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程
注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C
公平锁和非公平锁
锁可以分为公平锁和不公平锁,重入锁和非重入锁(关于重入锁的介绍会在ReentrantLock源代码分析中介绍),以上过程实际上是非公平锁的获取和释放过程。
公平锁严格按照先来后到的顺去获取锁,而非公平锁允许插队获取锁。
公平锁获取锁的过程上有些不同,在使用公平锁时,某线程想要获取锁,不仅需要判断当前表示状态的变量的值是否为0,还要判断队列里是否还有其他线程,若队列中还有线程则说明当前线程需要排队,进行入列操作,并将自身阻塞;若队列为空,才能尝试去获取锁。而对于非公平锁,当表示状态的变量的值是为0,就可以尝试获取锁,不必理会队列是否为空,这样就实现了插队获取锁的特点。通常来说非公平锁的吞吐率比公平锁要高,我们一般常用非公平锁。
这里需要解释一点,什么情况下才会出现,表示锁的状态的变量的值是为0而且队列中仍有其它线程等待获取锁的情况。
假设有三个线程A、B、C。A线程为正在运行的线程并持有锁,队列中有一个C线程,位于队首。现在A线程要释放锁,具体执行的过程操作可分为两步:
1. 将表示锁状态的变量值由1变为0,
2. C线程被唤醒,这里要明确两点:
(1)C线程被唤醒并不代表C线程开始执行,C线程此时是处于就绪状态,要等待操作系统的调度
(2)C线程目前还并未出列,C线程要进入运行状态,并且通过竞争获取到锁以后才会出列。
如果C线程此时还没有进入运行态,同时未在队列中的B线程进行获取锁的操作,B就会发现虽然当前没有线程持有锁,但是队列不为空(C线程仍然位于队列中),要满足先来后到的特点(B在C之后执行获取锁的操作),B线程就不能去尝试获取锁,而是进行入列操作。
实现Condition接口的基本思想
Condition 本质是一个接口,它包含如下方法
// 让线程进入等通知待状态
void await() throws InterruptedException;
void awaitUninterruptibly();
//让线程进入等待通知状态,超时结束等待状态,并抛出异常
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
//将条件队列中的一个线程,从等待通知状态转换为等待锁状态
void signal();
//将条件队列中的所有线程,从等待通知阻塞状态转换为等待锁阻塞状态
void signalAll();
一个Condition实例的内部实际上维护了一个队列,队列中的节点表示由于(某些条件不满足而)线程自身调用await方法阻塞的线程。Condition接口中有两个重要的方法,即 await方法和 signal方法。线程调用这个方法之前该线程必须已经获取了Condition实例所依附的锁。这样的原因有两个:
(1)对于await方法,它内部会执行释放锁的操作,所以使用前必须获取锁。
(2)对于signal方法,是为了避免多个线程同时调用同一个Condition实例的singal方法时引起的(队列)出列竞争。下面是这两个方法的执行流程。
await方法:
1. 入列到条件队列(注意这里不是等待锁的队列)
2. 释放锁
3. 阻塞自身线程
------------被唤醒后执行-------------
4. 尝试去获取锁(执行到这里时线程已不在条件队列中,而是位于等待(锁的)队列中,参见signal方法)
4.1 成功,从await方法中返回,执行线程后面的代码
4.2 失败,阻塞自己(等待前一个节点释放锁时将它唤醒)
注意:await方法时自身线程调用的,线程在await方法中阻塞,并没有从await方法中返回,当唤醒后继续执行await方法中后面的代码(也就是获取锁的代码)。可以看出await方法释放了锁,又尝试获得锁。当获取锁不成功的时候当前线程仍然会阻塞到await方法中,等待前一个节点释放锁后再将其唤醒。
signal方法:
1. 将条件队列的队首节点取出,放入等待锁队列的队尾
2. 唤醒该节点对应的线程
注意:signal是由其它线程调用
Lock和Condition的使用例程
下面这个例子,就是利用lock和condition实现B线程先打印一句信息后,然后A线程打印两句信息(不能中断),交替十次后结束:
public class ConditionDemo {
volatile int key = 0;
Lock l = new ReentrantLock();
Condition c = l.newCondition();
public static void main(String[] args){
ConditionDemo demo = new ConditionDemo();
new Thread(demo.new A()).start();
new Thread(demo.new B()).start();
}
class A implements Runnable{
@Override
public void run() {
int i = 10;
while(i > 0){
l.lock();
try{
if(key == 1){
System.out.println("A is Running");
System.out.println("A is Running");
i--;
key = 0;
c.signal();
}else{
c.awaitUninterruptibly();
}
}
finally{
l.unlock();
}
}
}
}
class B implements Runnable{
@Override
public void run() {
int i = 10;
while(i > 0){
l.lock();
try{
if(key == 0){
System.out.println("B is Running");
i--;
key = 1;
c.signal();
}else{
c.awaitUninterruptibly();
}
}
finally{
l.unlock();
}
}
}
}
}
Lock与synchronized的区别
1. Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的
2. 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
3. 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
4. synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式。