多线程Synchronized锁的使用与线程之间的通讯

  • 一、什么是线程安全问题
  • 二、如何解决线程安全问题
  • 三、synchronized锁的基本用法
    • 1、修饰代码块(this锁)
    • 2、修饰实例方法(this锁)
    • 3、修饰静态方法
  • 四、死锁问题
    • (1)死锁
    • (2)死锁产生的必要条件
    • (3)诊断synchronized死锁
  • 五、线程如何实现同步
  • 六、多线程之间的通信
    • 1、等待/通知机制
    • 2、生产者和消费者模型

一、什么是线程安全问题

多线程同时对同一个全局变量做写操作,可能会受到其他线程的干扰,就会发生线程安全问题。

Java中的全局变量是存放在堆内存中的,而堆内容对于所有线程来说是共享的。

比如下面一个简单的代码案例:


public class ThreadCount implements Runnable{private int count = 10;@Overridepublic void run() {while (true) {if (count > 1) {try {// 模拟两个线程的阻塞状态Thread.sleep(30);} catch (Exception e) {throw new RuntimeException(e);}count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}}public static void main(String[] args) {ThreadCount threadCount = new ThreadCount();// 启动两个线程执行任务 new Thread(threadCount).start();new Thread(threadCount).start();}
}

代码比较简单,我们看下面控制台的打印:

Thread-1 == 9
Thread-0 == 9
Thread-0 == 7
Thread-1 == 7
Thread-1 == 6
Thread-0 == 6
Thread-1 == 5
Thread-0 == 5
Thread-1 == 4
Thread-0 == 4
Thread-0 == 2
Thread-1 == 2
Thread-1 == 1
Thread-0 == 1

可以看到两个线程之间产生了冲突,产生了线程安全问题。

二、如何解决线程安全问题

如何解决线程安全问题呢?或者说如何实现线程的同步呢?
核心思想:加锁

在同一个JVM中,多个线程需要竞争锁的资源。

那么哪些代码需要加锁呢?
可能会发生线程安全性问题的代码需要加锁。

还是上面的例子,我们在哪里加锁合适呢?
(1)锁加在run()方法上

@Overridepublic synchronized void run() {while (true) {if (count > 1) {try {// 模拟两个线程的阻塞状态Thread.sleep(30);} catch (Exception e) {throw new RuntimeException(e);}count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}}

这样可不可以呢?是可以的,但是我们来思考一个问题,如果synchronized加在了run()方法上,那么该执行过程是单线程还是多线程呢?
答案是单线程。我们可以看到以下控制台打印:

Thread-0 == 9
Thread-0 == 8
Thread-0 == 7
Thread-0 == 6
Thread-0 == 5
Thread-0 == 4
Thread-0 == 3
Thread-0 == 2
Thread-0 == 1

这是为什么呢?
原因是因为synchronized加在了run()方法上,获取到锁的线程不会释放锁,会一直持有锁,所以方法的执行就变成了单线程的;没有获取锁的线程,如果一直没有获取锁,中间需要经历一个锁的升级过程,最后会一直阻塞等待锁的释放。

(2)锁加在操作共享资源的代码上

@Overridepublic void run() {while (true) {if (count > 1) {try {Thread.sleep(30);} catch (Exception e) {throw new RuntimeException(e);}synchronized (this) {count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}}}

直接看控制台的输出结果:

Thread-1 == 9
Thread-0 == 8
Thread-0 == 7
Thread-1 == 6
Thread-1 == 5
Thread-0 == 4
Thread-0 == 3
Thread-1 == 2
Thread-1 == 1
Thread-0 == 0

这就解决了线程安全问题。
过程就是第一个线程和第二个线程同时去竞争this锁,假设第一个线程获取到锁,那么第二个线程就会阻塞等待,等第一个线程执行完操作资源后,释放锁之后才会获取到锁,执行操作。

三、synchronized锁的基本用法

1.修饰代码块,指定加锁对象,对指定对象加锁,进入同步代码块前要获取 给定对象 的锁。
2.修饰实例方法,作用于当前实例加锁,进入同步代码块前要获取 当前实例 的锁。
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码块前要获得 当前类对象 的锁。

1、修饰代码块(this锁)

public class ThreadCount implements Runnable {private int count = 10;@Overridepublic void run() {while (true) {sub();}}public void sub() {if (count > 1) {try {Thread.sleep(30);} catch (Exception e) {throw new RuntimeException(e);}// 修饰代码块,即this锁,进入同步代码块之前需要获取对象锁synchronized (this) {count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}}public static void main(String[] args) {// 同一个实例,执行不同线程,是线程安全的,不会出现问题//ThreadCount threadCount = new ThreadCount();//new Thread(threadCount).start();//new Thread(threadCount).start();// 不同实例,执行不同线程,会出现线程安全问题,这就是对象锁// 代码比较简单,可自行执行测试ThreadCount threadCount1 = new ThreadCount();ThreadCount threadCount2 = new ThreadCount();new Thread(threadCount1).start();new Thread(threadCount2).start();}
}

2、修饰实例方法(this锁)

 @Overridepublic void run() {while (true) {try {Thread.sleep(30);} catch (Exception e) {throw new RuntimeException(e);}sub();}}// 将synchronized加在实例方法上,则使用的还是this锁public synchronized void sub() {if (count > 1) {count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}

3、修饰静态方法

 private static int count = 10;...// 使用当前的  类名.class 锁public static synchronized void sub() {if (count > 1) {count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}

相当于

 public static  void sub() {synchronized(ThreadCount.class) {if (count > 1) {count--;System.out.println(Thread.currentThread().getName() + " == " + count);}}}

四、死锁问题

(1)死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

(2)死锁产生的必要条件

产生死锁的必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

(3)诊断synchronized死锁

以下这段代码会产生死锁问题,我们当作测试案例:

public class DeadlockThread implements Runnable {private int count = 1;private final String lock = "lock";@Overridepublic void run() {while (true) {count++;if (count % 2 == 0) {synchronized (lock) {a();}} else {synchronized (this) {b();}}}}public synchronized void a() {System.out.println(Thread.currentThread().getName() + ",a方法...");}public void b() {synchronized (lock) {System.out.println(Thread.currentThread().getName() + ",b方法...");}}public static void main(String[] args) {DeadlockThread deadlockThread = new DeadlockThread();Thread thread1 = new Thread(deadlockThread);Thread thread2 = new Thread(deadlockThread);thread1.start();thread2.start();}
}

诊断死锁我们可以使用jdk8自带的诊断工具jconsole.exe


如图,双击打开,选择对应的进程。

这里我们本地,不需要登录,直接选择不安全方式连接。

连接成功之后,点击线程,点击检测死锁,该工具就可以帮我们自动检测到产生死锁的线程,如图:

还能够显示出死锁线程的具体信息,锁的拥有者,以及对应的代码行数:

五、线程如何实现同步

线程如何实现同步?
或者说线程如何保证线程安全性问题?

  • 使用synchronized锁,JDK1.6开始,锁的升级过程
偏向锁 --> 轻量级锁 --> 重量级锁
  • 使用Lock锁(JUC),需要自己实现锁的升级过程,底层是基于AQS+CAS实现
  • 使用ThreadLocal,但是需要注意内存泄漏的问题
  • 原子类CAS非阻塞式

六、多线程之间的通信

1、等待/通知机制

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object 上,方法如下:
1.notify():通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁;
2.notifyAll():通知所有等待在该对象的线程;
3.wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会主动释放对象的锁。

2、生产者和消费者模型

下面我们通过一个案例来展示生产者消费者模型,模拟过程为两个线程,一个输入线程一个输出线程,输入线程输入内容,输出线程立马打印:

public class ThreadTest {// 共享变量class Res {public String userName;public char sex;}/***  输入线程*/class InputThread extends Thread {private Res res;public InputThread(Res res) {this.res = res;}@Overridepublic void run() {int count = 0;while (true) {if (count == 0) {res.userName = "zal";res.sex = '男';} else {res.userName = "zzal";res.sex = '女';}count = (count + 1) % 2;}}}/*** 输出线程*/class OutPutThread extends Thread {private Res res;public OutPutThread(Res res) {this.res = res;}@Overridepublic void run() {while (true) {System.out.println(res.userName + ", " + res.sex);}}}public static void main(String[] args) {new ThreadTest().print();}public void print() {// 全局对象Res res = new Res();// 输入线程InputThread inputThread = new InputThread(res);OutPutThread outPutThread = new OutPutThread(res);inputThread.start();outPutThread.start();}
}

然后我们看控制台输出打印,发现了问题。

这就意味着该代码出现了线程安全性问题,那么为了解决线程安全性问题,我们就需要对线程进行加锁,那么锁哪些代码块呢?

肯定是锁Res对象。

代码改进如下:

public class ThreadTest {class Res {public String userName;public char sex;}/***  输入线程*/class InputThread extends Thread {private Res res;public InputThread(Res res) {this.res = res;}@Overridepublic void run() {int count = 0;while (true) {synchronized (res) {if (count == 0) {res.userName = "zal";res.sex = '男';} else {res.userName = "zzal";res.sex = '女';}}count = (count + 1) % 2;}}}/*** 输出线程*/class OutPutThread extends Thread {private Res res;public OutPutThread(Res res) {this.res = res;}@Overridepublic void run() {while (true) {synchronized (res) {System.out.println(res.userName + ", " + res.sex);}}}}public static void main(String[] args) {new ThreadTest().print();}public void print() {// 全局对象Res res = new Res();// 输入线程InputThread inputThread = new InputThread(res);OutPutThread outPutThread = new OutPutThread(res);inputThread.start();outPutThread.start();}
}

我们对输入线程和输出线程的res对象都加了锁,并且锁住的是同一个对象,这下不会再出现线程安全问题了,运行截图如下:


可是又出现了新的问题,那就是输入和输出一片一片的打印,并不能实现我们输入线程输入,输出线程立马输出的功能。

出现问题的原因就是当输入线程获取锁的时候,那么输出线程就不能获取锁,就会进入阻塞状态,而当输出线程进行输出的时候,输入线程就不能输入了,所以就会出现这种现象。

最后,我们使用生产者和消费者模型进行改进代码,代码如下:

public class ThreadTest {class Res {public String userName;public char sex;/*** flag 标志*  当flag = false时,输入线程输入,输出线程等待*  当flag = true时,输出线程输出,输入线程等待*/public boolean flag = false;}/***  输入线程*/class InputThread extends Thread {private Res res;public InputThread(Res res) {this.res = res;}@Overridepublic void run() {int count = 0;while (true) {synchronized (res) {if (res.flag) {try {res.wait();} catch (InterruptedException e) {e.printStackTrace();}}if (count == 0) {res.userName = "zal";res.sex = '男';} else {res.userName = "zzal";res.sex = '女';}// 输出线程可以输出值res.flag = true;// 唤醒输出线程res.notify();}count = (count + 1) % 2;}}}/*** 输出线程*/class OutPutThread extends Thread {private Res res;public OutPutThread(Res res) {this.res = res;}@Overridepublic void run() {while (true) {synchronized (res) {// 如果res.flag = false,则输出线程主动释放锁// 同时会阻塞线程if (!res.flag) {try {res.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(res.userName + ", " + res.sex);res.flag = false;// 唤醒输出线程res.notify();}}}}public static void main(String[] args) {new ThreadTest().print();}public void print() {// 全局对象Res res = new Res();// 输入线程InputThread inputThread = new InputThread(res);OutPutThread outPutThread = new OutPutThread(res);inputThread.start();outPutThread.start();}
}

多线程Synchronized锁的使用与线程之间的通讯相关推荐

  1. 多线程-synchronized锁

    package 多线程.synchronized锁; /*. * * * * */ public class Sale implements Runnable {private int m = 10; ...

  2. RTX5 | 内存池04 - 共享内存用于线程之间的通讯(阻塞方式)- 使用信号量

    文章目录 一.前言 二.实验目的 三.代码 3.1.main.h 3.2.main.c 四.Debug 4.1.System Analyzer 4.2.Debug (printf) Viewer 一. ...

  3. RTX5 | 内存池03 - 共享内存用于线程之间的通讯(轮询方式)

    文章目录 一.前言 怎样防止内存溢出? 二.实验目的 三.代码 3.1.main.h 3.2.main.c 四.Debug 4.1.Debug (printf) Viewer 4.2.修改一下程序:线 ...

  4. Python | threading02 - 互斥锁解决多个线程之间随机调度,造成“线程不安全”的问题。

    文章目录 一.前言 二.线程不安全的现象 2.1.代码 2.2.运行 三.使用互斥锁解决线程不安全 3.1.代码 3.2.运行 四.忘记释放互斥锁,造成死锁 4.1.代码 4.2.运行 4.3.造成死 ...

  5. java 多线程 共享数据_JAVA多线程提高四:多个线程之间共享数据的方式

    多个线程访问共享对象和数据的方式 如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做. 如果每个线程执行的代码不同,这 ...

  6. iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)

    2017-07-08 remember17 Cocoa开发者社区 目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 ...

  7. 进阶12 多线程、等待唤醒机制、线程池

    多线程 我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计? 要解决上述问题,咱们得使用多进程或者多线程来解决. 并发与并行 并发:指 ...

  8. 多线程、并发/并行、自定义线程类、线程安全、守护线程、定时器、线程状态、线程池

    目录 进程和线程: 进程: 线程: 多线程的好处: 线程调度: 分时调度: 抢占式调度: 并发与并行: 线程的生命周期: 实现线程的两种基本方式(还有第三种): 创建Thread线程类: 创建Runn ...

  9. synchronized锁

    现在集群,分布式,微服务这么火,基本上也不会有单机服务了,所以synchronized基本上就废了,但不影响我们再回忆一下他的一些思想,很多东西,万变不离其宗. 概念: 能够保证同一时刻最多只有一个线 ...

最新文章

  1. 深度学习(6)构造简单的神经网络
  2. 微信开发系列之八 - 微信公众号的地图集成
  3. sort命令详解及Nginx统计运用
  4. C#调用带结构体指针的C Dll的方法
  5. SharePoint 解决管理员密码修改后的问题
  6. [转]Yii Framework: 从 model 中生成 select option
  7. 证件照处理软件(IDPhoto Processor)免费版 v3.2.10
  8. 系统性学习计算机(一)
  9. PMP工具与技术总结
  10. 嵌入式软件设计层级划分概念
  11. python正弦波叠加方波_电赛初探(一)——正弦波、方波、锯齿波转换
  12. 致Oracle开发者的学习资源清单
  13. 易到用车构架演进及上云探索
  14. python中的内置函数总结
  15. Android新手如何学习开发一款app?
  16. 自激多谐振荡电路实验总结,小白电路测试
  17. java tapestry_Java Web开发的轻便架构Tapestry5---简介
  18. vscode将后缀为.art文件识别为html文件
  19. SaaS是什么?企业采购SaaS有什么好处?
  20. NFT元宇宙游戏开发铸造源代码怎么写案例分享

热门文章

  1. Javascript中的every()与some()的区别和应用
  2. Hive(2):Apache Hive 安装部署
  3. UltraEdit-32 v14.10 简体中文版
  4. 专利产品被“山寨”热卖 广东商家怒将拼多多告上法庭
  5. 工信部叫停苹果 Callkit,微信不能直接接听视频了
  6. 9000+字,唠唠架构中的设计模式
  7. webpack打包时提示Invalid configuration object错误
  8. kubernetes kubectl apply -f和kubectl create -f有什么区别
  9. 情到深处人孤独 爱至穷时尽沧桑
  10. CAD计算器怎么调出来?CAD计算器应用技巧