【并发】并发锁机制-深入理解synchronized(一)
【并发】并发锁机制-深入理解synchronized(一)
这一篇文章主要介绍synchronized的使用和其底层原理!我将会由浅入深带大家学习synchronized!
synchronized 基础篇(使用)
一、Java共享内存模型带来的线程安全问题
思考: 两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
1. 代码示例
public class SyncDemo {
private static int counter = 0;
public static void increment() {
counter++;
}
public static void decrement() {
counter--;
}
public static void main(String[] args) throws InterruptedException {
// 线程一
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
}, "t1");
// 线程二
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, "t2");
t1.start();
t2.start();
// main线程 要等到t1、t2线程 运行结束后才会终止
t1.join();
t2.join();
System.out.println("counter = " + counter);
}
}
2. 运行结果
以上的结果可能是正数、负数、零(但是为0,基本是不可能出现的!)
3. 问题分析
Java 中对静态变量的自增,自减并不是原子操作。
我们可以查看 i++和 i--(i 为静态变量)的 JVM 字节码指令
i++的JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增
i--的JVM 字节码指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
isub // 自减
如果单线程情况下,这些JVM指令顺序执行,(不交错)那肯定是没有问题的。
但是,在多线程下,可能会交错运行!
4. 临界区(Critical Section)
一个程序运行多个线程本身是没有问题的!问题出在多个线程访问共享资源
多个线程(只)读共享资源其实也没有问题
在多个线程对共享资源读写操作时发生指令交错,就会出现问题!!!
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
例如,我们上述的代码中的如下部分:
//临界资源
private static int counter = 0;
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter--;
}
5. 竞态条件(Race Condition)
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
为了避免临界区的竞态条件发生,有多种手段可以达到目的:
- 阻塞式的解决方案:synchronized(本章节重点),Lock
- 非阻塞式的解决方案:原子变量(Atomic原子类,CAS)
需要注意的是,在Java中同步和互斥的场景都是使用synchronized实现的!但它们是有区别的!
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
二、synchronized 的使用
synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。
大致是有 5种用法!
1. 同步普通(实例)方法
public synchronized void method() {
...
}
2. 同步静态方法
public static synchronized void method() {
...
}
3. 同步this实例对象
synchronized(this) {
...
}
4. 同步类对象
synchronized(synchronizedDemo.class){
...
}
5. 同步对象实例
String lock = "";
synchronized(lock){
...
}
解决之前的共享问题
方法一:
private static int counter = 0;
public static synchronized void increment() {
counter++;
}
public static synchronized void decrement() {
counter--;
}
方法二:
private static int counter = 0;
private static String lock = "";
public static void increment() {
synchronized (lock) {
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
synchronized 实际是用对象锁保证了临界区内代码的原子性
遗留问题(高级篇种解释)
同步实例对象和同步类对象有什么区别?
这5种方式在性能上面是否有差别?