JUC高并发编程(二)——Synchronized关键字

news2024/7/6 20:42:45

文章目录

  • 前言
    • 为什么要用Synchronized关键字
  • 并发编程中的三个问题
    • 可见性
    • 原子性
    • 有序性
  • Synchronized保证三大特性
    • 使用synchronized保证可见性
    • 使用synchronized保证原子性
    • 用synchronized保证有序性
  • Synchronized的特征
    • 可重入特征
    • 不可中断特征

前言

    synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。即synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。它包括两种用法:synchronized 方法和 synchronized 块。

为什么要用Synchronized关键字

    在使用多线程进行并发编程的时候,如果有多个线程来操作共享数据,很有可能共享数据的值会出现错乱,我们称之为线程安全问题。导致出现问题的原因有:可见性问题;原子性问题;有序性问题。

并发编程中的三个问题

可见性

  • 可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。
  • 出现可见性问题的两个前提:至少有两个线程、有个共享变量

可见性问题演示:

/**
 * 目标:演示可见性问题
 * 1.创建一个共享变量
 * 2.创建一条线程不断读取共享变量
 * 3.创建一条线程修改共享变量
 */
public class Test01Visibility {
 
    // 1. 创建一个共享变量
    private static boolean flag = true;
 
    public static void main(String[] args) throws InterruptedException {
        // 2. 创建一条线程不断读取共享变量
        new Thread(() -> {
            while (flag) {
 
            }
        }).start();
 
        Thread.sleep(2000);
 
        // 3. 创建一条线程修改共享变量
        new Thread(() -> {
            flag = false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
 
}

当打印 “另一个线程修改了flag:false”,程序并没有停止,也就是说,线程1获取的flag还是初始获取的true,并没有立即得到修改后的值。
并发编程时,会出现可见性问题,一个线程对共享变量进行修改,另一个线程不能立即得到修改后的值(获取变量的值还是旧的值)

原子性

  • 原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
  • 出现原子性问题的两个前提:至少有两个线程、有个共享变量。
    原子性问题演示:
/**
 * 目标:演示原子性问题
 * 1.定义一个共享变量number
 * 2.对number进行1000的++操作
 * 3.使用5个线程来进行
 */
public class Test02Atomictity {
    // 1. 定义一个共享变量number
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // 2. 对number进行1000的++操作
        Runnable increment = ()  -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };
        List<Thread> list = new ArrayList<>();
        // 3. 使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t : list) {
            // 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
            t.join();
        }
        System.out.println("number = " + number);
    }
}

5个线程,每个线程执行1000次number++,最终结果应该是5000,但打印结果是“number:4130”。说明number++操作并不是原子性操作。
使用javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
在这里插入图片描述
其中,对于 number++ 而言(number 为静态变量),实际会产生如下的 JVM 字节码指令:

9: getstatic #12       // 获取静态字段number的值并将其推送到操作数栈中
12: iconst_1           // 将整数常量1推送到操作数栈中
13: iadd                 // 将操作数栈顶的两个整数相加,并将结果推送到操作数栈中
14: putstatic #12   // 将操作数栈顶的值存储到静态字段number中
注释:这段代码的作用是将静态字段number的值增加1。

由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd时,另一个线程又执行9: getstatic。会导致两次number++,实际上只加了1。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

