并发编程中,我们常用synchronized关键字来实现上锁,synchronized是java中的一个关键字,属于Java语言本身的内置特性,但是其在实际使用中存在一定的缺陷。
例如:一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。
如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再比如,当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象,这种情况也会影响效率。而利用Lock则可以解决这类问题。
另外,synchronized和lock相比,synchronized是Java语言的内置特性,而Lock是一个java接口,通过这个类来实现同步访问。还有一点极其重要的是synchronized是不需要手动释放锁的,而Lock是需要在用完之后手动释放锁的(常用方式是加try catch finally语句,在finally中释放锁)
1、Lock
接下来,看代码,显示JDK1.8中示例的Lock使用:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
在Lock接口中的方法有:
lock(),与lockInterruptibly(),返回值是void。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
利用Rentrantlock来实现Lock的各个方法:
public class LockTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
TestData testData = new TestData() ;
Thread t1 = new Thread(testData);
Thread t2 = new Thread(testData);
t1.start();
t2.start();
}
}
class TestData implements Runnable{
public ArrayList<Integer> arrayList = new ArrayList<Integer>();
Lock lock = new ReentrantLock();
public void insert() throws InterruptedException {
lock.lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(Thread.currentThread().getName()+"正在进行写操作");
}
System.out.println(Thread.currentThread().getName()+"写操作完毕");
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
lock.unlock();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
insert();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
运行的结果如下:
线程0先获得锁,运行完毕之后释放锁,线程1再获得锁进行运行。而利用reyLock(),可以获得是否得到锁的信息。
public void insert() throws InterruptedException {
if(lock.tryLock()) {
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(Thread.currentThread().getName()+"正在进行写操作");
}
System.out.println(Thread.currentThread().getName()+"写操作完毕");
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName()+"获取锁失败");
}
}
上面,我们只是加入了tryLock(),来判断是否能得到锁,当得到锁就开始运行,得不到则返回false,且不在申请了:
我们还可以使用lockInterruptibly()响应中断:
public static void main(String[] args) {
// TODO Auto-generated method stub
TestData testData = new TestData() ;
Thread t1 = new Thread(testData);
Thread t2 = new Thread(testData);
t1.start();
t2.start();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
}
class TestData implements Runnable{
public ArrayList<Integer> arrayList = new ArrayList<Integer>();
Lock lock = new ReentrantLock();
public void insert() throws InterruptedException {
lock.lockInterruptibly();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(Thread.currentThread().getName()+"正在进行写操作");
}
System.out.println(Thread.currentThread().getName()+"写操作完毕");
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
lock.unlock();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
insert();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
Lock接口及其实现类ReentrantLock的基本使用如上。
此外还有ReadWriteLock接口及其实现类ReentrantReadWriteLock,这个是为了实现读写锁,来满足读写的时候锁的。
class TestData implements Runnable{
public ArrayList<Integer> arrayList = new ArrayList<Integer>();
ReadWriteLock lock = new ReentrantReadWriteLock();
public void insert() throws InterruptedException {
lock.writeLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(Thread.currentThread().getName()+"正在进行写操作");
}
System.out.println(Thread.currentThread().getName()+"写操作完毕");
} catch (Exception e) {
// TODO: handle exception
} finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
lock.writeLock().unlock();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
insert();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}
上面就是加了写锁writeLock的实现示例,运行结果和正常上锁差不多,但是读锁readLock在应用时就可以解决A线程使用锁时B线程等待的问题,利用读锁,两线程可以同时使用,提高效率。
2、Lock和synchronized的不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
3、其他一些锁的概念:
1)可重入锁: synchronized和lock都是可重入锁。意思是如果线程A使用synchronized method1方法申请了锁,此时有需要使用synchronized method2方法,这时就不需要在去申请锁了(因为可能A已经持有了对象的锁、再申请会申请不到,进而一直阻塞)。
2)可中断锁:synchronized不是可中断锁,lock是可中断锁。lock接口的lockInterruptibly()的用法时已经体现了Lock的可中断性,意思是在等待锁时,若线程不想等了,可以中断,不申请了。
3)公平锁:公平锁即体现等待时间长的锁优先申请到,lock接口的实现类ReentrantLock及ReadWriteLock接口及其实现类ReentrantReadWriteLock,可以在new时加true来实现公平锁。
3)读写锁:读写锁则是ReadWriteLock接口及其实现类ReentrantReadWriteLock,实现读和读的时候不发生冲突。