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

news/2024/12/28 15:00:35/

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

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就将其赋值走了, 根本没来得及加一. 这种还未执行完就将其读入的操作, 也可称其为脏读. 

                                                                                 

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

                                                                                

 这样的操作就能保证在执行完一整个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观察到线程是属于阻塞状态.

                                                                                   

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

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

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

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

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

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

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

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

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

                                                                                      

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

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

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 下午两点左右


http://www.ppmy.cn/news/157702.html

相关文章

ARM Cortex A7 架构简介

Cortex-A7 MPCore 简介 MP表示是多核的意思&#xff0c;Cortex-A7 MPcore 处理器支持 1~4 核&#xff0c;A7主打低功耗&#xff0c;因此多用于运行普通应用&#xff0c;通常和Cortex-A15 组成 big.LITTLE 架构的&#xff0c;Cortex-A15 作为大核负责高性能运算。Cortex-A7 本身…

ARM Cortex-A77架构解读

Cortex-A76是2018年的亮点产品&#xff0c;无论是高通还是华为&#xff0c;都利用这个先进的架构设计出了极为出色的产品&#xff0c;使得移动计算设备的性能又大大向前推进了一步&#xff0c;并且依旧保持了极高的能耗比。在2019年&#xff0c;ARM并没有松懈下来&#xff0c;而…

ARM发布Cortex-A78参数细节

面向7nm工艺的Cortex-A77架构发布2年多之后&#xff0c;ARM公司今晚正式推出了新一代CPU架构——Cortex-A78&#xff0c;适用于5nm工艺&#xff0c;性能提升20%&#xff0c;功耗则降低了50%。Cortex-A78就是前两年曝光的Hercules&#xff08;大力神&#xff09;处理器&#xff…

“微商城”项目(4首页)

1.显示轮播图 首页和商品详情页都有图片轮播图展示&#xff0c;考虑到Vue组件代码的复用性&#xff0c;把轮播图相关代码单独放置在src\components\swiper.vue文件中。 在src\pages\Home.vue文件中&#xff0c;编写HTML结构代码&#xff0c;示例代码如下。 <template>…

StackLLaMA: A hands-on guide to train LLaMA with RLHF

Paper name StackLLaMA: A hands-on guide to train LLaMA with RLHF Paper Reading Note Project URL: https://huggingface.co/blog/stackllama Code URL: https://huggingface.co/docs/trl/index TL;DR Huggingface 公司开发的 RLHF 训练代码&#xff0c;已集成到 hugg…

Android - 线程 Thread

一、概念 1.1 主线程 一个线程总是由另一个线程启动&#xff0c;所以总有一条特殊的线程即主线程。 1.2 UI线程 APP启动时系统会为它创建一条执行线程即主线程&#xff0c;主线程用来处理所有与用户界面相关的操作&#xff08;触摸、布局、绘制、动画&#xff09;以及UI组件的…

音视频基础 及 海思sample_venc解读

1、sample的整体架构 (1)sample其实是很多个例程&#xff0c;所以有很多个main (2)每一个例程面向一个典型应用&#xff0c;common是通用性主体函数&#xff0c;我们只分析venc (3)基本的架构是&#xff1a;venc中的main调用venc中的功能函数&#xff0c;再调用common中的功…

linux中断

一 Linux中断原理 Linux中断&#xff08;Interrupt&#xff09;是指在计算机执行过程中&#xff0c;由于某些事件发生&#xff08;例如硬件请求、错误、异常等&#xff09;&#xff0c;CPU暂停当前正在执行的程序&#xff0c;转而执行相应的处理程序的过程。中断是计算机多任务…