有序性

  • 有序性:程序执行的顺序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。
    例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。
    有序性问题代码演示:
    jcstress是java并发压测工具:https://wiki.openjdk.java.net/display/CodeTools/jcstress
    修改pom文件,添加依赖:
      <dependency>
            <groupId>org.openjdk.jcstress</groupId>
            <artifactId>jcstress-core</artifactId>
            <version>0.3</version>
        </dependency>
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
class Test03Orderliness {
    int num = 0;
    boolean ready = false;
    // 线程一执行的代码
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

I_Result 是一个对象,有一个属性r1 用来保存结果,在多线程情况下可能出现几种结果?
情况1:线程1先执行actor1,这时ready = false,所以进入else分支结果为1。
情况2:线程2执行到actor2,执行了num = 2;和ready = true,线程1执行,这回进入 if 分支,结果为4。
情况3:线程2先执行actor2,只执行num = 2;但没来得及执行 ready = true,线程1执行,还是进入else分支,结果为1。
还有一种结果0。
运行测试:

mvn clean install
java -jar target/jcstress.jar

总结:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。

Synchronized保证三大特性

使用synchronized保证可见性

package com.itheima.demo02_concurrent_problem;
/**
案例演示:
一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,
另一个线程并不会停止循环.
*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
	private static boolean run = true;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			while (run) {
				// 增加对象共享数据的打印,println是同步方法
				System.out.println("run = " + run);
			}
		});
		t1.start();
		Thread.sleep(1000);
		Thread t2 = new Thread(() -> {
			run = false;
			System.out.println("时间到,线程2设置为false");
		});
		t2.start();
	}
}

在这里插入图片描述
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。那加了Synchronized关键字后,就可保证共享资源的可见性。

使用synchronized保证原子性

public class Test02Atomictity {
    // 1. 定义一个共享变量number
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // 2. 对number进行1000的++操作
        Runnable increment = ()  -> {
            for (int i = 0; i < 1000; i++) {
            	synchronized (Test01Atomicity.class) {
					number++;
				}
            }
        };
        List<Thread> list = new ArrayList<>();
        // 3. 使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t : list) {
            // 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
            t.join();
        }
        System.out.println("number = " + number);
    }
}

对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。
还是javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
在这里插入图片描述
保证一个线程把这几步执行完之后,另一个线程才能执行不会中途被抢占,从而保证原子性。

用synchronized保证有序性

重排序:为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

int a = 1;
int b = 2;
int c = a + b;

上面3个操作的数据依赖关系如图所示:
在这里插入图片描述
如上图所示a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。
但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。
下图是该程序的两种执行顺序。
在这里插入图片描述

可以这样:
int a = 1;
int b = 2;
int c = a + b;
 
也可以重排序这样:
int b = 2;
int a = 1;
int c = a + b;

使用synchronized保证有序性代码示例:

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
class Test03Orderliness {
	private Object obj = new Object();
    int num = 0;
    boolean ready = false;
    // 线程一执行的代码
    @Actor
    public void actor1(I_Result r) {
    	synchronized(obj){
	        if(ready) {
	            r.r1 = num + num;
	        } else {
	            r.r1 = 1;
	        }
	    }
    }
    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
    	synchronized(obj){
        	num = 2;
        	ready = true;
        }
    }
}

synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

Synchronized的特征

可重入特征

可重入:一个线程可以多次执行synchronized,重复获取同一把锁。
可重入特征代码示例:

public class Demo01 {
	public static void main(String[] args) {
		Runnable sellTicket = new Runnable() {
		@Override
		public void run() {
			synchronized (Demo01.class) {
				System.out.println("我是run");
				test01();
			}
		}
		public void test01() {
			synchronized (Demo01.class) {
				System.out.println("我是test01");
			}
		}
		};
		new Thread(sellTicket).start();
		new Thread(sellTicket).start();
	}
}

可重入原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

不可中断特征

不可中断:一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
Synchronized不可中断代码演示:

/**
* 目标:演示synchronized不可中断
* 1. 定义一个Runnable
* 2. 在Runnable定义同步代码块
* 3. 先开启一个线程来执行同步代码块,保证不退出同步代码块
* 4. 后开启一个线程来执行同步代码块(阻塞状态)
* 5. 停止第二个线程
*/
public class Demo02_UnInterruptible {

   private static Object obj = new Object();

