当前位置:网站首页>synchronized 解决共享带来的问题

synchronized 解决共享带来的问题

2022-07-06 08:10:00 兀坐晴窗独饮茶

共享带来的问题

小案例

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

package cn.knightzz.example;

import lombok.extern.slf4j.Slf4j;


@SuppressWarnings("all")
@Slf4j(topic = "c.TestShareValue")
public class TestShareValue {
    

    static int value = 0;

    public static void main(String[] args) throws InterruptedException {
    

        Thread t1 = new Thread(() -> {
    

            for (int i = 0; i < 5000; i++) {
    
                value++;
                log.debug("value + 1 = {}", value);
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
    

            for (int i = 0; i < 5000; i++) {
    
                value--;
                log.debug("value - 1 = {}", value);
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.debug("value : {} ", value);
    }
}

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理

解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

img

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

出现负数的情况 :

在这里插入图片描述

在这里插入图片描述

img

临界资源

  • 一个程序运行多个线程本身是没有问题的

    • 问题出在多个线程访问共享资源
  • 多个线程读共享资源其实也没有问题

    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    static int counter = 0;

    public void increment(){
    
        // 临界区 
        counter++;
    }

    public void decrement(){
    
        counter--;
    }

竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchronized 解决方案

互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的 :

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized对象锁 , 采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized(对象) // 线程1, 线程2(blocked)
{
    
    临界区
}

小案例

package cn.knightzz.test;

import lombok.extern.slf4j.Slf4j;

@SuppressWarnings("all")
@Slf4j(topic = "c.TestSynchronized")
public class TestSynchronized {
    

    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
    
        method2();
    }

    private static void method1() throws InterruptedException {
    

        Thread t1 = new Thread(() -> {
    
            for (int i = 0; i < 50000; i++) {
    
                log.debug("counter add : {} ", counter);
                counter++;
            }
            log.debug("t1 thread end ... ");
        }, "t1");

        Thread t2 = new Thread(() -> {
    
            for (int i = 0; i < 50000; i++) {
    
                log.debug("counter sub : {} ", counter);
                counter--;
            }
            log.debug("t2 thread end ... ");
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // 最终结果可能是 正数, 负数或者 0
        log.debug("method1 : counter = {}", counter);
        // 重置counter
        counter = 0;
    }

    private static void method2() throws InterruptedException {
    

        Thread t1 = new Thread(() -> {
    
            for (int i = 0; i < 500000; i++) {
    
                synchronized (TestSynchronized.class) {
    
                    // 使用静态类对象作为 锁
                    log.debug("counter add : {} ", counter);
                    counter++;
                }
            }
            log.debug("t1 thread end ... ");
        }, "t1");

        Thread t2 = new Thread(() -> {
    
            for (int i = 0; i < 500000; i++) {
    
                synchronized (TestSynchronized.class) {
    
                    log.debug("counter sub : {} ", counter);
                    counter--;
                }
            }
            log.debug("t2 thread end ... ");
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // 最终结果可能是 正数, 负数或者 0
        log.debug("method2 : counter = {}", counter);
        // 重置counter
        counter = 0;
    }
}

img

synchronized 可以被认为是一个房间的钥匙, 临界区可以被认为是一个房间

  • 当线程拿到锁以后, 会进入房间然后关上门, 此时其他线程无法进入临界区执行临界区的代码
  • 如上面的图, 线程2到了门口, 会发现门在关着, 并且线程2无法获取锁, 所以只能被阻塞, 等待线程1执行结束释放锁
  • 另外需要注意的是 : 并不是拿到锁就可以一直执行下去, CPU分配给线程1的时间片结束后, 线程1就会被踢出临界区, 然后门重新被锁住.
  • 此时线程仍然拿着锁, 等到线程1再次获得CPU时间片后, 执行完临界区的代码, 释放锁后, 线程2才可以获取锁进入临界区去执行临界区的代码

图解如下 :

img

原子性的理解

 for (int i = 0; i < 50000; i++) {
    
                synchronized (TestSynchronized.class) {
    
                    // 使用静态类对象作为 锁
                    log.debug("counter add : {} ", counter);
                    counter++;
                }
}

如上面的代码, 此时 synchronized 包裹的是 counter++; 这行代码 , 操作的是 counter 这个临界资源

counter++ 在jvm里面有四步指令 :

  • get static i 获取静态变量i
  • iconst 准备常量
  • iadd 自增
  • putstatic 将i的值写入到 静态变量

原子性就是要保证, 这四步是完整不可分割的整体, 在这四步执行完以前, 任何线程都无法拿到锁 , 一旦这四步执行结束, 那么锁就会被释放, 所以 t1线程和t2线程可以交替混合打印

思考与总结

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切

换所打断

思考下面的问题 : ?

  1. 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
  2. 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
  3. 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

问题1 :

  1. 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性

img

上图需要注意的是 : 当一次counter++ 或者counter--执行结束以后, 对象锁就会被释放, 所以才会出现两个线程交替打印, 如果放在for循环外的话, 那么for循环就是一个整体, 具有原子性, 在执行结束前, 别的线程拿不到锁, 所以 此时是先后打印, 打印完线程1以后, 才会打印线程2

我们可以把存放临界区代码的地方看做一个房子, 这个房子只有一道门, 房子里面存放不同的临界区代码资源

// t1线程
synchronized (TestSynchronized.class) {
    
                    // 使用静态类对象作为 锁
                    log.debug("counter add : {} ", counter);
                    counter++;
                }

// t2 线程
 synchronized (TestSynchronized.class) {
    
                    log.debug("counter sub : {} ", counter);
                    counter--;
                }

可以看到上面的代码, 使用的都是 TestSynchronized.class 这个类对象, TestSynchronized.class 是锁对象, 这个锁对象对应着一个房间(临界区) , 房间内存放着synchronized 包裹的临界区代码, 每当对应线程想要执行对应的代码的时候, 都要先获取锁.

回到问题 , 如果synchronizedfor循环外包裹, 则

  • for循环是临界代码, 当线程获取锁后, 会直接把for循环执行完, 锁才会释放
  • 整个for循环会被视为一个整体, 单个线程的for循环执行结束之前, 是不会受到其他线程的干扰
  • 直观可以看到的就是 程序会先打印t1或者t2的输出, 等到t1或者t2执行完以后, 才会执行另外的线程的代码

假如是包裹在counter++ 上 :

 synchronized (TestSynchronized.class) {
    
                    log.debug("counter sub : {} ", counter);
                    counter--;
}


getstatic i // 获取i变量
iconst_1 // 准备常量1
iadd // i 自增1
putstatic i // 将1写入到i

因为加了 synchronized , 上面的四行JVM指令是一个整体 , 不存在这四行指令尚未执行完毕的时候被其他线程干扰, 比如 : 执行到**iadd**后 准备执行**putstatic i**时线程上下文切换, 指令中断! , 这四个指令执行完以前, 不会被任何线程打断

img

可以看到 t1 和 t2 实际上是混合交替运行的

如果是包裹在 for 循环外的话 :

 synchronized (TestSynchronized.class) {
    
                for (int i = 0; i < 5000; i++) {
    

                    // 使用静态类对象作为 锁
                    log.debug("counter add : {} ", counter);
                    counter++;
                }
            }

如上面的代码, 相当于 for 循环被视为一个整体, 具有原子性,那么, 在for循环执行结束前, 不会受到其他线程的干扰

img

直观的感受, t1执行完毕以后, t2才获得锁对象开始执行

问题2 :

  1. 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象

如果两个锁对象不同, 但是操作的同一个临界资源的话, 就相当于没有锁不会产生互斥 :

img

如上图所示 : 相当于有两个房间, 因为锁不同, 一把锁可以理解对应一个房间 , 所以当线程1获取 testQuestion1的锁时, 并不会和线程2出现互斥, 不影响线程2获取 testQuestion2 锁, 所以最终结果就和都不加锁是一样的

img

也可以理解为另外开了一个门, 在同一时间, 两个线程都可以进入房间去修改临界资源

问题3

  1. 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

如果一个加锁, 另外一个不加, 也相当于都没加, 因为 t2线程可以随意的执行, 不需要获得锁就可以执行去修改临界资源的值

思考

其实如果想要保证原子性 , 就必须互斥 , 要保证, 所有的临界区代码都在一个房间, 也就是说要都使用同一把锁才可以

img

否则, 只要有一个临界区不在这个房间, 那么就无法保证原子性, 因为这个临界区的代码执行不受限制, 可以随意修改临界资源的

面向对象改进

把需要保护的共享变量放入一个类

package cn.knightzz.improve;

import lombok.extern.slf4j.Slf4j;

@SuppressWarnings("all")
@Slf4j(topic = "c.Room")
public class Room {
    

    private int counter = 0;

    public void increment() {
    
        // 使用当前对象作为锁
        synchronized(this) {
    
            log.debug("increment counter : {} " , counter);
            counter++;
        }
    }
    public void decrement() {
    
        // 使用当前对象作为锁
        synchronized(this) {
    
            log.debug("decrement counter : {} " , counter);
            counter--;
        }
    }

    public int getCounter() {
    
        return counter;
    }
}

然后使用时直接调用 Room 提供的方法即可

package cn.knightzz.improve;

import cn.knightzz.test.TestSynchronized;
import lombok.extern.slf4j.Slf4j;

/** * @author 王天赐 * @title: TestObjectImprove * @projectName hm-juc-codes * @description: 面向对象改进 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-07-03 21:09 */
@SuppressWarnings("all")
@Slf4j(topic = "c.Room")
public class TestObjectImprove {
    

    public static void main(String[] args) throws InterruptedException {
    

        Room room = new Room();

        Thread t1 = new Thread(() -> {
    
            // 在 for 循环执行的过程中, 不会受到其他线程的干扰
            for (int i = 0; i < 5000; i++) {
    
                room.increment();
            }
            log.debug("t1 thread end ... ");
        }, "t1");

        Thread t2 = new Thread(() -> {
    
            // 在 for 循环执行的过程中, 不会受到其他线程的干扰
            for (int i = 0; i < 5000; i++) {
    
                room.decrement();
            }
            log.debug("t2 thread end ... ");
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.debug("counter : {} ... ", room.getCounter());
    }
}
原网站

版权声明
本文为[兀坐晴窗独饮茶]所创,转载请带上原文链接,感谢
https://blog.csdn.net/weixin_40040107/article/details/125589631