Java 多线程同步 synchronized

多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题

多线程的问题,又叫Concurrency 问题

步骤 1 : 演示同步问题

假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击

就是有多个线程在减少盖伦的hp

同时又有多个线程在恢复盖伦的hp

假设线程的数量是一样的,并且每次改变的值都是1,那么所有线程结束后,盖伦应该还是10000滴血。

但是。。。

注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数

  1. package charactor;
  2. public class Hero{
  3. public String name;
  4. public float hp;
  5. public int damage;
  6. //回血
  7. public void recover(){
  8. hp=hp+1;
  9. }
  10. //掉血
  11. public void hurt(){
  12. hp=hp-1;
  13. }
  14. public void attackHero(Hero h) {
  15. h.hp-=damage;
  16. System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
  17. if(h.isDead())
  18. System.out.println(h.name +"死了!");
  19. }
  20. public boolean isDead() {
  21. return 0>=hp?true:false;
  22. }
  23. }

.

  1. package multiplethread;
  2. import charactor.Hero;
  3. public class TestThread {
  4. public static void main(String[] args) {
  5. final Hero gareen = new Hero();
  6. gareen.name = "盖伦";
  7. gareen.hp = 10000;
  8. System.out.printf("盖伦的初始血量是 %.0f%n", gareen.hp);
  9. //多线程同步问题指的是多个线程同时修改一个数据的时候,导致的问题
  10. //假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击
  11. //用JAVA代码来表示,就是有多个线程在减少盖伦的hp
  12. //同时又有多个线程在恢复盖伦的hp
  13. //n个线程增加盖伦的hp
  14. int n = 10000;
  15. Thread[] addThreads = new Thread[n];
  16. Thread[] reduceThreads = new Thread[n];
  17. for (int i = 0; i < n; i++) {
  18. Thread t = new Thread(){
  19. public void run(){
  20. gareen.recover();
  21. try {
  22. Thread.sleep(100);
  23. } catch (InterruptedException e) {
  24. // TODO Auto-generated catch block
  25. e.printStackTrace();
  26. }
  27. }
  28. };
  29. t.start();
  30. addThreads[i] = t;
  31. }
  32. //n个线程减少盖伦的hp
  33. for (int i = 0; i < n; i++) {
  34. Thread t = new Thread(){
  35. public void run(){
  36. gareen.hurt();
  37. try {
  38. Thread.sleep(100);
  39. } catch (InterruptedException e) {
  40. // TODO Auto-generated catch block
  41. e.printStackTrace();
  42. }
  43. }
  44. };
  45. t.start();
  46. reduceThreads[i] = t;
  47. }
  48. //等待所有增加线程结束
  49. for (Thread t : addThreads) {
  50. try {
  51. t.join();
  52. } catch (InterruptedException e) {
  53. // TODO Auto-generated catch block
  54. e.printStackTrace();
  55. }
  56. }
  57. //等待所有减少线程结束
  58. for (Thread t : reduceThreads) {
  59. try {
  60. t.join();
  61. } catch (InterruptedException e) {
  62. // TODO Auto-generated catch block
  63. e.printStackTrace();
  64. }
  65. }
  66. //代码执行到这里,所有增加和减少线程都结束了
  67. //增加和减少线程的数量是一样的,每次都增加,减少1.
  68. //那么所有线程都结束后,盖伦的hp应该还是初始值
  69. //但是事实上观察到的是:
  70. System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n,n,gareen.hp);
  71. }
  72. }

步骤 2 : 分析同步问题产生的原因

  1. 假设增加线程先进入,得到的hp是10000
  2. 进行增加运算
  3. 正在做增加运算的时候,还没有来得及修改hp的值,减少线程来了
  4. 减少线程得到的hp的值也是10000
  5. 减少线程进行减少运算
  6. 增加线程运算结束,得到值10001,并把这个值赋予hp
  7. 减少线程也运算结束,得到值9999,并把这个值赋予hp

    hp,最后的值就是9999

    虽然经历了两个线程各自增减了一次,本来期望还是原值10000,但是却得到了一个9999

    这个时候的值9999是一个错误的值,在业务上又叫做脏数据



