Java并发编程:(3)synchronized和Lock

时间:2021-03-10 20:53:29

 

1 线程安全问题

思考这样一个问题

        单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现多个线程同时访问同一个临界资源(或共享资源:一个变量、一个对象、一个文件、一个数据库表)情况,多个线程并发执行过程不可控,很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。

例如

         当线程A读取到一个数据D的时候,然后开始使用,但是有可能在使用前,线程B改变了数据D,导致线程A读取使用的数据和此时实际的数据值不一致(数据库中叫做读取了脏数据)。

还有很多其他的情况,这里不一一列举了。

 

那么如何解决线程安全问题的呢?

      大部分并发模式解决线程安全问题都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

通常采取的做法:在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问该临界资源。

在Java中,提供了两种方式来实现同步互斥访问:synchronizedLock

 

2 synchronized方式

先明白一个概念:互斥锁---达到多个线程对同一个资源互斥访问的目的。

Java中的对象都有一个锁标记(monitor,也叫监视器),当多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

synchronized作为一个Java中的关键字,可以通过给一个对象标记一个方法代码块,来达到加互斥锁的目的;当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的互斥锁,其他线程暂时无法访问该对象,处于等待状态。只有当前有用互斥锁的线程释放该对象的锁后,其他线程才能执行这个方法或者代码块。

2.1 synchrozied修饰方法 

下面是一个两个线程同时插入数据的例子: 

public class Test { 
public static void main(String[] args) {
final InsertData insertData = new InsertData();

new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();

new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}

class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();

public void insert(Threadthread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}

结果:两个线程同时调用了InsertData对象的insert方法插入数据,并且过程是不可控的,两者插入顺序也不可控制,可能出现交叉的插入顺序;

           Java并发编程:(3)synchronized和Lock


如果给insert方法加上synchronized关键字修饰:

class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();

public synchronized void insert(Threadthread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}

结果:只有线程0调用该方法执行完毕之后,释放了互斥锁,线程1才能拿到锁调用该对象方法。

          Java并发编程:(3)synchronized和Lock

总结:

1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法(使用到临界资源);

     当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法(不会使用到临界资源)。

2)如果线程A需要访问对象object1的synchronized方法fun1,另外线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型,也不会产生线程安全问题,因为他们访问的是不同的对象,不存在互斥问题。

3)另外,如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问staticsynchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

上述第三点的例子如下:

public class Test { 
public static void main(String[]args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}

class InsertData {
public synchronized void insert(){
System.out.println("执行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行insert完毕");
}

public synchronized static void insert1() {
System.out.println("执行insert1");
System.out.println("执行insert1完毕");
}
}

            Java并发编程:(3)synchronized和Lock

 

        从上述执行结果可以知道:访问非static synchronized方法占用的对象锁,不会阻塞访问static synchronized方法占用的类锁,可以理解为类锁的优先级更高。 

2.2 synchronized修饰代码块

Synchronized修饰代码块的形式:       synchronized(synObject){ 代码 }

当线程A执行这段代码块时,会获取对象Object的互斥锁,则其他线程无法同时访问;Object可以是this也可以是当前对象的一个属性,表示当前对象的锁和该属性的锁。

例如:

class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();

public void insert(Threadthread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();

public void insert(Threadthread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}

较之synchronized方法,synchronized代码块更加灵活;因为一个方法中可能只需要同步一部分代码,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以有针对性的同步需要同步的地方,提高执行效率。

 

总结两种方式:

1) 每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。

2) synchronized方法和synchronized代码块出现异常时,JVM会自动释放当前线程占用的锁,不会出现死锁现象。

 

 

3 Lock方式

既然可以通过synchronized来实现同步访问了,为什么又提供Lock方式了呢?

     是因为Synchronized方式有自己的缺陷,synchronized是Java语言内置的关键字。

     使用synchronized修饰的方法或代码块,被一个线程占用的时候,只能通过两种方式释放锁:执行完毕释放锁发生异常JVM让线程释放锁

     但是,如果拥有锁的线程因为某种原因等待资源而一直处于等待状态而不释放锁,导致其他线程一直阻塞无法得到锁,这样很影响执行效率。

再比如说:有的方法代码块可以多个线程同时访问,但是不会影响到彼此,又因为synchronized修饰只能单线程访问,这样也很影响效率。

 

但是, Lock提供了比synchronized更多的功能:

1)Lock不是Java语言内置的, Lock是一个类,通过这个类可以实现同步访问,synchronized是Java语言内置的关键字。

2)synchronized方式在方法或代码块执行前后线程自动加减锁;而Lock方式则必须要用户去手动释放锁,否则就有可能导致出现死锁现象(发生异常也不会自动释放锁)。

3)Lock方式的lockInterruptibly()方法获取某个锁时,等待状态的线程可以响应中断;但是synchronized方式的等待线程无法被中断,只有一直等待下去。

 

3.1 Lock

Lock类位于java.util.concurrent.locks包下,是一个接口:

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
ConditionnewCondition();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的;unLock()方法是用来释放锁的。


1)lock()方法  是平常使用得最多的获取锁的形式,如果锁已被其他线程获取,则进行等待。

