并发编程-05线程安全性之原子性【锁之synchronized】

举报
小工匠 发表于 2021/09/10 23:21:38 2021/09/10
【摘要】 文章目录 线程安全性文章索引脑图概述原子性synchronized 修饰的4种对象修饰代码块作用范围及作用对象Demo多线程下 同一对象的调用多线程下不同对象的调用 修饰方法作用范围...

线程安全性文章索引

并发编程-03线程安全性之原子性(Atomic包)及原理分析

并发编程-04线程安全性之原子性Atomic包的4种类型详解

并发编程-05线程安全性之原子性【锁之synchronized】

并发编程-06线程安全性之可见性 (synchronized + volatile)

并发编程-07线程安全性之有序性


脑图

在这里插入图片描述


概述

举个例子:
【多线程场景】假设有个变量a在主内存中的初始值为1,线程A和线程B同时从主内存中获取到了a的值,线程A更新a+1,线程B也更新a+1,经过线程AB更新之后可能a不等于3,而是等于2。因为A和B线程在更新变量a的时候从主内存中拿到的a都是1,而不是等A更新完刷新到主内存后,线程B再从主内存中取a的值去更新a,所以这就是线程不安全的更新操作.

解决办法

  • 使用锁 1. 使用synchronized关键字synchronized会保证同一时刻只有一个线程去更新变量. 2、Lock接口 【篇幅原因先不讨论lock,另开篇介绍】。
  • 使用JDK1.5开始提供的java.util.concurrent.atomic包,见 并发编程-04线程安全性之原子性Atomic包详解

先简单说下synchronized和lock

  • synchronized 依赖jvm

  • lock 依赖特殊的cpu指令,代码实现,比如ReentranLock

这里我们重点来看下synchronized关键字是如何确保线程安全的原子性的。


原子性synchronized 修饰的4种对象

  • 修饰代码块
  • 修饰方法
  • 修饰静态方法
  • 修饰类

修饰代码块

作用范围及作用对象

被修饰的代码被称为同步语句块,作用范围为大括号括起来的代码,作用于调用的对象, 如果是不同的对象,则互不影响

Demo

