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

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

 

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

 

(1) 继承Thread类

 

 

 

(2) 实现Runnable接口

 

 

Java -- 浅谈“同步锁”和“死锁”_第1张图片

 

 

Java -- 浅谈“同步锁”和“死锁”_第2张图片

 

 

思考下,上述两种实现方式我们应该优先使用哪个呢?(从继承和实现的层面上考虑)

 

 

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

 

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

 

 

二、Synchronize 是什么 ?

 

通俗易懂的解释:

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

摘自知乎

 

 

三、Synchronize的作用

 

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

 

   

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

       

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

     

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

 

Java -- 浅谈“同步锁”和“死锁”_第3张图片

 

       这个锁就很厉害了,我们完全可以做到自主控制锁;比如每次养成好的习惯在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;

/**
 * 

start()和run()的区别

* * @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)); } }

 

  直接来看输出结果:

 

Java -- 浅谈“同步锁”和“死锁”_第4张图片

 

 

 

五、什么是死锁

 

 

(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;

/**
 * 

手写死锁 -- 方式1

* * @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的锁"); } } } }

 

 

执行效果:

 

Java -- 浅谈“同步锁”和“死锁”_第5张图片

 

 

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

 

 

 

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

 

package com.appleyk.dbinit.MyLock;

/**
 * 

手写死锁 -- 方式2

* * @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的锁"); } } } } }

 

 

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

 

Java -- 浅谈“同步锁”和“死锁”_第6张图片

 

 

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

 

package com.appleyk.dbinit.MyLock;

/**
 * 

手写死锁 -- 方式3

* * @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(); } }

 

 

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

 

Java -- 浅谈“同步锁”和“死锁”_第7张图片

 

 

 

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

 

 

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

 

Java -- 浅谈“同步锁”和“死锁”_第8张图片

 

 

(1)连接主进程

 

Java -- 浅谈“同步锁”和“死锁”_第9张图片

 

 

 

Java -- 浅谈“同步锁”和“死锁”_第10张图片

 

 

(2)查看&检测

 

Java -- 浅谈“同步锁”和“死锁”_第11张图片

 

 

Java -- 浅谈“同步锁”和“死锁”_第12张图片

 

B、利用jstack命令

 

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

 

Java -- 浅谈“同步锁”和“死锁”_第13张图片

 

 

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

 

Java -- 浅谈“同步锁”和“死锁”_第14张图片

 

 

 

 

Java -- 浅谈“同步锁”和“死锁”_第15张图片

 

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

 

 

 

八、写在最后

 

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

 

 

 

 

 

你可能感兴趣的:(Java锁,Java并发编程)