随笔 - 15  文章 - 4  评论 - 1  阅读 - 49869

Java中多线程 synchronized和lock的区别和使用方法

    之前介绍了synchronized实现的多线程同时访问同一对象,但是细心的小伙伴都会查到还有一个lock也可以实现并发的功能,这篇博客就是介绍lock的用法,及lock与synchronized的区别。

  

  lock是在Java5之后,在java.util.concurrent.locks包提供的,我认为用来完成一些synchronized的缺陷而诞生的,所以在说lock的用法前先说下synchronized有哪些缺陷:

  我们都知道在synchronized锁定的代码段中,只要一个线程抢到了锁,其他线程要想得到锁只能等待释放锁,而synchronized释放锁只有两种方式:

    1、抢到锁的线程代码正常运行完,然后线程让出锁;

    2、代码块运行异常,这个时候JVM会让抢到锁的线程释放锁;

如果不在这两种情况下,就不能释放锁,其他线程就得不到锁无法运行,比如等待IO或者其他的(sleep)等阻塞状态,都是无法交出锁的,其他线程都只能等待,这样会很影响运行效率的。

  还有就是IO的时候,多线程读写文件,多线程同时读操作并不冲突,读操作和写操作,写操作和写操作都会有冲突现象。

  这时候如果采用synchronized,

  一个读操作线程在进行,其他读操作线程在等待无法进行读操作。

  所以需要一种可以让多线程同时进行读操作而且线程间不会发生冲突的机制。

  在Java5以后就有了lock,lock可以做到,而且lock还可以知道是不是已获取到锁,这是synchronized做不到的。

  

  lock虽然比synchronized的功能多了很多,但是还是要注意几点的,

  1、lock不是Java语言内置的,synchronized是Java语言的关键字,因此为内置特性,lock是Java语言的一个类,通过这个类可以实现同步访问。

  2、lock和synchronized的不同还在synchronized不需要用户去释放锁,当使用synchronized关键字时,当代码块运行结束之后,系统会自动让线程释放对锁的占用。而lock必须用户来手动释放锁,如果没有主动释放就有可能导致死锁的现象。

  接下来看一下java.util.concurrent.lock包常用的类和接口。

  首先是lock,通过查看源码可以看出,lock是一个接口:

1 public interface Lock {
2     void lock();
3     void lockInterruptibly() throws InterruptedException;
4     boolean tryLock();
5     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
6     void unlock();
7     Condition newCondition();
8 }

 

  lock();lockInterruptibly();tryLock();tryLock(long time,TimeUnit unit);是用来获取锁的,unlock()方法是用来释放锁的,newCondition()方法暂不说,有时间会在其他文章中讲述。

  你会发现在lock接口中声明了四个方法来获取锁,那他们的区别是是什么呢?

  首先是lock();lock()是用的最多的lock方法,用来获取锁,如果锁已经被其他线程获取,则等待,

  由于在前面讲到如果采用Lock,必须主动释放锁,并且在发生异常时,不会自动释放锁。因此,一般使用Lock必须在try{}catch块中进行,并且将释放锁的操作放在finally块中运行,以保证一定被释放,防止死锁的发生。一般使用Lock进行同步的的话,一下吗这种形式使用:

  

复制代码
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
复制代码

 

  tryLock()方法是有返回值的,它表示尝试获取锁,如果获取到返回true,获取不到(锁已经被其他线程获取),返回false,总之tryLock()方法无论是否获取导致都会返回,不会在拿不到锁时等待。

  tryLock(long time,TimeUnit unit)方法跟tryLock()方法差不多,只不过跟tryLock()不同的是tryLock(long time,TimeUnit unit)会等待一段时间,如果一开始就获到锁或者在等待时间内获取到锁,则会返回true,开始或等待时间内没有获取到锁,则返回false。所以,tryLock()的使用一般为:

 

复制代码
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
复制代码

 

  lockInterruptibly()方法比较特殊,当通过这个方法获取锁时,如果线程正在等待获取锁,则这个线程能够相应中断,即中断线程的等待状态。也就是说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程而直接返回,这个时候B线程不用获取锁,而抛出一个InterruptedException。

  因为lockInterruptibly()方法有异常抛出,所以lockInterruptibly()方法必须放在try块中,或着在调用lockInterruptibly

()方法的方法外声明抛出InterruptedException。

  lockInterruptibly()方法使用方法一般如下

 

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

 

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法

不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过locklnterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下才能相应中断的。

  而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一只等待下去。

  2.ReentrantLock

ReentrantLock,意思是“可重入锁”,可重入锁即多层锁,虽然都获取到了锁,但不是同一个,理解起来类似递归的一种概念。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例具体看一下如何使用ReentrantLock,

例子1,Lock()的正确使用方法

复制代码
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    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 = new ReentrantLock();    //注意这个地方
        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();
        }
    }
}
复制代码

 

Thread-0得到了锁
Thread-1得到了锁
Thread-0释放了锁
Thread-1释放了锁

这就看到了先有两次得到锁,再有两次释放锁,有的同学会想到为什么会先有两次得到锁,不应该先得到一次锁,释放之后才能再次得到锁吗?这是因为这个锁是在方法内的局部锁,所以此方法无法实现线程同步。两个线程是无阻塞无等待,可以同时访问此方法,所以结果是先得到两个锁,再释放两个锁。如果想实现结果是先得到一次锁,释放之后才能再次得到锁,应如下实例

复制代码
public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    
    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();
        }
    }
}
复制代码

 

 

 

 

posted on   粒子少爷  阅读(346)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示