步骤 3 : 解决思路

总体解决思路是: 在增加线程访问hp期间,其他线程不可以访问hp

  1. 增加线程获取到hp的值,并进行运算
  2. 在运算期间,减少线程试图来获取hp的值,但是不被允许
  3. 增加线程运算结束,并成功修改hp的值为10001
  4. 减少线程,在增加线程做完后,才能访问hp的值,即10001
  5. 减少线程运算,并得到新的值10000



步骤 4 : synchronized 同步对象概念

解决上述问题之前,先理解

synchronized关键字的意义

如下代码:

  1. Object someObject =new Object();
  2. synchronized (someObject){
  3. //此处的代码只有占有了someObject后才可以执行
  4. }

synchronized表示当前线程,独占 对象 someObject

当前线程独占 了对象someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。

someObject 又叫同步对象,所有的对象,都可以作为同步对象

为了达到同步的效果,必须使用同一个同步对象

释放同步对象的方式: synchronized 块自然结束,或者有异常抛出

  1. package multiplethread;
  2. import java.text.SimpleDateFormat;
  3. import java.util.Date;
  4. public class TestThread {
  5. public static String now(){
  6. return new SimpleDateFormat("HH:mm:ss").format(new Date());
  7. }
  8. public static void main(String[] args) {
  9. final Object someObject = new Object();
  10. Thread t1 = new Thread(){
  11. public void run(){
  12. try {
  13. System.out.println( now()+" t1 线程已经运行");
  14. System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
  15. synchronized (someObject) {
  16. System.out.println( now()+this.getName()+ " 占有对象:someObject");
  17. Thread.sleep(5000);
  18. System.out.println( now()+this.getName()+ " 释放对象:someObject");
  19. }
  20. System.out.println(now()+" t1 线程结束");
  21. } catch (InterruptedException e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. }
  25. }
  26. };
  27. t1.setName(" t1");
  28. t1.start();
  29. Thread t2 = new Thread(){
  30. public void run(){
  31. try {
  32. System.out.println( now()+" t2 线程已经运行");
  33. System.out.println( now()+this.getName()+ " 试图占有对象:someObject");
  34. synchronized (someObject) {
  35. System.out.println( now()+this.getName()+ " 占有对象:someObject");
  36. Thread.sleep(5000);
  37. System.out.println( now()+this.getName()+ " 释放对象:someObject");
  38. }
  39. System.out.println(now()+" t2 线程结束");
  40. } catch (InterruptedException e) {
  41. // TODO Auto-generated catch block
  42. e.printStackTrace();
  43. }
  44. }
  45. };
  46. t2.setName(" t2");
  47. t2.start();
  48. }
  49. }

步骤 5 : 使用synchronized 解决同步问题

所有需要修改hp的地方,有要建立在占有someObject的基础上

