编程(39)----------多线程中的锁

这篇具有很好参考价值的文章主要介绍了编程(39)----------多线程中的锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

假设一个这样的场景: 在多线程的代码中, 需要在不同的线程中对同一个变量进行操作. 那此时就会出现问题: 多线程是并发进行的, 也就是说代码运行的时候, 俩个线程会同时对一个变量进行操作, 这样就会涉及到多线程的安全问题:

class Counter{
    public int count;

    public  void add(){
            count++;
    }
}
public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在这个代码中, 两个线程会分别对count进行自增五千次, 按理说最后打印的结果是一万. 但实际上,多次运行后代码的结果,很难做到一万, 常见于八九千的结果. 

其原因在于, add的过程并非不可拆分的, 也就是不具有原子性. 在实际的运行中, add可以大致分为三步: 读取, 加一, 最后再赋值. 当然这并非专业的术语说法, 这里只简单的以此为描述. 

由于两个线程同时进行, 也就是都要执行这三步, 且是以抢占式进行执行. 那执行顺序就必然乱套了. 很可能会出现线程1刚将count原值读入, 线程2就将其赋值走了, 根本没来得及加一. 这种还未执行完就将其读入的操作, 也可称其为脏读. 

                                                                                 编程(39)----------多线程中的锁

 为避免这种乱套的多线程安全问题, 常用办法便是采用加锁(Synchronized), 其用于修饰方法和代码块. 但是特别注意, 加锁是锁的对象. 当某个对象加锁后, 只有当其再解锁后, 另一个线程才能重新获取锁, 否者会陷入阻塞等待的状态:

                                                                                编程(39)----------多线程中的锁

 这样的操作就能保证在执行完一整个add后再执行下一个add. 虽会降低运行速率, 但能保证代码的准确性. 代码上的修改只需将add进行加锁即可保证得到准确的结果:

//只需在此处加锁即可
    public synchronized void add(){
            count++;
    }
}

//或者代码块加锁
    public void add(){
        synchronized (this) {
            count++;
        }
    }

  若两个线程针对不同对象加锁或者一个加锁一个不加锁, 那么也不会存在阻塞等待的情况.

还有一种特殊情况: 多重锁. 即一个线程加了两把锁, 虽然说当一个线程对对象上锁后, 另一个线程是应该阻塞等待的, 但此时若上锁线程就是要访问的线程呢? 这时是否可以考虑开绿灯呢? 这就好比小偷偷不属于自己的东西, 这是不被允许的犯罪行为. 那如果他偷的是自己的东西呢? 这完全是可以的, 因为这压根就不算偷窃.

因此, 对于可以实现多重锁的关键字, 就被认为是可重入的, 反之是不可重入. 在java中的synchronized是属于可重入, 也就是说, 加上述代码合并运行, 仍可以得到正确的结果, 但并非所有的锁都支持该功能:

 //可重入
    public synchronized void add(){
        synchronized (this) {
            count++;
        }
    }

若不支持可重入, 则会陷入死锁状态, 卡在那里 一直阻塞等待.

当然, 死锁的状态并非只有上述的这一种. 第二种是两个线程两把锁, 即两个线程先分别加锁, 然后再尝试获得对方的锁:

public class demo2 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    System.out.println("获取锁2");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized ((lock1)){
                    System.out.println("获取锁1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

在这个代码中就能够看出, 当两个线程将锁1 锁2获取后, 要相互获取对方的锁, 但对方的锁未解锁, 因此在这种情况想两个线程都被阻塞, 不能继续运行. 在这种情况下代码会一直处于运行状态. 可以用jconsole观察到线程是属于阻塞状态.

                                                                                   编程(39)----------多线程中的锁

 第三种死锁即第二种死锁的一般情况, 多线程多把锁而非两把锁. 这里涉及到一个经典的吃面问题. 假设, 有一个圆桌, 共坐了五个人, 每两个人之间, 放了一根筷子. 也就是说共放了五根筷子.

假设吃面的人必须得先拿起他左边的筷子, 再拿起他右边的一根筷子. 那在这种情况下考虑极端情况, 当五个人同时都想吃面时, 会同时都拿起左边的筷子, 且右边没有筷子可拿. 这个时候就僵住了, 谁也吃不了面, 谁也不会放下筷子. 同理, 在多线程种, 每个线程就好比每个人, 每跟筷子就好比每个锁, 考虑极端情况, 会出现这种全部僵在一起的状态.

要解决这个问题, 就得先了解死锁的必要条件:\

1. 互斥使用. 线程一上锁, 线程二只能等着.

2. 不可抢占. 线程一获得锁之后, 只能自己释放锁, 而不能由线程二强行获取锁

3.保持稳定性. 若线程一已经获得锁A, 它再尝试获得锁B时, 锁A是不会因为线程一获得锁B而解锁锁A.

4.循环等待. 也就是刚才所演示的. 线程一获得锁A的同时, 线程二获得锁B. 然后线程一要获得锁B, 线程二要获得锁A, 僵持不下.

对于Synchronized而言, 其实必要条件只有第四点. 前三点是无法去改变的. 但对于其他锁来说不一定. 因此, 想要解决死锁, 就只能从, 循环等待入手.

解决方法是, 给每一把锁标号, 再按照标号的一定顺序进行加锁.

                                                                                      编程(39)----------多线程中的锁

以吃面来举例. 将每根筷子标号, 并规定拿筷子必须从小号开始拿. 对应多线程种按锁的标号顺序由小到大加锁. 这样的话, 一号筷子和二号筷子之间的人就拿一号, 二号筷子和三号筷子之间的人就拿二号, 以此类推.

当轮到一号筷子和五号筷子之间的人拿筷子时, 出现问题了. 由于规定按小号拿, 因此应该是拿一号筷子而非五号筷子. 但此时的一号筷子已经被占用. 因此他只能等待, 也就是多线程中的阻塞. 与此同时, 前一个人可以再拿到四号筷子的基础上拿到五号筷子, 也就是获取到锁, 从而执行多线程. 以这种方式, 就不会出现所有人都吃不到面, 避免所有线程都处于阻塞状态. 反应到代码中, 就只需将锁调换一下即可:

public class demo2 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        //标号: 锁1 为一号, 锁2 为二号. 由小到大加锁
        Thread t1 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2){
                    System.out.println("获取锁2");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized ((lock2)){
                    System.out.println("获取锁1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

除此以外, 解决这类问题还可以使用银行家算法. 但是在实际工作中, 使用并不广泛. 因为其过于复杂, 实用性不高.

-------------------------------------------最后编辑于2023.6.1 下午两点左右文章来源地址https://www.toymoban.com/news/detail-468400.html

到了这里,关于编程(39)----------多线程中的锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • StampedLock:高并发场景下一种比读写锁更快的锁

    摘要: 在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?有,那就是JDK1.8中新增的StampedLock! 本文分享自华为云社区《【高并发】高并发场景下一种比读写锁更快的锁》,作者: 冰 河。 ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不

    2024年02月07日
    浏览(46)
  • C++多线程场景中的变量提前释放导致栈内存异常

    在子线程中尝试使用当前函数的资源 ,是非常危险的,但是C++支持这么做。因此C++这么做可能会造成栈内存异常。 上述是一个正常的多线程代码。 但是如果将其中多线程传参设置为引用传递,可能就会造成栈内存异常了,如下所示: 编译成功,但是运行失败。 运行结果:

    2024年02月13日
    浏览(57)
  • Java多线程编程中的线程同步

    基本概念: ​ 线程同步是多线程编程中的一个重要概念,用于控制多个线程对共享资源的访问,以防止数据的不一致性和并发问题。 在多线程环境下,多个线程同时访问共享资源可能导致数据的竞争和不正确的结果。 是确保多个线程按照特定的顺序和规则访问共享资源,以

    2024年02月13日
    浏览(44)
  • Java多线程编程中的线程死锁

    ​ 在多线程编程中,线程死锁是一种常见的问题,它发生在两个或多个线程互相等待对方释放资源的情况下,导致程序无法继续执行 。本文将介绍线程死锁的概念、产生原因、示例以及如何预防和解决线程死锁问题。 线程死锁的概念 ​ 线程死锁是指两个或多个线程被阻塞

    2024年02月12日
    浏览(35)
  • InnoDB锁初探(一):锁分类和RR不同场景下的锁机制

    数据库锁是Mysql实现数据一致性的基础之一,是在事务的基础之上,基于Mysql Server层或存储引擎层实现的。 前置条件: 查看语句: 按照锁的粒度,可以分为表锁和行锁 共享锁 排他锁 意向锁是表级的 同样具有意向共享锁(IS)、意向排他锁(IX) TABLE LOCK table *** trx id *** lo

    2024年02月09日
    浏览(42)
  • 多线程编程之——终止(打断)正在执行中的线程

    ps:文字有点多,想看结果的,直接跳转:《二》 把线程交给spring管理好不好? 将线程交给Spring管理是一个常见的做法,特别是在基于Spring的应用程序中。通过将线程纳入Spring的管理范围,你可以利用Spring的依赖注入和生命周期管理功能,更好地控制线程的生命周期和资源。

    2024年02月05日
    浏览(41)
  • C# 中的多线程和异步编程

    最近在看代码的过程中,发现有很多地方涉及到多线程、异步编程,这是比较重要且常用的知识点,而本人在这方面还理解尚浅,因此开始全面学习C#中的多线程和异步编程,文中部分内容摘抄自一位前辈的网站:网址链接,为了更便于理解和学习,本人还在个别地方做了一

    2023年04月08日
    浏览(48)
  • 一文读懂flutter线程: 深入了解Flutter中的多线程编程

    在移动应用开发领域,Flutter已经成为了一个备受欢迎的框架,用于创建高性能、跨平台的应用程序。Flutter的一个关键特性是其能够轻松处理多线程编程,以改进应用程序的性能和响应性。本文将深入探讨Flutter中的多线程编程,包括为什么需要多线程、如何在Flutter中创建和管

    2024年01月20日
    浏览(80)
  • Android中的多线程编程与异步处理

    在移动应用开发中,用户体验是至关重要的。一个流畅、高效的应用能够吸引用户并提升用户满意度。然而,移动应用面临着处理复杂业务逻辑、响应用户输入、处理网络请求等多个任务的挑战。为了确保应用的性能和用户体验,多线程编程和异步处理成为了不可或缺的技术

    2024年02月11日
    浏览(53)
  • Java - JUC(java.util.concurrent)包详解,其下的锁、安全集合类、线程池相关、线程创建相关和线程辅助类、阻塞队列

    JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题 java.lang.Thread.State tools(工具类):又叫信号量三组工具类,包含有 CountDownLatch(闭锁) 是一个同步辅助类,在完成一组正在其他线程中

    2024年02月05日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包