synchronized关键字
创始人
2024-04-09 01:26:53
0

多线程编程中,最让人头疼的问题莫过于线程安全,如果对存在线程安全问题的代码不加以处理,可能会带来严重的后果,例如用两个线程对同一个变量进行增加操作

class Counter {//这个 变量 是两个线程要去自增的变量public int count;public void  increase() {count++;}
}public class Demo15 {private static Counter counter = new Counter();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0; i < 50000; ++i) {counter.increase();}});t1.start();Thread t2 = new Thread(()->{for(int i = 0; i < 50000; ++i) {counter.increase();}});t2.start();//等待t1和t2执行完,再打印count的结果t1.join();t2.join();//在main中打印一下两个线程自增完成后,得到的count结果System.out.println(counter.count);//如果不加锁,极端情况//所有的操作都是串行的,最终结果就是10w(可能出现,极小概率事件)//所有操作都是交错的(并行),最终结果就是5w(可能出现,极小概率事件)}
}

在这里插入图片描述

预期结果为10w,但实际结果却为67627,这就是线程安全问题导致的。

产生线程不安全的原因有很多:

  1. 线程是抢占式执行的,线程之间的调度充满随机性(线程不安全的万恶之源,但是我们无可奈何)
  2. 多个线程对同一个变量进行修改操作(如果多个线程针对不同的变量进行修改,没事。如果多个线程针对同一个变量读,也没事),上诉代码线程不安全就是这个原因导致的
  3. 针对变量的操作不是原子的(针对有些操作,比如读取变量的值,只是对应的一条机器指令,此时这样的操作本身就可以视为原子的。通过加锁操作,也可以把好几条指令打包成原子的)
  4. 内存可见性也会影响到线程安全。例如针对同一个变量,线程A进行循环读取,但循环内部并不修改,此时编译器就会优化,把这个变量从内存保存到寄存器中,每次都读取寄存器中的内容(这样做是为了提高效率,因为寄存器的读写效率高于内存),此时线程B修改了这个变量(从内存中读到CPU上,修改完毕后,再写回内存),但不会影响线程A,因为线程A并没有从内存中读取这个变量。在Java中,内存可见性用关键字volatile保证,但它不保证原子性
  5. 指令重排序也会影响到线程安全,不了解的可以看我之前写的博客指令重排问题。大部分代码,彼此的顺序,谁在前,谁在后,无所谓,些代码却依赖前后关系,编译器就会智能的调整代码的前后顺序,从而提高程序的效率,但是应该保证逻辑不变的情况下,再去调整顺序。如果代码是单线程的程序,编译器的判定一般都是很准的,但是如果代码是多线程,编译器也可能存在误判。 volatile也能防止指令重排序

上述介绍的这5种情况,都是产生线程不安全的原因

对此我们应该对increase()方法加锁或者count这个变量加锁,在C++中需要创建mutex变量,再加锁,然后再解锁,个人觉得有点麻烦(C++加锁的方式有很多)。在Java中,加锁的方式也有很多,最简单,最常用的方式就是在increase()方法最前面加上synchronized关键字,将整个方法锁住,这样就解决了线程安全问题

class Counter {//这个 变量 是两个线程要去自增的变量public int count;synchronized public void  increase() {count++;}
}

在这里插入图片描述

synchronized 会自动加锁,本质是修改了Object对象中的"对象头"里面的一个标记
synchronized是个可重入锁,可重入锁内部会记录当前的锁被哪个线程占用,同时也会记录一个"加锁次数(引用计数)"。当锁的计数减到0后,就解锁,可重入锁的意义就是降低了使用成本,提高了开发效率,但是也带来了更大的开销(维护锁属于哪个线程,并增加了计数,降低了运行效率)

synchronized 的使用方法
1.直接修饰普通的方法
使用synchronized的时候,本质就是针对某个"对象"进行加锁,此时锁对象就是this

在这里插入图片描述

2.直接修饰代码块
需要显示指定哪个对象需要加锁(Java中的任何对象都可以作为锁对象)

在这里插入图片描述

3.修饰静态方法(更严谨的叫法应该是"类方法")
相当于针对当前类的类对象加锁

在这里插入图片描述

synchronized

  1. 既是一个乐观锁,也是一个悲观锁(根据锁竞争的激烈程度,自适应)
  2. 是一个普通的互斥锁
  3. 既是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应)
  4. 轻量级锁的部分基于自旋锁实现,重量级的部分基于挂起等待锁实现
  5. 非公平锁
  6. 可重入锁

synchronized几个典型的优化手段(只考虑JDK1.8)

1.锁膨胀/所升级

体现synchronized能够"自适应"这样的能力
代码还能执行到synchronized部分,此时处于无锁状态
当首个线程执行到了synchronized部分,此时就会进入偏向锁状态,偏向锁只是做了一个标记,并没有真的加锁,这样带来的好处就是后续如果没有线程竞争,就避免了加锁,解锁带来的开销
如果此时又有其他线程执行到了synchronized部分,产生锁竞争,此时进入**轻量级锁(自旋锁)状态
如果竞争进一步加剧,就会进入
重量级锁(互斥锁)**状态

无锁——>偏向锁——>轻量级锁(自旋锁)——>重量级锁(互斥锁)

2.锁粗化/细化

此处的粗细是指"锁的粒度"
"锁的粒度"是指加锁的代码涉及到的范围
加锁的代码范围越大,认为锁的粒度越粗
加锁的代码范围越小,认为锁的粒度越细

到底锁的粒度是粗好,还是细好?各有各的好
如果锁的粒度比较细,多个线程之间的并发性就更高
如果锁的粒度比较粗,加锁解锁的开销就更小

Java编译器就会有一个优化,会自动判定(一般来说编译器优化后,效率会变高,但也有意外情况)
如果两次加锁之间的间隔较大(中间隔的代码多),会细化(一般不会进行这种优化)
如果两次加锁之间的间隔较小(中间隔的代码少),会粗化(很可能触发这个优化)


锁消除

有些代码,明明不用加锁,结果你给上锁了,编译器就会发现这个加锁操作好像没什么必要,就直接把锁给去掉了

例如给单线程进行加锁,这个时候编译器就会进行锁消除,
单线程中使用到了StringBuffer,Vector等,它们都是在标准库中进行的加锁操作,实际使用的时候可能存在锁消除

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...