而对象 someObject在同一时间,只能被一个线程占有。 间接地,导致同一时间,hp只能被一个线程修改

  1. package multiplethread;
  2. import java.awt.GradientPaint;
  3. import charactor.Hero;
  4. public class TestThread {
  5. public static void main(String[] args) {
  6. final Object someObject = new Object();
  7. final Hero gareen = new Hero();
  8. gareen.name = "盖伦";
  9. gareen.hp = 10000;
  10. int n = 10000;
  11. Thread[] addThreads = new Thread[n];
  12. Thread[] reduceThreads = new Thread[n];
  13. for (int i = 0; i < n; i++) {
  14. Thread t = new Thread(){
  15. public void run(){
  16. //任何线程要修改hp的值,必须先占用someObject
  17. synchronized (someObject) {
  18. gareen.recover();
  19. }
  20. try {
  21. Thread.sleep(100);
  22. } catch (InterruptedException e) {
  23. // TODO Auto-generated catch block
  24. e.printStackTrace();
  25. }
  26. }
  27. };
  28. t.start();
  29. addThreads[i] = t;
  30. }
  31. for (int i = 0; i < n; i++) {
  32. Thread t = new Thread(){
  33. public void run(){
  34. //任何线程要修改hp的值,必须先占用someObject
  35. synchronized (someObject) {
  36. gareen.hurt();
  37. }
  38. try {
  39. Thread.sleep(100);
  40. } catch (InterruptedException e) {
  41. // TODO Auto-generated catch block
  42. e.printStackTrace();
  43. }
  44. }
  45. };
  46. t.start();
  47. reduceThreads[i] = t;
  48. }
  49. for (Thread t : addThreads) {
  50. try {
  51. t.join();
  52. } catch (InterruptedException e) {
  53. // TODO Auto-generated catch block
  54. e.printStackTrace();
  55. }
  56. }
  57. for (Thread t : reduceThreads) {
  58. try {
  59. t.join();
  60. } catch (InterruptedException e) {
  61. // TODO Auto-generated catch block
  62. e.printStackTrace();
  63. }
  64. }
  65. System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
  66. }
  67. }

步骤 6 : 使用hero对象作为同步对象

既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个hero对象,索性就使用gareen来作为同步对象

进一步的,对于Hero的hurt方法,加上:

  1. synchronized (this) {
  2. }

表示当前对象为同步对象,即也是gareen为同步对象

  1. package multiplethread;
  2. import java.awt.GradientPaint;
  3. import charactor.Hero;
  4. public class TestThread {
  5. public static void main(String[] args) {
  6. final Hero gareen = new Hero();
  7. gareen.name = "盖伦";
  8. gareen.hp = 10000;
  9. int n = 10000;
  10. Thread[] addThreads = new Thread[n];
  11. Thread[] reduceThreads = new Thread[n];
  12. for (int i = 0; i < n; i++) {
  13. Thread t = new Thread(){
  14. public void run(){
  15. //使用gareen作为synchronized
  16. synchronized (gareen) {
  17. gareen.recover();
  18. }
  19. try {
  20. Thread.sleep(100);
  21. } catch (InterruptedException e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. }
  25. }
  26. };
  27. t.start();
  28. addThreads[i] = t;
  29. }
  30. for (int i = 0; i < n; i++) {
  31. Thread t = new Thread(){
  32. public void run(){
  33. //使用gareen作为synchronized
  34. //在方法hurt中有synchronized(this)
  35. gareen.hurt();
  36. try {
  37. Thread.sleep(100);
  38. } catch (InterruptedException e) {
  39. // TODO Auto-generated catch block
  40. e.printStackTrace();
  41. }
  42. }
  43. };
  44. t.start();
  45. reduceThreads[i] = t;
  46. }
  47. for (Thread t : addThreads) {
  48. try {
  49. t.join();
  50. } catch (InterruptedException e) {
  51. // TODO Auto-generated catch block
  52. e.printStackTrace();
  53. }
  54. }
  55. for (Thread t : reduceThreads) {
  56. try {
  57. t.join();
  58. } catch (InterruptedException e) {
  59. // TODO Auto-generated catch block
  60. e.printStackTrace();
  61. }
  62. }
  63. System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
  64. }
  65. }

.

  1. package charactor;
  2. public class Hero{
  3. public String name;
  4. public float hp;
  5. public int damage;
  6. //回血
  7. public void recover(){
  8. hp=hp+1;
  9. }
  10. //掉血
  11. public void hurt(){
  12. //使用this作为同步对象
  13. synchronized (this) {
  14. hp=hp-1;
  15. }
  16. }
  17. public void attackHero(Hero h) {
  18. h.hp-=damage;
  19. System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
  20. if(h.isDead())
  21. System.out.println(h.name +"死了!");
  22. }
  23. public boolean isDead() {
  24. return 0>=hp?true:false;
  25. }
  26. }

