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啊....等等等

 

 

 

 

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 您好!如果我理解正确的话,您的问题是关于JMU Java 07多线程同步访问的。以下是我的回答: 多线程编程是指在一个程序中同时执行多个线程的编程技术。多线程编程在提高程序性能、增强程序交互性等方面有很大的优势,但也会引入一些问题,比如线程安全问题。 当多个线程同时访问共享资源时,可能会出现线程不安全的问题,比如数据竞争、等。为了避免这些问题,我们需要使用同步机制来确保线程安全。 Java中提供了多种同步机制,比如synchronized关键字、ReentrantLock类、Semaphore类等。这些同步机制都可以用来保证多线程访问共享资源的安全性。 其中,synchronized是最常用的同步机制之一。在Java中,synchronized关键字可以用来修饰方法或代码块,以实现同步访问共享资源。synchronized可以保证在同一时间只有一个线程可以访问共享资源,从而避免了数据竞争等线程安全问题。 除了synchronized之外,Java还提供了一些其他的同步机制,比如ReentrantLock类和Semaphore类。ReentrantLock类提供了比synchronized更灵活的机制,可以实现可重入、公平等功能。Semaphore类可以控制同时访问共享资源的线程数量,从而避免资源被过度占用的问题。 总的来说,多线程编程是一项复杂而重要的技术,需要仔细研究和实践。在实际开发中,我们需要根据具体的需求选择合适的同步机制来确保多线程访问共享资源的安全性。 ### 回答2: 多线程编程是在当前计算机领域中最为常见的技术之一,它可以利用计算机中的多核处理器来使程序运行更加高效。但是,多线程编程中可能会出现的最大问题就是线程安全,因为线程之间可能会访问相同的资源,从而导致竞态条件。 在Java中,可以通过使用synchronized关键字来实现同步访问,从而避免线程安全问题。synchronized关键字可以用于两种不同的情形:同步方法和同步块。在同步方法中,方法是同步的,即每个线程在执行该方法时都需要获取该对象的,如果该已经被其他线程获取,则需要等待直到此被释放。在同步块中,需要手动指定,即每个线程在执行同步块时需要获取该指定,其他线程如果需要访问该代码块中的共享资源也需要获取该指定,这样就保证了该代码块中的所有共享资源的同步访问。 除了synchronized关键字外,Java还提供了其他一些同步机制来实现线程安全,如ReentrantLock类和CountDownLatch类等。ReentrantLock类可以实现更为灵活的同步访问控制,但需要手动释放;而CountDownLatch类则用于同步一个或多个线程,使这些线程在某个条件满足之前一直处于等待状态。 在进行多线程编程时,应该尽量避免对同步访问造成瓶颈,应该通过减小同步代码块的范围等方式来提高程序的效率。此外,多线程编程时还应该进行线程安全性的测试,以确保程序能够正确地运行。 ### 回答3: 在Java中,多线程是一种非常常见的编程方式。由于多线程的特点,对共享资源的访问会出现竞争的情况,这种竞争可能会导致数据不一致或程序异常等问题。因此,在多线程编程中,我们需要采取一些措施来保证共享资源的访问能够正确、有序地进行,这就是同步机制。 同步机制包括两种方式:和信号量。是最基本的同步机制。有两种类型:互斥(Mutex)和读写(ReadWriteLock)。互斥用于保护共享资源,保证同一时间只有一个线程可以访问它,其他线程需要等待释放后才能继续访问。读写用于读写分离场景,提高了多线程访问共享资源的并发性。读写支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。 信号量是一种更加高级的同步机制。信号量可以用来控制并发线程数和限制访问共享资源的最大数量。在Java中,Semaphore类提供了信号量的实现。Semaphore可以控制的线程数量可以是任意的,线程可以一起执行,也可以分批执行。 除了和信号量,Java还提供了一些其他同步机制,比如阻塞队列、Condition等。阻塞队列是一种特殊的队列,它支持线程在插入或者删除元素时阻塞等待。Condition是一种的增强,它可以让线程在某个特定条件下等待或者唤醒。 在多线程编程中,使用同步机制需要注意以下几点。首先,同步机制要尽可能的保证资源访问的公平性,避免因为某些线程执行时间过长导致其他线程等待时间过长。其次,同步机制要尽可能的避免的发生,尤其要注意线程之间的依赖关系。最后,同步机制的实现要尽可能地简单,避免过于复杂的代码实现带来的维护成本。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值