Java synchronized与死锁深入探究

目录
  • 1.synchronized的特性
  • 2.synchronized使用示例:
  • 3.Java标准库中的线程安全类
  • 4.死锁是什么
  • 5.如果避免死锁

1.synchronized的特性

1). 互斥性

当某个线程执行到 synchronized 所修饰的对象时 , 该线程对象会加锁(lock) , 其他线程如果执行到同一个对象的 synchronized 就会产生阻塞等待.

  • 进入 synchronized 修饰的代码块 , 相当于加锁.
  • 退出 synchronized 修饰着代码块 , 相当于解锁.

synchronized 使用的锁存储在Java对象里 , 可以理解为每个对象在内存中存储时 , 都有一块内存表示当前的锁定状态.类似于公厕的"有人" , "无人".

如果是"无人"状态 , 此时就可以使用 , 使用时需设置为"有人"状态.

如果是"有人"状态 , 此时就需要排队等待.

如果理解阻塞等待?

针对每一把锁 , 操作系统都会维护一个等待队列 , 当一个线程获取到这个锁之后 , 其他线性再尝试获取这个锁 , 就会获取不到锁 , 陷入阻塞等待. 一直等到之前这个线程释放锁后 , 操作系统才会唤醒其他线程来再次竞争这个锁.

2)可重入

synchronized 对同一个线程来说是可重入的 , 不会出现把自己锁死的情况.

如何理解把自己锁死?

观察下面这段代码可以发现 , 当某个线程调用add方法时 , 就会对 this 对象先加锁 , 接着进入代码块又会对 this 对象再次尝试加锁. 站在 this 对象的角度 , 它认为自己已经被另外的线程占用了 , 那么第二次加锁是否需要阻塞等待呢? 如果运行上述情况 , 那么这个锁就是可重入的 , 否则就是不可重入的.不可重入锁会导致出现死锁 , 而Java中的 synchronized 是可重入锁 , 因此没有上述问题.

synchronized public void add(){
        synchronized (this) {
            count++;
        }
    }

在可重入锁内部 , 包含了"线程持有者"和"计数器"两个信息.

  • 如果每个线程加锁时 , 发现锁以及被占用了 , 但加锁的人是它自己 , 那么仍然可以获取到锁 , 让计数器自增.
  • 解锁的时候当计数器递减到0时 , 才真正释放锁.

2.synchronized使用示例:

1). 修饰普通方法

锁的是 Counter 对象.

class Counter{
    public int count;
    synchronized public void add(){
            count++;
        }
}

2). 修饰静态方法

锁的是 Counter 类

class Counter{
    public int count;
    synchronized public static void add(){
            count++;
        }
}

3).修饰代码块.明确指定锁哪个对象

锁当前对象:

class Counter{
    public int count;
    public void add(){
        synchronized (this) {
            count++;
        }
    }
}

锁类对象:

class Counter{
    public int count;
    public void add(){
        synchronized (Counter.class) {
            count++;
        }
    }
}

类锁和对象锁有什么区别?

顾名思义 , 对象锁用来锁住当前对象 , 类锁用来锁住当前类.如果一个类有多个实例对象 , 那么如果对其中一个对象加锁 , 别的线程只会在访问这个对象时阻塞等待 , 访问其他对象时没有影响.但如果是类锁 , 那么当一个线程对这个类加锁后 , 其他线程访问该类的所有对象都要阻塞等待.

3.Java标准库中的线程安全类

Java 标准库中有很多线程是不安全的 , 这些类可能涉及多线程修改共享数据 , 却又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • HashSet
  • TreeSet
  • StringBuilder

但还有一些是线程安全的 , 使用一些锁机制来控制.

  • Vector
  • HashTable
  • CurrentHashMap
  • StringBuffer
@Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

这些线程之所以不加锁是因为 , 加锁会损失部分性能.

4.死锁是什么

死锁是这样一种情况 , 多个线程同时被阻塞 , 其中一个或全部都在等待某个资源被释放.由于线程被无限期的阻塞 , 因此程序不可能正常终止.

死锁的三个典型情况