步骤 7 : 在方法前,加上修饰符synchronized

在recover前,直接加上synchronized ,其所对应的同步对象,就是this

和hurt方法达到的效果是一样

外部线程访问gareen的方法,就不需要额外使用synchronized 了

  1. package charactor;
  2. public class Hero{
  3. public String name;
  4. public float hp;
  5. public int damage;
  6. //回血
  7. //直接在方法前加上修饰符synchronized
  8. //其所对应的同步对象,就是this
  9. //和hurt方法达到的效果一样
  10. public synchronized void recover(){
  11. hp=hp+1;
  12. }
  13. //掉血
  14. public void hurt(){
  15. //使用this作为同步对象
  16. synchronized (this) {
  17. hp=hp-1;
  18. }
  19. }
  20. public void attackHero(Hero h) {
  21. h.hp-=damage;
  22. System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
  23. if(h.isDead())
  24. System.out.println(h.name +"死了!");
  25. }
  26. public boolean isDead() {
  27. return 0>=hp?true:false;
  28. }
  29. }

.

  1. package multiplethread;
  2. import java.awt.GradientPaint;
  3. import charactor.Hero;
  4. public class TestThread {
  5. public static void main(String[] args) {
  6. final Hero gareen = new Hero();
  7. gareen.name = "盖伦";
  8. gareen.hp = 10000;
  9. int n = 10000;
  10. Thread[] addThreads = new Thread[n];
  11. Thread[] reduceThreads = new Thread[n];
  12. for (int i = 0; i < n; i++) {
  13. Thread t = new Thread(){
  14. public void run(){
  15. //recover自带synchronized
  16. gareen.recover();
  17. try {
  18. Thread.sleep(100);
  19. } catch (InterruptedException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. }
  24. };
  25. t.start();
  26. addThreads[i] = t;
  27. }
  28. for (int i = 0; i < n; i++) {
  29. Thread t = new Thread(){
  30. public void run(){
  31. //hurt自带synchronized
  32. gareen.hurt();
  33. try {
  34. Thread.sleep(100);
  35. } catch (InterruptedException e) {
  36. // TODO Auto-generated catch block
  37. e.printStackTrace();
  38. }
  39. }
  40. };
  41. t.start();
  42. reduceThreads[i] = t;
  43. }
  44. for (Thread t : addThreads) {
  45. try {
  46. t.join();
  47. } catch (InterruptedException e) {
  48. // TODO Auto-generated catch block
  49. e.printStackTrace();
  50. }
  51. }
  52. for (Thread t : reduceThreads) {
  53. try {
  54. t.join();
  55. } catch (InterruptedException e) {
  56. // TODO Auto-generated catch block
  57. e.printStackTrace();
  58. }
  59. }
  60. System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp);
  61. }
  62. }

步骤 8 : 线程安全的类

如果一个类,其方法都是有synchronized修饰的,那么该类就叫做线程安全的类

同一时间,只有一个线程能够进入 这种类的一个实例 的去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据)

比如StringBuffer和StringBuilder的区别

StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类

而StringBuilder就不是线程安全的类