   public static void main(String[] args) throws InterruptedException {
       // 1. 定义一个Runnable
       Runnable run = () -> {
           // 2. 在Runnable定义同步代码块
           synchronized (obj) {
               String name = Thread.currentThread().getName();
               System.out.println(name + "进入同步代码块");
               // 保证不退出同步代码块
               try {
                   Thread.sleep(888888);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       };

       // 3. 先开启一个线程来执行同步代码块
       Thread t1 = new Thread(run);
       t1.start();
       Thread.sleep(1000);
       // 4. 后开启一个线程来执行同步代码块(阻塞状态)
       Thread t2 = new Thread(run);
       t2.start();

       // 5.停止第二个线程
       System.out.println("停止线程前");
       t2.interrupt();
       System.out.println("停止线程后");

       System.out.println(t1.getState());

       System.out.println(t2.getState());
   }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/801216.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python爬虫时遇到SSL证书验证错误解决办法汇总

在进行Python爬虫任务时&#xff0c;遇到SSL证书验证错误是常见的问题之一。SSL证书验证是为了确保与服务器建立的连接是安全和可信的&#xff0c;但有时候可能会由于证书过期、不匹配或未受信任等原因导致验证失败。为了解决这个问题&#xff0c;本文将提供一些实用的解决办法…

提高业务效率:利用手机号在网状态 API 进行智能筛选

引言 随着科技的不断发展&#xff0c;手机已成为现代人生活中不可或缺的工具。人们通过手机完成通信、娱乐、购物等各种活动&#xff0c;使得手机号成为了一个重要的个人标识。对于企业而言&#xff0c;了解手机号的在网状态对于业务发展和客户管理至关重要。为了提高业务效率…

https和http有什么区别

https和http有什么区别 简要 区别如下&#xff1a; ​ https的端口是443.而http的端口是80&#xff0c;且二者连接方式不同&#xff1b;http传输时明文&#xff0c;而https是用ssl进行加密的&#xff0c;https的安全性更高&#xff1b;https是需要申请证书的&#xff0c;而h…

Linux常用命令——dpkg-statoverride命令

在线Linux命令查询工具 dpkg-statoverride Debian Linux中覆盖文件的所有权和模式 补充说明 dpkg-statoverride命令用于Debian Linux中覆盖文件的所有权和模式&#xff0c;让dpkg于包安装时使得文件所有权与模式失效。 语法 dpkg-statoverride(选项)选项 -add&#xff1…

深度:解密数据库的诗与远方!

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 不同于历史上的黄金和石油&#xff0c;数据成为了我们新的宝藏&#xff0c;一个驱动社会进步、催生创新的无尽源泉。然而&#xff0c;这些形式各异、复杂纷繁的数据需要一个管理者&#xff0c;一个保险库&#xff0c;一个解…

【动态规划part09】| 198.打家劫舍、213.打家劫舍II、337.打家劫舍III

&#x1f388;LeetCode198.打家劫舍 链接&#xff1a;198.打家劫舍 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷…

【数据结构】实验三:链表

实验三链表 一、实验目的与要求 1&#xff09;熟悉链表的类型定义&#xff1b; 2&#xff09;熟悉链表的基本操作&#xff1b; 3&#xff09;灵活应用链表解决具体应用问题。 二、实验内容 1&#xff09;请设计一个单链表的存储结构&#xff0c;并实现单链表中基本运算算…

基于ssm+mysql+jsp高校疫情防控出入信息管理系统

基于ssmmysqljsp高校疫情防控出入信息管理系统 一、系统介绍二、功能展示1.登陆2.教师管理3.学生管理4.打卡记录管理5.学生申请通行证6.通行证管理7.留言信息管理8.公告类型管理9.公告管理 四、获取源码 一、系统介绍 学生 : 个人中心、打卡记录管理、学生申请通行证、通行证管…

Java 8 Stream流:代码简洁之道

文章目录 前言一、filter二、map三、mapToInt、mapToLong、mapToDouble四、flatMap五、flatMapToInt、flatMapToLong、flatMapToDouble六、distinct七、sorted八、peek九、limit十、forEach十一、forEachOrdered十二、toArray十三、reduce十四、collect十五、min、max十六、cou…

mysql(二) 索引-基础知识

继续整理复习、我以我的理解和认知来整理 "索引" 会通过 文 和 图 来展示。 文&#xff1a; 基本概念知识&#xff08;mysql 的索引分类、实现原理&#xff09; 图&#xff1a; 画B树等 MySQL官方对索引的定义是&#xff1a;索引&#xff08;Index&#xff09;是帮…

记录--虚拟 DOM 和实际 DOM 有何不同?

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 前言 本文我们会先聊聊 DOM 的一些缺陷&#xff0c;然后在此基础上介绍虚拟 DOM 是如何解决这些缺陷的&#xff0c;最后再站在双缓存和 MVC 的视角来聊聊虚拟 DOM。理解了这些会让你对目前的前端框架有…

第四章 HL7 架构和可用工具 - 查看数据结构

文章目录 第四章 HL7 架构和可用工具 - 查看数据结构查看数据结构查看代码表使用自定义架构编辑器 第四章 HL7 架构和可用工具 - 查看数据结构 查看数据结构 当单击“数据结构”列中的名称时&#xff0c;InterSystems 会显示该数据结构中的所有字段。这是 HL7 数据结构页面。…

影视行业案例 | 燕千云助力大地影院集团搭建智能一体化IT服务管理平台

影视行业过去三年受新冠肺炎疫情影响&#xff0c;经历了一定程度的冲击和调整&#xff0c;但也展现出了强大的韧性和潜力。2023年中国影视产业规模可能达到2600亿元左右&#xff0c;同比增长11%左右。影视行业的发展趋势主要表现在内容创新、模式创新和产业融合三个方面&#x…

第八章:将自下而上、自上而下和平滑性线索结合起来进行弱监督图像分割

0.摘要 本文解决了弱监督语义图像分割的问题。我们的目标是在仅给出与训练图像关联的图像级别对象标签的情况下&#xff0c;为新图像中的每个像素标记类别。我们的问题陈述与常见的语义分割有所不同&#xff0c;常规的语义分割假设在训练中可用像素级注释。我们提出了一种新颖的…

PSP - MMseqs2 编译最新版本源码 (14-7e284) 支持 MPI 功能 MSA 快速搜索

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/131966061 MPI (Message Passing Interface) 是用于并行计算的标准化和可移植的消息传递接口&#xff0c;可以在分布式内存的多台计算机上运行并行…

操作系统、人工智能、芯片和其它

最近出差一段时间&#xff0c;听到一些事&#xff0c;看到一些事&#xff0c;说点个人观感。有些话可能不好听&#xff0c;还希望不要被平台和谐。   从一位现在微软工作的前同事处得来的消息&#xff0c;微软下一代操作系统Windows 12将深度集成AI&#xff0c;如果再加上的它…

彻底搞懂CPU的特权等级

x86 处理器中,提供了4个特权级别:0,1,2,3。数字越小,特权级别越高! 一般来说,操作系统是的重要性、可靠性是最高的,需要运行在 0 特权级; 应用程序工作在最上层,来源广泛、可靠性最低,工作在 3 特权级别。 中间的1 和 2 两个特权级别,一般很少使用。 理论上来讲,…

redis到底几个线程?

通常我们说redis是单线程指的是从接收客户端请求->解析请求->读写->响应客户端这整个过程是由一个线程来完成的。这并不意味着redis在任何场景、任何版本下都只有一个线程 为何用单线程处理数据读写&#xff1f; 内存数据储存已经很快了 redis相比于mysql等数据库是…

集合---list接口及实现类

一、list概述 1、list接口概述 List接口继承自Collection接口&#xff0c;是单列集合的一一个重要分支&#xff0c;我们习惯性地会将实现了 List接口的对象称为List集合。在List集合中允许出现重复的元素&#xff0c;所有的元素是以一种线性方 式进行有序存储的&#xff0c;在…

在linux中怎样同时运行三个微服务保证退出时不会终止

前言 1.maven中打jar包 使用插件打包,必须在pom.xml中添加插件,否则不能在linux中编译运行 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version&g…