一篇文章了解Lock和Synchronized的区别和详细代码验证案例(Java)1.0

线程运行的状态

我们的锁都是在线程运行层面操作的,所以我们必须先了解线程有哪些运行状态,下图比较粗略的描述了线程的运行状态。
在这里插入图片描述
"New"和"Terminated"这两种状态其实并不是线程的状态,而是java.lang.Thread对象的状态。可以说,处于"新建"和"终止"状态的"线程"其实并不是线程,而只是一个代表着线程对象而已。这里我们就不把它们作为线程状态,接下来就剩4种状态,分别介绍。

  1. RUNNABLE:对应ReadyRunning两种状态,也就是说处于就绪和运行状态的线程在java.lang.Thread中都表现为"RUNNABLE"。
  2. BLOCKED:为阻塞状态,此线程需要获得某个锁才能继续执行,而这个锁目前被其他线程持有,所以进入了被动的等待状态,直到抢到了那个锁,才会再次进入Runnable状态。
  3. WAITING:为阻塞状态,表示此线程正处于无限期的主动等待中,直到有人唤醒它,它才会再次进入Runnable状态。
  4. TIMED_WAITING:为阻塞状态,表示此线程正处于有限期的主动等待中,要么有人唤醒它,要么等待够了一定时间之后,才会再次进入Runnable状态。
一、关键字和API的区别

synchronized是关键字,属于JVM层面。而Lock是java.util.concurrent.locks包的API层面。
我们看看它们的底层的字节码信息,写好Java代码,使用javap.exe打印出字节码文件信息。

public class Test {
    public static void main(String[] args) {

        synchronized (Test.class){

        }

        Lock lock = new ReentrantLock();
    }
}
Compiled from "Test.java"
public class 关键字volatile.Test {
  public 关键字volatile.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class 关键字volatile/Test
       2: dup
       3: astore_1
       4: monitorenter
       5: aload_1
       6: monitorexit
       7: goto          15
      10: astore_2
      11: aload_1
      12: monitorexit
      13: aload_2
      14: athrow
      15: new           #3                  // class java/util/concurrent/locks/ReentrantLock
      18: dup
      19: invokespecial #4                  // Method java/util/concurrent/locks/ReentrantLock."<init>":()V
      22: pop
      23: return
    Exception table:
       from    to  target type
           5     7    10   any
          10    13    10   any
}
  1. synchronized
    4: monitorenter:加锁
    6: monitorexit:正常退出,释放
    12: monitorexit:异常退出,保证退出释放
  2. lock
    15: new #3 // class java/util/concurrent/locks/ReentrantLock
二、自动释放锁

synchronized修饰的同步代码块运行完毕后,自动释放锁。而lock锁需要自行释放,不然可能会导致死锁,下面是lock的使用案例,来自于java.util.concurrent.CopyOnWriteArrayList.add()方法。lock一般和finally联合使用,避免死锁的情况发生。

public boolean add(E var1) {
        ReentrantLock var2 = this.lock;
        var2.lock();

        boolean var6;
        try {
        	//获取当前的数组
            Object[] var3 = this.getArray();
            //获取当前数组的长度
            int var4 = var3.length;
            //创建出一个新的数组,复制与var3,且长度+1
            Object[] var5 = Arrays.copyOf(var3, var4 + 1);
            //新添加的元素追加到新的数组里
            var5[var4] = var1;
            //设置新的数组到集合中
            this.setArray(var5);
            //完成返回true
            var6 = true;
        } finally {
        	//最后释放锁
            var2.unlock();
        }

        return var6;
    }
三、公平与非公平

公平锁:多线程排队获取锁,先来先得
非公平锁:多线程无序争抢锁,抢到算得,其吞吐量大于公平锁吞吐量。
synchronized为非公平锁,无法修改。lock默认为非公平锁,可以修改为公平锁,可以结合具体的业务需求求最优。我们来看看lock的源码。我们直接new ReentrantLock(),它的无参构造默认是NonfairSync(),也可以入参boolean,true为FairSync(),否则NonfairSync()。