1). 一个线程一把锁 , 连续加两次 , 如果锁是不可重入锁 , 就会死锁. Java中的synchronized和ReentranLock 都是可重入锁 , 因此不会出现上述问题.

2). 两个线程两把锁 , t1 和 t2 线程各种先针对锁A和锁B加锁 , 再尝试获取对方的锁.

例如 , 张三和女神去吃饺子 , 需要蘸醋和酱油 , 张三拿到醋 , 女神拿到酱油 , 张三对女神说:"你先把酱油给我 , 我用完就把醋给你" , 女神对张三说:"你先把醋给我 , 我用完就把酱油给你". 这时两人争执不下 , 就构成了死锁 , 醋和酱油就是两把锁 , 张三和女生就是两个线程.

public static void main(String[] args) {
        Object jiangyou = new Object();
        Object cu  = new Object();
        Thread zhangsan = new Thread(()->{
            synchronized (jiangyou){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (cu ){
                    System.out.println("张三把酱油和醋都拿到了");
                }
            }
        });
        Thread nvsheng = new Thread(()->{
            synchronized (cu ){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (jiangyou){
                    System.out.println("女神把酱油和醋都拿到了");
                }
            }
        });
        zhangsan.start();
        nvsheng.start();
    }

执行代码后 , 发现没有打印任何日志 , 说明没有线程拿到两把锁.

通过jconsole查看线程的情况:

3)多个线程多把锁

例如常见经典案例--"哲学家就餐问题"

假设有五个哲学家围着桌子吃饭 , 每个人中间放一个筷子 , 哲学家有两种状态 , 1.思考人生(相当于线程的阻塞状态) , 2.拿起筷子吃面条(相当于线程获取到锁执行计算) , 由于操作系统的随机调度 , 这五个哲学家随时都可能想吃面条 , 也随时都可能思考人生 , 但是想要吃面条就得同时拿起左右两个筷子.

假设同一时刻 , 所有哲学家同时拿起左手的筷子 , 所有的哲学家都拿不起右手的筷子 , 就会产生死锁.

死锁是一个严重的"BUG" , 导致一个程序的线程"卡死"无法正常工作.

5.如果避免死锁

死锁的四个必要条件:

1.互斥使用: 当资源被一个线程占有时 , 别的线程不能使用

2.不可抢占: 资源请求者不能从资源获取者手中夺取资源 , 只能等资源占有者主动释放.

3.请求和保持: 当资源请求者请求获取别的资源时 , 保存对原有资源的占有.

4.循环等待: 即存在一个等待队列 , P1占有P2的资源 , P2占有P3的资源 , P3占有P1的资 源, 这样就形成一个等待回路.

当上述四个条件都成立就会形成死锁 , 当然破坏其中一个条件也可以打破死锁 , 对于synchronized 来说 , 前三个条件是锁的基本特性 , 因此想要打破死锁只能从"循环等待"入手.

如何破除死锁?

如果我们给锁编号 , 然后指定一个固定的顺序来加锁(必然从小到大) , 任意线程加多把锁的时候都遵循上述顺序, 此时循环等待自然破除.

因此解决哲学家就餐问题就可以给每个筷子编号 , 每个人都遵守"先拿小的再拿大的顺序".此时1号哲学家和2号哲学家为了竞争筷子其中一个人就会阻塞等待 , 这时5号哲学家就有了可乘之机 , 5号哲学家拿起4号和5号筷子吃完面条 , 四号哲学家重复上述操作也吃完面条 , 这样就完美的打破了循环等待的问题.

同样 , 最初的张三和女神吃饺子问题也是同样的解决方式 , 规定两人都按"先拿醋再拿饺子"的顺序执行 , 就可以完美解决死锁问题.

public static void main(String[] args) {
        Object jiangyou = new Object();
        Object cu  = new Object();
        Thread zhangsan = new Thread(()->{
            synchronized (cu){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (jiangyou ){
                    System.out.println("张三把酱油和醋都拿到了");
                }
            }
        });
        Thread nvsheng = new Thread(()->{
            synchronized (cu ){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (jiangyou){
                    System.out.println("女神把酱油和醋都拿到了");
                }
            }
        });
        zhangsan.start();
        nvsheng.start();
    }

