Java -- 浅谈“同步锁”和“死锁”

一、实现多线程的三种方式

        博主在这啰嗦一下,网上也能搜索到,很多人可能知道多线程是什么,怎么开启一个多线程,但是如果要问你实现多线程的方式有哪几种,可能你会顿一下,要想准确地回答出这个问题,还真不是靠死记硬背就能记住的,我在这再重申一下,博文中也会提到前两种的实现方式,至于第三种,本篇不会涉及到,感兴趣的可以自己下来尝试一下;

(1) 继承Thread类



(2) 实现Runnable接口


(3)使用ExecutorService、Callable、Future实现有返回结果的多线程

   此处略................   ..................

 


二、Synchronize 是什么 ?

通俗易懂的解释:

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码

摘自知乎


三、Synchronize的作用

       根据上述言简意赅的解释,我们已经知道了这“家伙的”用途了,说抽象点,就是给修饰的对象(可以是方法、对象、或者是段代码段)加了一把"",什么是锁,我打个比方吧:假如你要上厕所,厕所只有一间,一间只有一个坑,上厕所的人可不只有你一个,怎么办,难道大家都拥挤进来,共享这一个坑吗? 当然不是,我们最惬意的方式就是一个人独享厕所,为了做到独享,我们需要排队(先获得锁的人有优先蹲坑权),为了不让其他人在自己蹲坑的时候闯进来,我们需要在上厕所的时候给门上把锁,把其他人"锁"在外面,防止自己在蹲坑的时候有人不遵守规矩"硬"闯进来;这样一来的话,只有等我们上完出来,把锁打开(释放锁)后,下一个人才能进来,独享他自己的蹲坑时间;


   

        当然,程序中锁的释放不是由我们自己写代码手动控制的(区别于Lock接口中的unlock方法),而是由JVM说的算的,如果同步块中的代码异常的话,JVM会主动释放当前线程获得的锁,如果线程顺利执行完毕后,JVM也会主动释放锁,反之,如果线程持有对象的锁却始终处于dosomething状态时,那么其他想要获得该对象锁的线程则会一直处于wait状态,即阻塞在那;

       


        就好比,你一个人占用厕所一直不出来,结果就是,后面的人都进不来,于是乎,一个小时,两个小时过去了,当排队的人都等不及的时候,他们可能会踹门而入,也有可能会报警;当然程序中,如果线程阻塞在那的话,我们除了祈求他只是处理慢了点外只能等了,要是后面的线程处理的任务很重要的话,那这个等就要命了,怎么办,只能kill掉整个进程了,接下来就是排查代码到底是哪个环节出错了;这就是synchronize的弊端,如果线程拿到锁不作为一直阻塞在那的话,其他线程只能等待了,等到地老天荒、海枯石烂,我去,我要是排在后面那个wait他的线程,说什么我都要知道是谁TM给我堵那了!我要Kill他!

     


        当然,还有一种锁叫ReentrantLock,区别于synchronize,这个锁是一个类,实现了Lock接口,我们看下Lock接口中有哪些方法:

       这个锁就很厉害了,我们完全可以做到自主控制锁;比如每次养成好的习惯在finally块中写unlock();比如线程执行时,先tryLock(),看看锁有没有被占用,如果占用的话,线程也不能闲着,可以去干其他事情,而且还提供了带参数的tryLock(Long,TimeUnit),可以在多少时间内拿不到锁的话去执行其他事情;再比如调用lockInterruptibly()可以中断阻塞的线程而不必让线程一直在那等等等,等到海枯石烂....本篇不对这个锁做详细的案列分析,后面有时间再单开博文说明吧;


四、知识扩充(start() 和 run() 的区别)

  /**
     * 一定要区别于start()和run()方法
     * (1)run()为Runnable接口中的方法
     * (2)Thread线程类实现了Runnable接口
     * (3)Thread线程类中包含了一个Runnable的实例target,run()方法真正是由target实现的【target.run()】
     * (4)start()方法为Thread类中的方法,其加了synchronized关键字,防止一个同步操作被多次启动
     * (5)start()方法内部实现机制调用的是本地库方法(native)
     * (6)start是开启一个线程,可以达到多线程执行的效果,start后并不会立马执行线程,而是
     *      交给cpu来调度
     * (7)run只是一个普通的方法,执行它,程序依然只有一个主线程
     *      且run方法中的代码块执行完后,才能执行下面的代码,没有真正达到多线程执行的效果
     */

  我们基于上面的注释说明,来看一段demo:

package com.appleyk.dbinit.MyLock;

/**
 * <p>start()和run()的区别</p>
 *
 * @author appleyk
 * @version V.1.0.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on 下午 6:04 2019-7-1
 */
public class DeadLock0 {

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " -- 我执行了!");
            }
        });

        long start = System.currentTimeMillis();
        thread.start(); // 异步的,开启一个线程后,并不会立马执行线程,而是扔给cpu进行调度,因此下面的内容继续执行,不受影响
        System.out.println("耗时:"+( System.currentTimeMillis()-start));
        start = System.currentTimeMillis();
        thread.run();  // 同步的,必须执行完run方法块中的代码才能继续执行下面的内容(阻塞)
        System.out.println("耗时:"+( System.currentTimeMillis()-start));

    }

}

  直接来看输出结果:


五、什么是死锁

(1)产生死锁的四个必要条件

A、互斥使用(资源独占) 


      一个资源每次只能给一个线程使用 

      说明:对象锁只有一把,同一时间,只能有一个线程持有,其他线程需等待



B、不可强占(不可剥夺) 


      资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放 

      说明:当线程A拿到对象锁时,线程B除了等待线程A主动释放对象锁时,什么都干不了(想都别想



C、请求和保持
   

      一个线程在申请新的资源的同时保持对原有资源的占有 

     说明:线程A原本持有对象M的锁,但又想要申请获取对象N的锁,这时候,如果获得对象N的锁遇到阻塞时,就会导致线程A

     原本持有的对象M的锁无法得到释放,这就导致其他想要获取对象M锁的线程陷入无限的等待中



D、循环等待 


     存在一个线程等待队列  {T1 , T2 , … , Tn},,其中T1等待T2释放占有的资源,T2等待T3释放占有的资源,…,Tn等待T1释放       占有的资源,形成一个线程等待环路

     说明:A说B写的模块代码有问题,B说C写的模块代码有问题,C又反过来说是A写的不对,卧槽,这..... egg疼!


(2)如何避免死锁

       上述产生死锁的四个必要条件只要有一个不成立,就可以推翻或者排除出现死锁的可能,因此,我们在使用多线程开发程序之前,一定要好好设计和斟酌一下,防止写出来的程序在线程调度上出岔子,造成死锁就麻烦了,慎重


 

(3)小结

        什么是死锁:两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去 (摘自知乎)


六、手写"死锁"

前言:知道了死锁是什么后,接下来我们就针对死锁的特性,手写一段代码,模拟一下两个线程互相抢夺资源无果造成无限等待的场景;


 

(1) 代码1 -- 继承Thread类

package com.appleyk.dbinit.MyLock;

/**
 * <p>手写死锁 -- 方式1</p>
 *
 * @author appleyk
 * @version V.1.0.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on 上午 8:14 2019-7-1
 */
public class DeadLock1 {

    public static void main(String[] args) {

        final Object obj1 = new Object();
        final Object obj2 = new Object();

        MyThread1 thread1 = new MyThread1(obj1, obj2);
        MyThread2 thread2 = new MyThread2(obj1, obj2);

        // 开启多线程,模拟死锁
        thread1.start();
        thread2.start();

    }
}

class MyThread1 extends Thread{

    private final Object obj1 ;
    private final Object obj2 ;

    public MyThread1(Object obj1 , Object obj2) {
        this.obj1 = obj1 ;
        this.obj2 = obj2;
    }

    @Override
    public void run() {

        // 线程1获得obj1对象的锁(其他线程等待arr1对象锁释放)
        synchronized(obj1){

            System.out.println(Thread.currentThread().getName()+"--已获得对象obj1的锁,准备获得对象obj2的锁");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            /**
             * 1、线程1获得obj2对象的锁(其他线程等待obj2对象锁释放)
             * 2、如果其他线程在线程1之前先拿到obj2对象锁的话,线程1需等待
             */
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName()+"--获得对象obj2的锁");
            }
        }
    }
}


class MyThread2 extends Thread {

    private final Object obj1;
    private final Object obj2;

    public MyThread2(Object obj1, Object obj2) {
        this.obj1 = obj1;
        this.obj2 = obj2;
    }