Java自学-多线程 同步synchronized的更多相关文章

  1. Java多线程-同步:synchronized 和线程通信:生产者消费者模式

    大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...

  2. Java多线程同步 synchronized 关键字的使用

    代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A, ...

  3. 【53】java的多线程同步剖析

    synchronized关键字介绍: synchronized锁定的是对象,这个很重要 例子: class Sync { public synchronized void test() { Syste ...

  4. Java之多线程同步基础

    java学习的道路上呢总有一些麻烦的东西需要花费一些时间去理解,比如个人认为不好搞的多线程. 线程是并列运行的 因为是并列运行,所以有时候会发生资源抢占,从而导致参数变化; 比如酱紫 package ...

  5. Java中多线程同步类 CountDownLatch

    在多线程开发中,常常遇到希望一组线程完成之后在执行之后的操作,java提供了一个多线程同步辅助类,可以完成此类需求: 类中常见的方法: 其中构造方法:CountDownLatch(int count) ...

  6. 多线程编程-- part 3 多线程同步->synchronized关键字

    多线程同时访问一个资源,可以会产生不可预料的结果,所以为这个资源加锁,访问资源的第一个线程为其加锁后,其他线程便不能在使用那个资源,直到锁被解除. 举个例子: 存款1000元,能取出800的时候我就取 ...

  7. 160407、java实现多线程同步

    多线程就不说了,很好理解,同步就要说一下了.同步,指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系.所以同步的关键是多个线程对象竞争同一个共享资源. 同步分为外同步和内同步.外同步就是在外 ...

  8. [Java][Android] 多线程同步-主线程等待全部子线程完毕案例

    有时候我们会遇到这种问题:做一个大的事情能够被分解为做一系列相似的小的事情,而小的事情无非就是參数上有可能不同样而已! 此时,假设不使用线程,我们势必会浪费许多的时间来完毕整个大的事情.而使用线程的话 ...

  9. Java自学-多线程 线程安全的类

    Java常见的线程安全相关的面试题 步骤 1 : HashMap和Hashtable的区别 HashMap和Hashtable都实现了Map接口,都是键值对保存数据的方式 区别1: HashMap可以 ...

随机推荐

  1. (树形DP入门题)Anniversary party(没有上司的舞会) HDU - 1520

    题意: 有个公司要举行一场晚会.为了让到会的每个人不受他的直接上司约束而能玩得开心,公司领导决定:如果邀请了某个人,那么一定不会再邀请他的直接的上司,但该人的上司的上司,上司的上司的上司等都可以邀请. ...

  2. 【Java并发基础】管程简介

    前言 在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的.除了Java之外,C/C++.C#等高级语言也都是支持管程的. 那么什么 ...

  3. C++ lambda 分析

    lambda 表达式分析 构造闭包:能够捕获作用域中变量的匿名函数的对象,Lambda 表达式是纯右值表达式,其类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type),所以在声 ...

  4. Android的学习之路一

    在Android的道路上越走越远==,本着一颗童心去学习,没想到最后会成为自己的职业.看到过知乎上写的,并不是兴趣使比尔盖茨以及乔布斯他们成就斐然,而是他们真正的牛逼使得即使买大饼也能成为世界首富.然 ...

  5. Android教程2020 - RecyclerView使用入门

    本文介绍RecyclerView的使用入门.这里给出一种比较常见的使用方式. Android教程2020 - 系列总览 本文链接 想必读者朋友对列表的表现形式已经不再陌生.手机上有联系人列表,文件列表 ...

  6. Selenium(一):元素定位

    一.Selenium 8种定位方式 baidu.html <form id="form" name="f" action="/s" c ...

  7. delphiXE开发蓝牙BLE4.0程序时遇到的notification问题

    IDE环境delphiXE8 蓝牙硬件ST17H26 service:0xfee7 chareter:0xfec9 const u16 my_OEMServiceUUID=0xfee7;const u ...

  8. mint ui的tabBar监听路由变化实现tabBar切换

    说明 最近学习vue,使用了mint ui的tabBar,感觉好难受,结合 tab-container使用更难受,因为它不是根据路由来切换页面的.mui与它基本相反,因此它能根据搜索栏的路由变化,相应 ...

  9. openstack启动云主机的流程

    看一下openstack启动云主机的流程图 通过上图分析可以知道: 1. 用户使用Dashboard或者CLI 把认证信息通过REST请求发送给keystone进行认证.2. Keystone校验用户 ...

  10. qt QDockWidget QStackWidget的简单使用

    stackdlg.h #ifndef STACKDLG_H #define STACKDLG_H #include <QtWidgets/QDialog> #include <QLi ...