到此这篇关于Java synchronized与死锁深入探究的文章就介绍到这了,更多相关Java synchronized 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解Java Synchronized的实现原理

    目录 Synchronized Synchronized的使用方式 Synchronized的底层实现 1.Java对象头 2.Monitor 3.线程状态流转在Monitor上体现 Synchronized 的锁升级 谈到多线程就不得不谈到Synchronized,重要性不言而喻,今天主要谈谈Synchronized的实现原理. Synchronized synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“.

  • Java synchronized重量级锁实现过程浅析

    目录 一.什么是重量级锁 二.重量级锁的演示 三.重量级锁的原理 四.锁的优缺点对比 一.什么是重量级锁 当有大量的线程都在竞争同一把锁的时候,这个时候加的锁,就是重量级锁. 这个重量级锁其实指的就是JVM内部的ObjectMonitor监视器对象: ObjectMonitor() { _header = NULL; //锁对象的原始对象头 _count = 0; //抢占当前锁的线程数量 _waiters = 0, //调用wait方法后等待的线程数量 _recursions = 0; //记

  • Java Synchronized锁的使用详解

    目录 Synchronized的用法 同步示例方法 同步静态方法 同步代码块 Synchronized的用法 在多线程并发问题中,常用Synchronized锁解决问题.Synchronized锁通常用于同步示例方法,同步静态方法,同步代码块等. 同步示例方法 我们可能自己使用过在方法前加Synchronized锁修饰,在多线程并发同时调用同一个实例化对象时,如果这个方法加上了Synchronized锁,那么也是线程安全的.举个栗子: package Thread; import java.ut

  • Java HashTable与Collections.synchronizedMap源码深入解析

    目录 一.类继承关系图 二.HashTable介绍 三.HashTable和HashMap的对比 1.线程安全 2.插入null 3.容量 4.Hash映射 5.扩容机制 6.结构区别 四.Collections.synchronizedMap解析 1.Collections.synchronizedMap是怎么实现线程安全的 2.SynchronizedMap源码 一.类继承关系图 二.HashTable介绍 HashTable的操作几乎和HashMap一致,主要的区别在于HashTable为

  • AQS加锁机制Synchronized相似点详解

    目录 正文 1. Synchronized加锁流程 2. AQS加锁原理 3. 总结 正文 在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁.Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS. AQS全称 AbstractQueuedSynchronizer ,即抽象队列同步器,是一种用来构建锁和同步器的框架. 我们常见的并发锁Reentr

  • Java synchronized偏向锁的概念与使用

    目录 一.什么是偏向锁 二.偏向锁原理 三.偏向锁演示 四.偏向锁的处理流程 五.偏向锁的撤销 六.偏向锁的好处 一.什么是偏向锁 HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁. 偏向锁的“偏”,它的意思是锁会偏向于第一个获得它的线程,会在对象头(Mark Word中)存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁.锁标志位以及ThreadID即可. 如下图是偏向锁对象头

  • Java synchronized与CAS使用方式详解

    目录 引言 synchronized synchronized的三种使用方式 synchronized的底层原理 JDK1.6对synchronized的优化 synchronized的等待唤醒机制 CAS 引言 上一篇文章中我们说过,volatile通过lock指令保证了可见性.有序性以及“部分”原子性.但在大部分并发问题中,都需要保证操作的原子性,volatile并不具有该功能,这时就需要通过其他手段来达到线程安全的目的,在Java编程中,我们可以通过锁.synchronized关键字,以及

  • Java synchronized同步关键字工作原理

    目录 一.简介 二.synchronized的特性 三.synchonized的使用及通过反汇编分析其原理 修饰代码块 monitorenter指令 monitorexit指令 修饰普通方法 修饰静态方法 四.synchronized锁对象存在哪里 五.synchronized与lock的区别 六.总结 一.简介 synchronized是一个同步关键字,在某些多线程场景下,如果不进行同步会导致共享数据不安全,synchronized关键字就可以用于代码同步. synchronized主要有3种

  • Synchronized 和 ReentrantLock 的实现原理及区别

    目录 前言 考点分析 知识扩展 ReentrantLock 源码分析 JDK 1.6 锁优化 自适应自旋锁 锁升级 总结 前言 在 JDK 1.5 之前共享对象的协调机制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的机制 ReentrantLock,该机制的诞生并不是为了替代 synchronized,而是在 synchronized 不适用的情况下,提供一种可以选择的高级功能. 典型回答: synchronized 属于独占式悲观锁,是通过 JVM 隐式

  • 95%的Java程序员人都用不好Synchronized详解

    目录 Synchronized锁优化 偏向锁 轻量级锁 获取锁 释放锁 自旋锁 适应自旋锁 锁消除 逃逸分析 重量级锁 三种锁的区别 锁升级 锁粗化 wait和notify的原理 Synchronized锁优化 文章内容整理自 博学谷狂野架构师 jdk1.6对锁的实现引入了大量的优化,如自旋锁.适应性自旋锁.锁消除.锁粗化.偏向锁.轻量级锁等技术来减少锁操作的开销. 锁主要存在四中状态,依次是:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁,他们会随着竞争的激烈而逐渐升级.注意锁可以升

  • java锁synchronized面试常问总结

    目录 synchronized都问啥? synchronized是什么? synchronized锁什么? synchronized怎么用? 结语 synchronized都问啥? 如果Java面试有什么是必问的,synchronized必定占据一席之地.初出茅庐时synchronized的用法,成长后synchronized的原理,可谓是Java工程师的“一生之敌”. 按照惯例,先来看synchronized的常见问题(在线Excel同步更新中): 根据统计数据可以总结出synchronize

  • Java必会的Synchronized底层原理剖析

    目录 1. synchronized作用 2. synchronized用法 3. synchronized加锁原理 synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用. 但不可否认的是synchronized依然是并发首选工具,连volatile.CAS.ReentrantLock都无法动摇synchronized的地位.synchronized是工作面试中的必备技能,今天就

  • 一个例子带你看懂Java中synchronized关键字到底怎么用

    目录 前言 使用同步方法 使用同步语句或块 什么是同步? 为啥需要同步? 总结 前言 在平时开发中,synchronized关键字经常遇到,你知道synchronized怎么用吗?本文给大家介绍一下. 我们有两种方法使用同步: 使用同步方法 使用同步语句或块 使用同步方法 要使方法同步,只需将synchronized关键字添加到其声明中: public class SynchronizedDemo { private int i = 0; public synchronized void add

  • Java同步锁Synchronized底层源码和原理剖析(推荐)

    目录 1 synchronized场景回顾 2 反汇编寻找锁实现原理 3 synchronized虚拟机源码 3.1 HotSpot源码Monitor生成 3.2 HotSpot源码之Monitor竞争 3.3 HotSpot源码之Monitor等待 3.4 HotSpot源码之Monitor释放 1 synchronized场景回顾 目标:synchronized回顾(锁分类–>多线程)概念synchronized:是Java中的关键字,是一种同步锁.Java中锁分为以下几种:乐观锁.悲观锁(

  • Java @Transactional与synchronized使用的问题

    目录 引言 发现问题 问题原因 解决问题 大致思路 @Transactional事务不生效问题 总结 引言 @Transactional是spring通过aop让我们轻松实现事务控制的一个注解:而synchronized是实现同步的java关键字:但是它们两个不能一起使用,一起使用会出现 synchronized失效的问题,这里简单记录一下这个问题: 发现问题 我在impl中实现一个功能逻辑时,为了保证幂等性,在方法中使用synchronized保证同一个用户短时间内多次请求只能串行执行,因为数

  • java同步锁的正确使用方法(必看篇)

    同步锁分类 对象锁(this) 类锁(类的字节码文件对象即类名.class) 字符串锁(比较特别) 应用场景 在多线程下对共享资源的安全操作. 需求:启动5个线程对共享资源total进行安全操作. 同步锁在多线程单例模式下的使用 以上三类同步锁都可以. package cn.myThread; public class MyThread implements Runnable { private static int total = 10; @Override public void run()

随机推荐