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公众号,关注后实时获知布最新的教程和优惠活动,谢谢。


问答区域    
2017-12-01 演示同步问题的代码中,即使只有recover部分的线程,也会出现同步问题
ik
hp初值10000,10000个线程操纵hp+1,线程运行完毕后,hp的值也未必是20000,对吧




2 个答案

嫩岩 答案时间:2017-12-21
有可能不到20000,但是不会超过20000.

TuoAiTang 答案时间:2017-12-13
不存在占用顺序的问题啊??




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




2017-11-29 多线程同步问题
孤独的狼



为什么把Thread.sleep(100)改成1000就出问题了
package multiplethread;

import charactor.Hero;

public class Tread {
	public static void main(String[] args) {
		final Hero gareen = new Hero("盖伦",10000,1);
		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(1000);
					}catch(Exception e){
						e.printStackTrace();
					}
				}
			};
			addThreads[i] = t;
			t.start();
		}
		
		for(int i = 0; i < n; i++){
			Thread t = new Thread(){
				public void run(){
					gareen.hurt();
					try{
						Thread.sleep(1000);
					}catch(Exception e){
						e.printStackTrace();
					}
				}
			};
			t.start();
			reduceThreads[i] = t;
		}
		for(Thread t:addThreads){
			try{
				t.join();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
		for(Thread t:reduceThreads){
			try{
				t.join();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
		System.out.println(gareen.hp);
		
	}
}

							






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





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群交流: 389538688
站长会在每个工作日早上尽量回答提问(如果有漏掉没有回答的,请进群提醒一下)
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
站长是玻璃心,提问的时候请语气温柔些 kiss~
截止2017-5-19日累计提问 1638个,站长回答了 1546个
截止2017-8-15日累计提问 2788个,站长回答了 2544个

上传截图