    @Override
    public void run() {

        /**
         * 1、线程2获得obj2对象的锁(其他线程等待obj2对象锁释放)
         * 2、如果其他线程在线程2之前先拿到obj2对象锁的话,线程2需等待
         */
        synchronized (obj2) {

            System.out.println(Thread.currentThread().getName() + "--已获得对象obj2的锁,准备获得对象obj1的锁");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            /**
             * 线程2获得obj1对象的锁(其他线程等待obj1对象锁释放)
             */
            synchronized (obj1) {
                System.out.println(Thread.currentThread().getName() + "--获得对象obj1的锁");
            }
        }
    }
}


执行效果:


补充:这两个哥们也是挺逗的,我等你,你等我,谁都不肯把自己手里处理完的对象锁释放了给对方用,索性,咱俩就一直干等着呗,等到海枯石烂..... 等等,这代码可是我写的啊,为什么会这样呢? 


 

(2) 代码2 -- 继承Thread类,通过Int标识控制获取对象锁的顺序

package com.appleyk.dbinit.MyLock;

/**
 * <p>手写死锁 -- 方式2</p>
 *
 * @author appleyk
 * @version V.1.0.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on 下午 5:18 2019-7-1
 */
public class DeadLock2 {

    public static void main(String[] args) {

        Thread thread1 = new MyThread( 1 );
        Thread thread2 = new MyThread( 0 );
        thread1.start();
        thread2.start();

    }
}

class MyThread extends Thread{

    private static final Object obj1 = new Object() ;
    private static final Object obj2 = new Object();
    private Integer flag = 1;

    MyThread( Integer flag){
        this.flag = flag;
    }

    @Override
    public void run() {

        if(1 == flag){
            synchronized (obj1){
                System.out.println(Thread.currentThread().getName()+"--已获得对象obj1的锁,准备获得对象obj2的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2){
                    // dosomething
                    System.out.println(Thread.currentThread().getName()+"-- 获得了对象obj2的锁");
                }
            }
        }else {
            synchronized (obj2){
                System.out.println(Thread.currentThread().getName()+"--已获得对象obj2的锁,准备获得对象obj1的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1){
                    // dosomething
                    System.out.println(Thread.currentThread().getName()+"-- 获得了对象obj1的锁");
                }
            }
        }
    }
}

效果(和第一种方式一样,这不废话吗):


(3) 代码3 -- 实现Runnable接口

package com.appleyk.dbinit.MyLock;

/**
 * <p>手写死锁 -- 方式3</p>
 *
 * @author appleyk
 * @version V.1.0.1
 * @blob https://blog.csdn.net/appleyk
 * @date created on 下午 4:30 2019-7-1
 */
public class DeadLock3 {

    static final Object obj1 = new Object();
    static final Object obj2 = new Object();

    public static void main(String[] args) {
        deadLock();
    }

    private static void deadLock(){

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj1){
                    System.out.println(Thread.currentThread().getName()+"--已获得对象obj1的锁,准备获得对象obj2的锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj2){
                        System.out.println(Thread.currentThread().getName()+"--获得对象obj2的锁");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(obj2){
                    System.out.println(Thread.currentThread().getName()+"--已获得对象obj2的锁,准备获得对象obj1的锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj1){
                        System.out.println(Thread.currentThread().getName()+"-获得对象obj1的锁");
                    }
                }
            }
        }).start();
    }
}

效果同上(还是贴图吧,省得有些人有强迫症,说我没测试过就贴代码):


七、如何知道你的程序出现了"死锁"

A、使用Java安装路径下的../bin/jconsole.exe


 

(1)连接主进程



 

(2)查看&检测


B、利用jstack命令

(1)JPS查看当前运行着的Java进程


(2)jstack检测指定pid是否发生死锁



        上图中,我们可以很明显的看出,Thread-1在等待获得对象<0x00...76b27dc78>的lock,但是对象<0x00...76b27dc78>的锁却被Thread-2所持有(被locked了),因为此情况满足产生死锁的条件,所以,我们最后可以看到检测的结果:Found 1 deadlock.


八、写在最后

        关于并发,必然要提到锁,关于锁,还是有很多要讲的,本篇只是浅显的介绍了同步锁和死锁,关于锁的设计思想和类型,还有很多很多要说道的地方,由于博主也是在不断的充电中,所以后续的内容博主会慢慢补充,比如什么是CAS啊,什么是AQS啊....等等等

      

猜你喜欢

转载自blog.csdn.net/Appleyk/article/details/94402475