how2j.cn


工具版本兼容问题
多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题

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


本视频是解读性视频,所以希望您已经看过了本知识点的内容,并且编写了相应的代码之后,带着疑问来观看,这样收获才多。 不建议一开始就观看视频



36分56秒
本视频采用html5方式播放,如无法正常播放,请将浏览器升级至最新版本,推荐火狐,chrome,360浏览器。 如果装有迅雷,播放视频呈现直接下载状态,请调整 迅雷系统设置-基本设置-启动-监视全部浏览器 (去掉这个选项)。 chrome 的 视频下载插件会影响播放,如 IDM 等,请关闭或者切换其他浏览器



步骤 1 : 演示同步问题   
步骤 2 : 分析同步问题产生的原因   
步骤 3 : 解决思路   
步骤 4 : synchronized 同步对象概念   
步骤 5 : 使用synchronized 解决同步问题   
步骤 6 : 使用hero对象作为同步对象   
步骤 7 : 在方法前,加上修饰符synchronized   
步骤 8 : 线程安全的类   
步骤 9 : 练习-在类方法前面加修饰符synchronized   
步骤 10 : 答案-在类方法前面加修饰符synchronized   
步骤 11 : 练习-线程安全的MyStack   
步骤 12 : 答案-线程安全的MyStack   

步骤 1 :

演示同步问题

edit
假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击
就是有多个线程在减少盖伦的hp
同时又有多个线程在恢复盖伦的hp
假设线程的数量是一样的,并且每次改变的值都是1,那么所有线程结束后,盖伦应该还是10000滴血。
但是。。。