public class ReentrantLock implements Lock, Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final ReentrantLock.Sync sync;

    public ReentrantLock() {
        this.sync = new ReentrantLock.NonfairSync();
    }

    public ReentrantLock(boolean var1) {
        this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
    }
}
四、获取锁的阻塞可以中断和设置超时时间

理解这块知识点前需要先了解线程中的interrupt()方法,一步一步来。Thread.interrupt()是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。(注意:线程中断仅仅是设置线程的中断状态位,不会停止线程)。

当一个线程处于中断状态时,并且还在的阻塞的状态,那么JVM会将线程的中断标志重新设置为false,线程继续执行,但是会抛出一个InterruptedException异常,然后开发人员可以在catch中捕获并处理异常(比如直接运行完毕,退出线程。本质就是线程阻塞中,被打断,抛出异常,程序员在catch中捕获,处理后事)。

理解过后,我们写段代码,先测试synchronized,线程在获取锁的过程中被打断,synchronized是如何处理的。t1拿到锁,但是要10秒后释放,t2本来在好好的等待t1锁的释放,阻塞中,谁知突然被中断,合理结果应该是此时t2就不再阻塞,抛出异常,t2线程运行结束。但是实际结果就是,t2仍然阻塞中,10秒后t1释放锁,t2获取锁后,抛出InterruptedException异常。
小结:t2在线程被打断的那一刻,阻塞获取锁已经没有意义,应该立刻结束阻塞,结束线程,释放资源

public class Test {

    public static void main(String[] args)throws Exception {
		//t1获取Test对象的锁,并且拿着锁sleep10秒中,再释放锁,执行完成
        Thread t1 = new Thread(() -> {
            synchronized (Test.class) {
                System.out.println(Thread.currentThread().getName() + " sync");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " completed.");
        }, "t1");
        
		//t2也想获取Test对象的锁,但是被t1拿了不放,只能等待
        Thread t2 = new Thread(() -> {
            synchronized (Test.class) {
                System.out.println(Thread.currentThread().getName() + " sync");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " completed.");
        }, "t2");
		
		//先执行,获取到锁
        t1.start();
        //保证t1一定拿到锁
        Thread.sleep(1000);
        
        //此时t2再去获取锁
        t2.start();
        t2.interrupt();
        System.out.println("t2 interrupted.");
    }
}

结合上面的反例,我们看看lock给我们带来了什么花样,惯例上代码。

下面不再是t1和t2争抢锁,而是t1和main,本质都一样。main线程从lock()方法中获取到锁,和synchronized (Test.class)一个意思,sleep10秒后通过unlock()释放锁,期间,t1用lockInterruptibly()方法在等待获取锁,突然被中断。运行结果就是我们在synchronized 没有实现的结果,t1立即中断阻塞,抛出异常,t1运行结束。

(题外话:将lock.lockInterruptibly()替换成lock.lock(),结果和synchronized的效果一样,可以试试)

public class Test {

    private void test() throws Exception {
        // 创建Lock锁对象
        final Lock lock = new ReentrantLock();
        // main获取锁,不释放
        lock.lock();
        System.out.println(Thread.currentThread().getName() + " sync");

        // t1线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //由于锁已经被main线程获取,且没有释放,t1获取锁时会阻塞这里
                    lock.lockInterruptibly();

                    Thread.sleep(1000);
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getName() + " interrupted.");
                }
                System.out.println(Thread.currentThread().getName() + " completed.");
            }
        }, "t1");

        // t1开始执行,获取锁,然后被阻塞
        t1.start();
        Thread.sleep(1000);

        // t1被打断
        t1.interrupt();

        // main线程再等待10秒中,再释放锁,看看t1是否也会等待10秒中,获取到锁再抛出异常,和synchronized一样
        Thread.sleep(10000);
        //释放锁
        lock.unlock();
    }

    public static void main(String[] args) throws Exception {
        //执行test方法
        new Test().test();
        //main线程运行完毕
        System.out.println(Thread.currentThread().getName() + " completed.");
    }
}

