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


问答区域    
2024-07-30 练习-线程安全的MyStack
木宇




练习-线程安全的MyStack
package com.cwt.study.java中级.s_20240729;

import java.util.LinkedList;

public class TestThreadTest2 implements Stack{
    LinkedList<Hero> heros = new LinkedList<Hero>();
    //插入
    public synchronized void push(Hero h) {
        heros.addLast(h);
    }
    //删除
    public synchronized Hero pull() {
        return heros.removeLast();
    }

    //查看
    public synchronized Hero peek() {
        return heros.getLast();
    }

    public static void main(String[] args) {

        TestThreadTest2 heroStack = new TestThreadTest2();
        for (int i = 0; i < 5; i++) {
            Hero h = new Hero("hero name " + i);
            System.out.println("压入 hero:" + h);
            heroStack.push(h);
        }
        /*for (int i = 0; i < 5; i++) {
            Hero h =heroStack.pull();
            System.out.println("弹出 hero" + h);

        }*/
        heroStack.peek();
        System.out.println( heroStack.peek());
    }
}

							





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





2024-07-13 线程安全的MyStack
虚心求学




线程不安全栈测试: 初始值 size:1 [[teemo,1,20]] 线程不安全栈的大小为:1 对栈做同等次数(10000)的入栈、出栈操作 增减线程运行后: size:9818 Error 线程不安全栈的大小为:9818 ----------------------------------------------------------- 线程安全栈测试: 初始值 size:1 [[teemo,1,20]] 线程安全栈的大小为:1 对栈做同等次数(10000)的入栈、出栈操作 增减线程运行后: size:1 [[teemo,1,20]] 线程安全栈的大小为:1
package j2se;

import java.util.LinkedList;

public class MyStack<E> implements Stack<E> {

	private LinkedList<E> value;

	public MyStack() {
		value = new LinkedList<>();
	}

	@Override
	public synchronized void push(E element) {
		value.add(element);
	}

	@Override
	public synchronized E pull() {
		if (!value.isEmpty())
			return value.pollLast();
		else
			return null;
	}

	@Override
	public synchronized E peek() {
		if (!value.isEmpty())
			return value.peekLast();
		else
			return null;
	}
	public synchronized int size()
	{
		return value.size();
	}
	@Override
	public synchronized String toString() {
		if (value.isEmpty())return null;
		String res = "size:"+size();
		try {
			res = "size:"+size()+" "+value.toString();
		} catch (Throwable e) {
			//e.printStackTrace();
			return "size:"+size()+" Error";
			// TODO: handle exception
		}
		return res;
	}

	public void print() {
		System.out.println(this);
	}

}

——————主方法调用——————

public static void main(String[] args) {
		Hero gareen = new Hero("gareen", 10);
		MyStack<Hero>stack = new MyStack<>();
		//加入一个初始值,teemo
		stack.push(new Hero("teemo",1));
		System.out.println("初始值");
		System.out.println(stack);
		System.out.println("线程安全栈的大小为:"+stack.size());
		System.out.println("");
		final int num = 10*1000;
		
		Thread[] pushThread = new Thread[num];
		Thread[] pullThread = new Thread[num];
		//对stack栈做同等数量的入栈、出栈操作
		//理论上,stack 经过上述操作后不被改变
		//stack应该只会剩余应该元素,即teemo
		for (int i = 0; i < num; i++) {
			Thread t = new Thread(() -> {
				stack.push(gareen);
			});
			t.start();
			pushThread[i] = t;
		}
		for (int i = 0; i < num; i++) {
			Thread t = new Thread(() -> {
				stack.pull();
			});
			t.start();
			pullThread[i] = t;
		}
		//mian主线程等待其他线程结束
		for (Thread t : pushThread) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		for (Thread t : pullThread) {
			try {
				t.join();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		//打印结果
		System.out.println("增减线程运行后:");
		System.out.println(stack);
		System.out.println("线程安全栈的大小为:"+stack.size());
	}

							





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





2021-03-02 final对象的值为什么之后可以改变?
2020-10-10 为什么我的步骤1没有引发同步问题
2020-07-16 关于循环体内的多线程问题


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

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

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

上传截图