1、两者所处层面不同
synchronized是Java中的一个关键字,当我们调用它时会从在虚拟机指令层面加锁,关键字为monitorenter和monitorexit
Lock是Java中的一个接口,它有许多的实现类来为它提供各种功能,加锁的关键代码为大体为Lock和unLock;
2、获锁方式
synchronized可对实例方法、静态方法和代码块加锁,相对应的,加锁前需要获得实例对象的锁或类对象的锁或指定对象的锁。说到底就是要先获得对象的监视器(即对象的锁)然后才能够进行相关操作。
Lock的使用离不开它的实现类AQS,而它的加锁并不是针对对象的,而是针对当前线程的,并且AQS中有一个原子类state来进行加锁次数的计数
3、获锁失败
使用关键字synchronized加锁的程序中,获锁失败的对象会被加入到一个虚拟的等待队列中被阻塞,直到锁被释放;1.6以后加入了自旋操作
使用Lock加锁的程序中,获锁失败的线程会被自动加入到AQS的等待队列中进行自旋,自旋的同时再尝试去获取锁,等到自旋到一定次数并且获锁操作未成功,线程就会被阻塞
4、偏向或重入
synchronized中叫做偏向锁
当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。
Lock中叫做重入锁
AQS的实现类ReentrantLock实现了重入的机制,即若线程a已经获得了锁,a再次请求锁时则会判断a是否持正有锁,然后会将原子值state+1来实现重入的计数操作
5、Lock独有的队列
condition队列是AQS中的一个Lock的子接口的内部现类,它一般会和ReentrantLock一起使用来满足除了加锁和解锁以外的一些附加条件,比如对线程的分组和临界数量的判断(阻塞队列)
6、解锁操作
synchronized:不能指定解锁操作,执行完代码块的对象会自动释放锁
Lock:可调用ulock方法去释放锁比synchronized更灵活
来看下面具体例子:
public class DeadLockTest implements Runnable{
//final ReentrantLock lock = new ReentrantLock();
static String source1 = "A";
static String source2 = "B";
public void run() {
if(Thread.currentThread().getName().equals("1")) {
synchronized(source1) {
//lock.lock();
System.out.println("线程1获取资源1锁");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//lock.lock();
synchronized(source2) {
System.out.println("线程1获取资源2锁");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//lock.unlock();
//lock.unlock();
}else {
synchronized(source2) {
//lock.lock();
System.out.println("线程2获取资源2锁");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(source1) {
//lock.lock();
System.out.println("线程2获取资源1锁");
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//lock.unlock();
//lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
DeadLockTest run = new DeadLockTest();
new Thread(run,"1").start();
new Thread(run,"2").start();
}
}
上述代码使用JVM关键字synchronized简单的实现了一个死锁的效果
我们先来分析一下使用synchronized关键字得出的结果:
结果:
线程1获取资源1锁
线程2获取资源2锁
分析:
我们由两个资源source1和source2、两个线程1和2,这两个线程都想获取资源source1和source2
首先,最好先将两个资源source1和source2设置为static属性,让两个线程能够共享它
source1和source2都是String类型的,因为synchronized关键字修饰的应该是一个对象,普通的变量(int、float等)不能获取对象锁
接下来,两个资源
1、线程1使用synchronized来对资源source1加锁(即资源source1尝试获取对象锁),成功后获取资源source1,接下来调用sleep方法阻塞,但不释放对象锁
2、线程2使用synchronized来对资源source2加锁,成功后获取资源source2,接下来调用sleep方法阻塞,但不释放对象锁
3、线程1醒来,再次使用synchronized来对资源source2加锁,因为资源source2已被线程2所占用,资源source2还没有释放自己的锁,所以线程1阻塞,等待资源source2释放锁
4、线程2醒来,再次使用synchronized来对资源source1加锁,因为资源source1已被1所占用,资源source1还没有释放自己的锁,所以线程2阻塞,等待资源source1释放锁
5、线程1和线程2都等待着资源source1和资源source2释放锁,导致死锁
接下来,我们将synchronized关键字注释掉,注释掉的ReentrantLock锁对象,然后打开lock和unlock的注释,运行结果如下:
线程2获取资源2锁
线程2获取资源1锁
线程1获取资源1锁
线程1获取资源2锁
1、线程2尝试获取ReentrantLock对象lock,成功后加锁,并使用资源2,然后睡眠,阻塞自己
2、线程1尝试获锁去lock,因为线程2正持有此对象并且还未释放锁,所以线程1获锁失败,并被加入到等待队列中然后阻塞
3、线程2醒来,再次尝试获锁,因为ReentrantLock可重入,所以再次获锁成功,于是线程2使用资源1,然后两次释放锁。释放完毕后,会唤醒在等待队列中等待的线程1
4、线程1尝试获锁,因为ReentrantLock对象空着,所以获锁成功,然后执行流程如上述第三步
那么同样的代码使用ReentrantLock为什么不会产生死锁呢?
第一、 因为synchronized关键字是JVM为我们提供的属性,sleep方法也是一个native方法,两者确实应该配套使用。而ReentrantLock锁是Java类库中提供的一个类,使用Java代码来模拟锁,synchronized和ReentrantLock在层次级别上不同
第二、 两者实现原理稍有不同。synchronized修饰的必定是一个对象,并且每一个对象都有着属于自己的锁记录指针,我们每次加锁或释放锁都是修改此指针的指向
而ReentrantLock的内部类实现了AQS部分方法,实现了对线程操作的功能。每次加锁失败会将当前线程放入等待队列中,并阻塞它;否则,当前尝试获取资源的线程可以无阻的执行
即synchronized操作的是对象,ReentrantLock操作的是线程
ReentrantLock产生死锁的过程:
public class IntLock implements Runnable{
private boolean flag;
private static ReentrantLock lock1=new ReentrantLock();
private static ReentrantLock lock2=new ReentrantLock();
public IntLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
try {
if(flag){
lock1.lock();
System.out.println(flag + "线程获取了Lock1");
Thread.currentThread().sleep(100);
lock2.lock();
System.out.println(flag+"线程获取了Lock2");
}else{
lock2.lock();
System.out.println(flag + "线程获取了Lock2");
Thread.currentThread().sleep(100);
lock1.lock();
System.out.println(flag+"线程获取了Lock1");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new IntLock(true));
Thread thread2=new Thread(new IntLock(false));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("主线程已结束");
}
}
以上代码创建了两个ReentrantLock对象,同时开启两个副线程去进行加锁,每个线程对应各自的ReentrantLock对象。当线程1持有了它的锁对象lock1,并且线程2持有了它的对象锁lock2后两个线程再次尝试获取对方的对象锁时,因为ReentrantLock为排他锁(即一把锁同时只能被一个线程锁持有),所以其它线程去获取当前线程的ReentrantLock时就会被放入此ReentrantLock的等待队列中被阻塞,这样就造成了死锁。
我们使用ReentrantLock时最好用static来修饰它的对象来确保此对象被所有线程共享
附上死锁产生原因和避免死锁方法:
以下资源来自网络:
死锁产生条件:
1、 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2、 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
3、 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
4、 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。