注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数
演示同步问题
package charactor; public class Hero{ public String name; public float hp; public int damage; //回血 public void recover(){ hp=hp+1; } //掉血 public void hurt(){ hp=hp-1; } public void attackHero(Hero h) { h.hp-=damage; System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp); if(h.isDead()) System.out.println(h.name +"死了!"); } public boolean isDead() { return 0>=hp?true:false; } }
package multiplethread; import charactor.Hero; public class TestThread { public static void main(String[] args) { final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; System.out.printf("盖伦的初始血量是 %.0f%n", gareen.hp); //多线程同步问题指的是多个线程同时修改一个数据的时候,导致的问题 //假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击 //用JAVA代码来表示,就是有多个线程在减少盖伦的hp //同时又有多个线程在恢复盖伦的hp //n个线程增加盖伦的hp int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ gareen.recover(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } //n个线程减少盖伦的hp for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ gareen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } //等待所有增加线程结束 for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //等待所有减少线程结束 for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //代码执行到这里,所有增加和减少线程都结束了 //增加和减少线程的数量是一样的,每次都增加,减少1. //那么所有线程都结束后,盖伦的hp应该还是初始值 //但是事实上观察到的是: System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n,n,gareen.hp); } }
步骤 2 :

分析同步问题产生的原因

edit
1. 假设增加线程先进入,得到的hp是10000
2. 进行增加运算
3. 正在做增加运算的时候,还没有来得及修改hp的值减少线程来了
4. 减少线程得到的hp的值也是10000
5. 减少线程进行减少运算
6. 增加线程运算结束,得到值10001,并把这个值赋予hp
7. 减少线程也运算结束,得到值9999,并把这个值赋予hp
hp,最后的值就是9999
虽然经历了两个线程各自增减了一次,本来期望还是原值10000,但是却得到了一个9999
这个时候的值9999是一个错误的值,在业务上又叫做脏数据
分析同步问题产生的原因
总体解决思路是: 在增加线程访问hp期间,其他线程不可以访问hp
1. 增加线程获取到hp的值,并进行运算
2. 在运算期间,减少线程试图来获取hp的值,但是不被允许
3. 增加线程运算结束,并成功修改hp的值为10001
4. 减少线程,在增加线程做完后,才能访问hp的值,即10001
5. 减少线程运算,并得到新的值10000
解决思路
步骤 4 :

synchronized 同步对象概念

edit
解决上述问题之前,先理解
synchronized关键字的意义
如下代码:

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


synchronized表示当前线程,独占 对象 someObject
当前线程独占 了对象someObject,如果有其他线程试图占有对象someObject,就会等待,直到当前线程释放对someObject的占用。
someObject 又叫同步对象,所有的对象,都可以作为同步对象
为了达到同步的效果,必须使用同一个同步对象

释放同步对象的方式: synchronized 块自然结束,或者有异常抛出
synchronized 同步对象概念
package multiplethread; import java.text.SimpleDateFormat; import java.util.Date; public class TestThread { public static String now(){ return new SimpleDateFormat("HH:mm:ss").format(new Date()); } public static void main(String[] args) { final Object someObject = new Object(); Thread t1 = new Thread(){ public void run(){ try { System.out.println( now()+" t1 线程已经运行"); System.out.println( now()+this.getName()+ " 试图占有对象:someObject"); synchronized (someObject) { System.out.println( now()+this.getName()+ " 占有对象:someObject"); Thread.sleep(5000); System.out.println( now()+this.getName()+ " 释放对象:someObject"); } System.out.println(now()+" t1 线程结束"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t1.setName(" t1"); t1.start(); Thread t2 = new Thread(){ public void run(){ try { System.out.println( now()+" t2 线程已经运行"); System.out.println( now()+this.getName()+ " 试图占有对象:someObject"); synchronized (someObject) { System.out.println( now()+this.getName()+ " 占有对象:someObject"); Thread.sleep(5000); System.out.println( now()+this.getName()+ " 释放对象:someObject"); } System.out.println(now()+" t2 线程结束"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t2.setName(" t2"); t2.start(); } }
步骤 5 :

使用synchronized 解决同步问题

edit
所有需要修改hp的地方,有要建立在占有someObject的基础上
而对象 someObject在同一时间,只能被一个线程占有。 间接地,导致同一时间,hp只能被一个线程修改。
使用synchronized 解决同步问题
package multiplethread; import java.awt.GradientPaint; import charactor.Hero; public class TestThread { public static void main(String[] args) { final Object someObject = new Object(); final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //任何线程要修改hp的值,必须先占用someObject synchronized (someObject) { gareen.recover(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //任何线程要修改hp的值,必须先占用someObject synchronized (someObject) { gareen.hurt(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp); } }
步骤 6 :

使用hero对象作为同步对象

edit
既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个hero对象,索性就使用gareen来作为同步对象
进一步的,对于Hero的hurt方法,加上:
synchronized (this) {
}
表示当前对象为同步对象,即也是gareen为同步对象
package multiplethread; import java.awt.GradientPaint; import charactor.Hero; public class TestThread { public static void main(String[] args) { final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //使用gareen作为synchronized synchronized (gareen) { gareen.recover(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //使用gareen作为synchronized //在方法hurt中有synchronized(this) gareen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp); } }
package charactor; public class Hero{ public String name; public float hp; public int damage; //回血 public void recover(){ hp=hp+1; } //掉血 public void hurt(){ //使用this作为同步对象 synchronized (this) { hp=hp-1; } } public void attackHero(Hero h) { h.hp-=damage; System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp); if(h.isDead()) System.out.println(h.name +"死了!"); } public boolean isDead() { return 0>=hp?true:false; } }
步骤 7 :

在方法前,加上修饰符synchronized

edit
在recover前,直接加上synchronized ,其所对应的同步对象,就是this
和hurt方法达到的效果是一样
外部线程访问gareen的方法,就不需要额外使用synchronized 了
package charactor; public class Hero{ public String name; public float hp; public int damage; //回血 //直接在方法前加上修饰符synchronized //其所对应的同步对象,就是this //和hurt方法达到的效果一样 public synchronized void recover(){ hp=hp+1; } //掉血 public void hurt(){ //使用this作为同步对象 synchronized (this) { hp=hp-1; } } public void attackHero(Hero h) { h.hp-=damage; System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp); if(h.isDead()) System.out.println(h.name +"死了!"); } public boolean isDead() { return 0>=hp?true:false; } }
package multiplethread; import java.awt.GradientPaint; import charactor.Hero; public class TestThread { public static void main(String[] args) { final Hero gareen = new Hero(); gareen.name = "盖伦"; gareen.hp = 10000; int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //recover自带synchronized gareen.recover(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread(){ public void run(){ //hurt自带synchronized gareen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n,n,gareen.hp); } }
步骤 8 :

线程安全的类

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

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

比如StringBuffer和StringBuilder的区别
StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类
而StringBuilder就不是线程安全的类
线程安全的类
步骤 9 :

练习-在类方法前面加修饰符synchronized

edit  姿势不对,事倍功半! 点击查看做练习的正确姿势
在对象方法前,加上修饰符synchronized ,同步对象是当前实例。
那么如果在类方法前,加上修饰符 synchronized,同步对象是什么呢?

提示:要完成本练习,需要反射reflection的知识,如果未学习反射,可以暂时不做
步骤 10 :

答案-在类方法前面加修饰符synchronized

edit
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
查看本答案会花费3个积分,您目前总共有点积分。查看相同答案不会花费额外积分。 积分增加办法 或者一次性购买JAVA 中级总计0个答案 (总共需要0积分)
查看本答案会花费3个积分,您目前总共有点积分。查看相同答案不会花费额外积分。 积分增加办法 或者一次性购买JAVA 中级总计0个答案 (总共需要0积分)
账号未激活 账号未激活,功能受限。 请点击激活
因为需要需要反射reflection的知识,所以答案见反射的答案-在静态方法上加synchronized,同步对象是什么?
步骤 11 :

练习-线程安全的MyStack

edit  姿势不对,事倍功半! 点击查看做练习的正确姿势
答案-使用LinkedList实现Stack栈 中的MyStack类,改造为线程安全的类。
步骤 12 :

答案-线程安全的MyStack

edit
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
查看本答案会花费3个积分,您目前总共有点积分。查看相同答案不会花费额外积分。 积分增加办法 或者一次性购买JAVA 中级总计0个答案 (总共需要0积分)
查看本答案会花费3个积分,您目前总共有点积分。查看相同答案不会花费额外积分。 积分增加办法 或者一次性购买JAVA 中级总计0个答案 (总共需要0积分)
账号未激活 账号未激活,功能受限。 请点击激活
package collection; import java.util.LinkedList; import charactor.Hero; public class MyStack implements Stack{ LinkedList<Hero> heros = new LinkedList<Hero>(); //插入的时候,加上synchronized,同步对象是当前实例 public synchronized void push(Hero h) { heros.addLast(h); } //取出的时候,加上synchronized,同步对象是当前实例 public synchronized Hero pull() { return heros.removeLast(); } //查看没必要加上synchronized,因为不修改数据 public Hero peek() { return heros.getLast(); } }


HOW2J公众号,关注后实时获知最新的教程和优惠活动,谢谢。


问答区域    
2021-03-02 final对象的值为什么之后可以改变?
1991320

请问站长, 步骤6 中, gareen 是final 的啊 final Hero gareen = new Hero(); 那为什么之后 gareen 的hp 还可以通过hurt()或者 recover()改变?




1 个答案

lelnice
答案时间:2021-03-05
对象前面加final,final的是对象引用地址,也就是你不能再更改该对象的引用地址了。 如果在Hero类里面的hp前面加上final,那才是不能再更改hp了。



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2020-10-10 为什么我的步骤1没有引发同步问题
兵长




我这里先是执行完全部加血线程,然后执行全部减血。(正常情况下应该是两个for循环中的多线程是互相抢占资源应该是无序的吧?)代码没有改变,直接copy的
package thread;
public class SynTest {
    static class Hero{
        public String name;
        public float hp;
        public int damage;
        //回血
        public void recover(){
            hp=hp+1;
        }
        //掉血
        public void hurt(){
            hp=hp-1;
        }
        public void attackHero(Hero h) {
            h.hp-=damage;
            System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n",name,h.name,h.name,h.hp);
            if(h.isDead())
                System.out.println(h.name +"死了!");
        }

        public boolean isDead() {
            return 0>=hp?true:false;
        }

    }
    public static void main(String[] args) {
        final Hero gareen = new Hero();
        gareen.name = "盖伦";
        gareen.hp = 10000;
        System.out.printf("盖伦的初始血量是 %.0f%n", gareen.hp);

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

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

        //用JAVA代码来表示,就是有多个线程在减少盖伦的hp
        //同时又有多个线程在恢复盖伦的hp
        //n个线程增加盖伦的hp

        int n = 10000;
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.recover();
                    System.out.println("加血");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
        }
        //n个线程减少盖伦的hp
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.hurt();
                    //System.out.println("减血");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
        //等待所有增加线程结束
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //等待所有减少线程结束
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //代码执行到这里,所有增加和减少线程都结束了
        //增加和减少线程的数量是一样的,每次都增加,减少1.
        //那么所有线程都结束后,盖伦的hp应该还是初始值

        //但是事实上观察到的是:

        System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n,n,gareen.hp);
    }
}
盖伦的初始血量是 10000
10000个增加线程和10000个减少线程结束后
盖伦的血量变成了 10000


4 个答案

nannananananan
答案时间:2021-10-09
我把join去掉就出现了问题

每天都要学Java啊
答案时间:2020-11-11
如果看不到效果,增加运行次数,比如 n = 100000 我 n 为 1w也看不到变化

兵长
答案时间:2020-10-15
@FARO_Z 嗯。我把sleep时间改成500。问题就出现了 ~~~~ 盖伦的初始血量是 1000 1000个增加线程和1000个减少线程结束后 盖伦的血量变成了 1001 线程争夺中,sleep稍微‘久’一点,效果明显

FARO_Z
答案时间:2020-10-13
我昨天在群里问了一下,可能是你的CPU主频比较高,导致start后,还没没来得及运行,就开始运行后面的join,这样就形成了加血完成,再减血的问题.....



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到





2020-07-16 关于循环体内的多线程问题
2020-07-14 步骤四的代码为什么没有实现多线程?
2020-06-28 不知道代码的用处


提问太多,页面渲染太慢,为了加快渲染速度,本页最多只显示几条提问。还有 37 条以前的提问,请 点击查看

提问之前请登陆
提问已经提交成功,正在审核。 请于 我的提问 处查看提问记录,谢谢
关于 JAVA 中级-多线程-同步 的提问

尽量提供截图代码异常信息,有助于分析和解决问题。 也可进本站QQ群交流: 578362961
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
在已经存在的几千个提问里,有相当大的比例,是因为使用了和站长不同版本的开发环境导致的,比如 jdk, eclpise, idea, mysql,tomcat 等等软件的版本不一致。
请使用和站长一样的版本,可以节约自己大量的学习时间。 站长把教学中用的软件版本整理了,都统一放在了这里, 方便大家下载: https://how2j.cn/k/helloworld/helloworld-version/1718.html

上传截图