多线程下 同一对象的调用

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedDemo {
	
	public void test() {
		// 修饰代码块 ,谁调用该方法synchronized就对谁起作用   即作用于调用的对象 。 如果是不同的对象,则互不影响
		synchronized (this) {
			for (int i = 0; i < 10; i++) {
				log.info("修饰代码块 i = {} ",i);
			}
		}
	}
	
	public static void main(String[] args) {
		// 同一个调用对象
		SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
		ExecutorService executorService = Executors.newCachedThreadPool();
		// 启动两个线程去 【使用同一个对象synchronizedDemo】调用test方法 
		for (int i = 0; i < 2; i++) {
			executorService.execute(() ->{
				synchronizedDemo.test();
			});
		}
		
 //     使用Thread  可以按照下面的方式写	
//		for (int i = 0; i < 2; i++) {
//			new Thread(()-> {
//				synchronizedDemo.test2();
//			}).start();
//		}
		
		
		// 最后 关闭线程池
		executorService.shutdown();
	}
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

我们先思考下执行的结果是什么样子的?

上述代码,我们通过线程池,通过循环开启了2个线程去调用含有同步代码块的test方法 , 我们知道 使用synchronized关键字修饰的代码块作用的对象是调用的对象(同一个对象)。 因此这里的两个线程都拥有同一个对象synchronizedDemo的引用,两个线程,我们命名为线程A 线程B。 当线程A调用到了test方法,因为有synchronized关键字的存在,线程B只能等待线程A执行完。 因此A会输出0~9,线程A执行完之后,A释放锁,线程Bh获取到锁后,继续执行。

实际执行结果:

[图片]

符合我们分析和预测。

如果我们把 test 方法的synchronized关键字去掉会怎样呢? 来看下

在这里插入图片描述
执行结果

在这里插入图片描述

可知,synchronized关键字修饰的代码块,确保了同一调用对象在多线程的情况下的执行顺序


多线程下不同对象的调用

为了更好地区分,我们给调用方法加个参数

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedDemo {

	public void test(String flag) {
		// 修饰代码块 ,谁调用该方法synchronized就对谁起作用 即作用于调用的对象 。 如果是不同的对象,则互不影响
		synchronized (this) {
			for (int i = 0; i < 10; i++) {
				log.info("{} 调用 修饰代码块 i = {} ",flag ,i);
			}
		}
	}
	
	public static void main(String[] args) {
		
		ExecutorService executorService = Executors.newCachedThreadPool();

		
		// 对象 synchronizedDemo
		SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
		
		// 对象 synchronizedDemo2
		SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

		// synchronizedDemo 调用 test
		executorService.execute(()->{
			synchronizedDemo.test("synchronizedDemo");
		});
		
		// synchronizedDemo2 调用 test
		executorService.execute(()->{
			synchronizedDemo2.test("synchronizedDemo2");
		});
		
		// 最后 关闭线程池
		executorService.shutdown();
	}
}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

先来猜测下执行结果呢?
两个不同的对象,调用test方法,应该是互不影响的,所以执行顺序是交替执行的。

运行结果:

在这里插入图片描述


修饰方法

被修饰的方法称为同步方法,作用的范围是整个方法,作用于调用的对象, 如果是不同的对象,则互不影响

作用范围及作用对象

同 修饰代码块

Demo

增加个方法 test2

	// 修饰方法 谁调用该方法synchronized就对谁起作用 即作用于调用的对象 。 如果是不同的对象,则互不影响
	public synchronized void test2() {
		// 修饰代码块 ,
		for (int i = 0; i < 10; i++) {
			log.info("调用 修饰代码块 i = {} ",  i);
		}
	}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

多线程下同一个对象的调用

同 修饰代码块

在这里插入图片描述
结果:
在这里插入图片描述


多线程下不同对象的调用

同 修饰代码块

在这里插入图片描述
结果:
在这里插入图片描述


通过上面的测试结论可以知道 修饰代码块和修饰方法
如果一个方法内部是一个完整的synchronized代码块,那么效果和synchronized修饰的方法效果是等同的 。

还有一点需要注意的是,如果父类的某个方法是synchronized修饰的,子类再调用该方法时,是不包含synchronized. 因为synchronized不属于方法声明的一部分。 如果子类想使用synchronized的话,需要在方法上显示的声明其方法为synchronized


修饰静态方法

作用范围及作用对象

整个静态方法, 作用于所有对象


Demo

多线程同一个对象的调用

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedStaticMethodDemo {

	// 修饰静态方法
	public synchronized static void test() {
		for (int i = 0; i < 10; i++) {
			log.info("调用 修饰方法 i = {} ", i);
		}
	}

	public static void main(String[] args) {

		ExecutorService executorService = Executors.newCachedThreadPool();
		
		for (int i = 0; i < 2; i++) {
			executorService.execute(() ->{
				test();
			});
		}
	

		// 最后 关闭线程池
		executorService.shutdown();
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

结果:

在这里插入图片描述


多线程下不同对象的调用

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedStaticMethodDemo {

	// 修饰静态方法
	public synchronized static void test() {
		for (int i = 0; i < 10; i++) {
			log.info("调用 修饰方法 i = {} ", i);
		}
	}

	public static void main(String[] args) {

		ExecutorService executorService = Executors.newCachedThreadPool();
		
		SynchronizedStaticMethodDemo demo1 = new SynchronizedStaticMethodDemo();
		SynchronizedStaticMethodDemo demo2 = new SynchronizedStaticMethodDemo();
		
		// demo1调用
		executorService.execute(() ->{
			// 其实直接调用test方法即可,这里仅仅是为了演示不同对象调用 静态同步方法
			demo1.test();
		});
		
		// demo2调用
		executorService.execute(() ->{
			// 其实直接调用test方法即可,这里仅仅是为了演示不同对象调用 静态同步方法
			demo2.test();
		});
		
		// 最后 关闭线程池
		executorService.shutdown();
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

结果:

在这里插入图片描述


修饰类

作用范围及作用对象

修饰范围是synchronized括号括起来的部分,作用于所有对象


Demo

多线程下同一对象的调用

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedStaticClassDemo2 {

	// 修饰一个类
	public  void test() {
		synchronized (SynchronizedStaticClassDemo2.class) {
			for (int i = 0; i < 10; i++) {
				log.info("调用 修饰方法 i = {} ", i);
			}
		}
	}

	public static void main(String[] args) {

		ExecutorService executorService = Executors.newCachedThreadPool();
		
		SynchronizedStaticClassDemo2 demo = new SynchronizedStaticClassDemo2();
		
		// demo调用
		executorService.execute(() ->{
			demo.test();
		});
		
		// demo调用
		executorService.execute(() ->{
			demo.test();
		});
		
		// 最后 关闭线程池
		executorService.shutdown();
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

结果
在这里插入图片描述


多线程下不同对象的调用

package com.artisan.example.sync;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SynchronizedStaticClassDemo2 {

	// 修饰一个类
	public  void test() {
		synchronized (SynchronizedStaticClassDemo2.class) {
			for (int i = 0; i < 10; i++) {
				log.info("调用 修饰方法 i = {} ", i);
			}
		}
	}

	public static void main(String[] args) {

		ExecutorService executorService = Executors.newCachedThreadPool();
		
		SynchronizedStaticClassDemo2 demo1 = new SynchronizedStaticClassDemo2();
		SynchronizedStaticClassDemo2 demo2 = new SynchronizedStaticClassDemo2();
		
		// demo1调用
		executorService.execute(() ->{
			demo1.test();
		});
		
		// demo2调用
		executorService.execute(() ->{
			demo2.test();
		});
		
		// 最后 关闭线程池
		executorService.shutdown();
	}
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

结果

在这里插入图片描述


使用Synchronized来保证线程安全

先回顾下 线程不安全的写法

在这里插入图片描述

方法一

下面用Synchronized来改造下

我们知道synchronized修饰静态方法,作用的对象是所有对象 , 因此 仅需要将 静态add方法 修改为同步静态方法即可。
在这里插入图片描述

多次运算
在这里插入图片描述


方法二

假设 add方法不是静态方法呢? 我们知道 当synchronized修饰普通方法,只要是同一个对象,也能保证其原子性

假设 add方法为普通方法
在这里插入图片描述

改造如下:

在这里插入图片描述

多次运行,结果总是10000

在这里插入图片描述


原子性的实现方式小结

  • synchronized
    不可中断锁,适合不激烈的竞争,可读性较好

  • atomic包
    竞争激烈时能维持常态,比Lock性能好,但只能同步一个值

  • lock
    可中断锁,多样化同步,竞争激烈时能维持常态。后面针对lock单独展开。


代码

https://github.com/yangshangwei/ConcurrencyMaster

文章来源: artisan.blog.csdn.net,作者:小小工匠,版权归原作者所有,如需转载,请联系作者。

原文链接:artisan.blog.csdn.net/article/details/87537199

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。