how2j.cn


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

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

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



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



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

步骤 1 :

演示同步问题

假设盖伦有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 :

分析同步问题产生的原因

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关键字的意义
如下代码:

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 解决同步问题

所有需要修改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对象作为同步对象

既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个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

在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 :

线程安全的类

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

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

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

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

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

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

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

在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
因为需要需要反射reflection的知识,所以答案见反射的答案-在静态方法上加synchronized,同步对象是什么?
步骤 11 :

练习-线程安全的MyStack

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

答案-线程安全的MyStack

在查看答案前,尽量先自己完成,碰到问题再来查看答案,收获会更多
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公众号,关注后实时获知布最新的教程和优惠活动,谢谢。


问答区域    
2018-09-22 关于步骤4、5中join()方法的个人理解
长叹
按照前面章节讲的,A线程调用join()方法以后,只有等A线程运行结束,才会运行B线程。而在步骤4和步骤5中,因为使用了增强型for循环不断地加入join()方法,才使得两个线程不断地交叉运行,避免了出现加血线程全部运行完了才开始减血线程的情况 个人愚见,不知正确与否,劳请站长指点,谢谢








答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2018-08-24 大佬们看看这样为何报错?就是把thread.sleep加入到synchornized占用里边,会出现内存溢出的报错!
liaoxiaowen



rt
 try {
                    	synchronized (someObject) {
    						gareen.hurt();
    						Thread.sleep(100);
    					}
                    	
                        
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
盖伦的初始血量是 10000
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:714)
	at multiplethread.TestThread4.main(TestThread4.java:69)






答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到





2018-08-24 为什么要加thread.sleep啊?还有就是我把sleep加入synchronized 占用中,会报错,现实outofmemory
2018-06-21 视频里杂音太大了
2018-06-08 思路是对的。。代码不对啊 加入join是所有加完到2W左右再所有减
2018-02-24 视频里的噪音能去掉吗。或者重录一下视频。噪音太影响学习了。
2018-01-30 t.join() 为什么不能直接写在t.start()后面?
2018-01-22 addThreads[i] = t; 这句的作用
2017-12-01 演示同步问题的代码中,即使只有recover部分的线程,也会出现同步问题
2017-11-29 多线程同步问题
2017-10-30 非常奇怪的一个问题关于t1,t2谁先占有someObject,希望站长或者或者其他朋友能解答一下
2017-09-13 这里面是不是可以不用join方法,以及不需要那2个线程的数组,也能达到同样的效果?
2017-08-18 非同步的代码可以对加锁的临界资源进行访问
2017-08-10
2017-07-26 老师好,想请问下关于Hero类的线程安全问题。
2017-07-08 在对象方法前,加上修饰符synchronized ,同步对象是当前实例。
2017-06-10
2017-06-08 为什么要final啊
2017-05-17 主线程会等其他线程吗
2017-04-20 关于join提问
2017-04-20 在对象前加同步和在方法前加同步哪个更常用
2017-04-19 synchronized修饰静态方法,能保证同步效果吗
2017-02-17 采用步骤五里面的方法没起到同步的作用
2017-01-15 这里调用join()方法是什么意思呢?




提问之前请登陆
关于 JAVA 中级-多线程-同步 的提问

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

上传截图