因为Lock形式不管是否发生异常,必须手动去释放锁。因此,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中以保证锁一定被被释放,防止死锁发生。

Lock同步形式:

Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){

}finally{
lock.unlock(); //释放锁
}

2)tryLock()方法 是有返回值的---true表示锁获取成功,false表示锁获取失败(即锁已被其他线程获取);即使拿不到锁也不会一直等待,会立即返回。

获取锁的形式:

Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exceptionex){

}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}

3)lockInterruptibly()方法 比较特殊,表示处于等待获取锁的线程可以中断其等待状态。线程B使用lock.lockInterruptibly()获取锁时处于等待状态,此时可以使用threadB.interrupt()方法中断线程B的等待过程。形式如下:

public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}

但是,获取了锁的线程不会被interrupt()方法中断的---单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。


3.2 ReentrantLock

  ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且提供了更多的方法。

使用例子:

1) lock()的正确使用方法

public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方
public static void main(String[] args) {
final Test test = new Test();

new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();

new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}

public void insert(Thread thread){
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
//TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}

        lock不能作为局部变量,要作为类变量,否则每个线程调用了该方法都会保存局部变量--副本,都会获取不同的锁,不会互斥,也不会发生冲突,就失去了锁的意义。

2)lockInterruptibly()响应中断的使用方法:

 public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[]args) {
Testtest = new Test();
MyThreadthread1 = new MyThread(test);
MyThreadthread2 = new MyThread(test);
thread1.start();
thread2.start();

try {
Thread.sleep(2000);
} catch (InterruptedExceptione) {
e.printStackTrace();
}
thread2.interrupt();
}

public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName()+"得到了锁");
long startTime =System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis()- startTime >= Integer.MAX_VALUE)
break;
//插入数据
}
}
finally {
System.out.println(Thread.currentThread().getName()+"执行finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test= test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedExceptione) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}

2. 3 ReadWriteLock

ReadWriteLock是一个接口,只定义了两个方法:

public interface ReadWriteLock {    
LockreadLock(); //获取读锁
LockwriteLock();//获取写锁
}

将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

 

2.4 ReentrantReadWriteLock

ReentrantReadWriteLock类实现了ReadWriteLock接口,提供了很多丰富的方法,主要的方法:readLock()和writeLock()用来获取读锁写锁

ReentrantReadWriteLock具体用法:

      如果多个线程及逆行读取操作,使用synchronized关键字只有一个线程处于读取状态,其他都处于等待获取锁的状态,但是使用ReentrantReadWriteLock类的读写锁就不同了,多个线程可以同时进行读操作,可以大大的提升效率。

public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();

new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}

public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();

while(System.currentTimeMillis()- start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}

注意:

1)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则要等待读锁被释放。

2)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则要等待写锁被释放。

这两种性质都防止了读取的数据被纂改,读取到不一致的数据。


4 synchronized和Lock的选择

Lock和synchronized有以下几点不同:

  1)synchronized是Java中的关键字,由内置的语言实现;Lock是一个接口。

2)synchronized会在方法或代码块前后自动加解锁,发生异常时JVM会自动释放线程占有的锁,不会导致死锁现象发生;而Lock需要手动加解锁,发生异常时,需要在finally块中通过lock.unLock()方法释放锁,否则可能造成死锁。

  3)使用synchronized时,等待的线程会一直等待下去,不能够响应中断;Lock可以让等待锁的线程响应中断。

  4)synchronized方式无法得知是否获取到锁,Lock方式则可以办到。

  5)相比于synchronized方式,Lock方式可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

 

5 锁


  1.可重入锁

具备可重入性的锁称作可重入锁;synchronized和ReentrantLock都是可重入锁。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程已经拥有了当前对象的锁,如果去重新申请锁,就会出现得不到锁的思索现象,所以不必重新去申请锁可以直接执行synchronized方法method2。

所以,可以把可重入性理解为:以线程为分配主体的锁分配机制。


  2.可中断锁

  顾名思义,可以相应中断的锁就是可终端锁;synchronized不是可中断锁,而Lock是可中断锁。

  前面lock.lockInterruptibly()方法的用法就体现了Lock的可中断性,具体用法前面已经叙述过了。


  3.公平锁

  公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

  非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  synchronized就是非公平锁,Lock方式的两个实现类ReentrantLock和ReentrantReadWriteLock默认情况下也是非公平锁,都无法保证等待的线程获取锁的顺序。

但是Lock方式可以设置为公平锁:

ReentrantLocklock = new ReentrantLock(true);

因为ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

  默认情况下,如果使用无参构造器,则是非公平锁。

  

  另外在ReentrantLock类中定义了很多方法,比如:

  isFair()        //判断锁是否是公平锁

  isLocked()    //判断锁是否被任何线程获取了

  isHeldByCurrentThread()   //判断锁是否被当前线程获取了

  hasQueuedThreads()   //判断是否有线程在等待该锁

  在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。


  4.读写锁

  读写锁将对一个资源(比如文件)的访问分成了2个锁:读锁和写锁,才使得多个线程之间的读操作不会发生冲突。

  读写锁ReadWriteLock是一个接口,ReentrantReadWriteLock类则实现了这个接口。

  可以通过readLock()获取读锁,通过writeLock()获取写锁。