超时时间比较好理解,我们看看Lock接口的源代码,其中tryLock(long var1, TimeUnit var3)获取锁的方法有超时时间,如果阻塞时间超过则抛出异常

public interface Lock {
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

小结:以上两个方式,使我们可以在线程获取锁的阻塞过程中可以更加精细化的控制。

五、条件精确唤醒

synchronized 的锁继承Object,唤醒的方法只有notify()、notifyAll(),要不唤醒1个要不就唤醒全部。

Lock可以实现1到n之间唤醒,结合上面Lock接口的实际源码,具体落地实现是newCondition()方法实现。我们写一段代码案例,来看看Lock的条件唤醒是如何应用的。

public class Test {

    //Test对象资源的锁,多线程访问修改,可以用lock()和unlock()实现同步
    private Lock lock = new ReentrantLock();

    //由lock锁创建出的条件,获取到锁之后,如果条件不满足,条件也可以阻塞当前线程,并且释放lock锁
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    //我们假设的条件。index为多少就运行第几个方法
    private int index = 1;

    public static void main(String[] args) throws Exception {
        Test test = new Test();

        //多线程,有顺序的操作test资源类,按照1、2、3这样执行
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test1();
            }
        }, "t1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test2();
            }
        }, "t2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test3();
            }
        }, "t3").start();
    }

    private void test1() {
        //阻塞获取锁
        lock.lock();
        try {
            //是否符合条件,初始化为1,所以开始就符合条件(while循环防止线程虚假唤醒)
            while (index != 1) {
                //不符合,则释放锁,线程条件阻塞在此,test3运行完成,通知条件c1,c1醒来,判断index=1
                c1.await();
            }
            //index=1,执行方法
            System.out.println(Thread.currentThread().getName() + " sync. invoke test1()");
            Thread.sleep(1000);
            //执行完毕后,修改index,通知条件c2
            index = 2;
            c2.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void test2() {
        lock.lock();
        try {
            while (index != 2) {
                c2.await();
            }
            System.out.println(Thread.currentThread().getName() + " sync. invoke test2()");
            Thread.sleep(1000);
            index = 3;
            c3.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void test3() {
        lock.lock();
        try {
            while (index != 3) {
                c3.await();
            }
            System.out.println(Thread.currentThread().getName() + " sync. invoke test3()");
            Thread.sleep(1000);
            index = 1;
            c1.signal();
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

运行结果,多线程并发顺序执行。如果我们拿synchronized来,怎么实现这个需求呢?

t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()
t1 sync. invoke test1()
t2 sync. invoke test2()
t3 sync. invoke test3()

下面是我拿synchronized来实现的,大家看看比对一下优劣,欢迎评论

public class Test {
	//synchronized的锁对象
    private Object lock = new Object();

    //我们假设的条件。index为多少就运行第几个方法
    private int index = 1;

    public static void main(String[] args) throws Exception {
        Test test = new Test();

        //多线程,有顺序的操作test资源类,按照1、2、3这样执行
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test1();
            }
        }, "t1").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test2();
            }
        }, "t2").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                test.test3();
            }
        }, "t3").start();
    }

    private void test1() {
        synchronized (lock) {
            try {
                while (index != 1) {
                    lock.wait();
                }
                System.out.println(Thread.currentThread().getName() + " sync. invoke test1()");
                Thread.sleep(1000);

                index = 2;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void test2() {
        synchronized (lock) {
            try {
                while (index != 2) {
                    lock.wait();
                }
                System.out.println(Thread.currentThread().getName() + " sync. invoke test2()");
                Thread.sleep(1000);

                index = 3;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void test3() {
        synchronized (lock) {
            try {
                while (index != 3) {
                    lock.wait();
                }
                System.out.println(Thread.currentThread().getName() + " sync. invoke test3()");
                Thread.sleep(1000);

                index = 1;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值