并发编程 synchronized (一) 初识

目录

上下文切换对非原子操作的影响

示例

原因

 详细分析

临界区 Critical Section

竞态条件 Race Condition

通过 synchronized 解决

思考

面向对象改进


上下文切换对非原子操作的影响

示例

我们都知道,两个线程 Thread0 Thread1,对同一个量 count 进行操作;一个让其自增,一个让其自减,次数相等;结果可能为 负数、正数、0(极少)

示例代码:

package com.juc.ppy.synchronize;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.UnsafeCount")
public class UnsafeCount {
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                count--;
            }
        }, "t2");

        // 启动线程 t1 t2
        t1.start();
        t2.start();
        // 主线程等待 t1 t2 执行完毕
        t1.join();
        t2.join();
        log.debug("最终结果{}", count);
    }
}

原因

上下文:存储器程序计数器中线程执行的进度信息

上下文切换:  ①Thread0 时间片用完,操作系统保存 Thread0 上下文到内存,Thread0 暂停运行,让出处理器使用权(切出)

②加载 Thread1 的上下文,Thread1 获得处理器使用权,开始或继续运行(切入)......

③Thread0 再次获取处理器使用权,继续执行

原子性:一个或一系列操作不会被线程调度(上下文切换)所打断

综上:因为静态变量 count 自增自减操作是非原子性的 + 并发情况下,线程上下文切换;导致结果出现负数与正数

 详细分析

count++ 看似只有一步操作,其实从字节码看,有四步操作

① getstatic        i // 取出静态变量 i 的值

② iconst_1 // 准备常量 1

③ iadd // 自增

④ putstatic        i // 将修改后的值存入静态变量 i

可能在 Thread0 中 count 自增之后的结果还没有重新存入 count 时,时间片用完了,轮到Thread1 使用处理器;Thread1 获取到的值还是旧值

临界区 Critical Section

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

count++ 与 count-- 既有读又有写 

  1. 线程运行多个线程是没有问题的
  2. 多个线程共享资源也没有问题
  3. 当多个线程对共享资源进行读写操作时,线程切换(指令交错),就会出问题

竞态条件 Race Condition

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

通过 synchronized 解决

package com.juc.ppy.synchronize;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.UnsafeCount")
public class UnsafeCount {
    static int count = 0;
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                synchronized(lock){
                    count++;
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                synchronized(lock){
                    count--;
                }
            }
        }, "t2");

        // 启动线程 t1 t2
        t1.start();
        t2.start();
        // 主线程等待 t1 t2 执行完毕
        t1.join();
        t2.join();
        log.debug("最终结果{}", count);
    }
}

为了避免临界区的竞态条件发生(避免在线程对共享资源进行读写时,发生线程切换对结果造成影响,即其他线程也可以读写);此处使用 synchronized 保证了临界区代码的原子性

synchronized:俗称对象锁,采用互斥方式;同一时刻,只能有一个线程拥有锁(Thread0);其他线程想获取该锁,就会被阻塞(Thread1);保证 Thread0 能正确执行完临界区代码(不代表不会发生上下文切换)

即使时间片用完,切换成 Thread1;由于 ①Thread0 还没有执行完临界区代码②Thread1 无法获取锁,就无法执行临界区代码③所以影响不到Thread0

 

 

思考

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

        结果还是0,但是当 t1 获得锁之后,只有当循环结束了才会释放锁   

Thread t1 = new Thread(() -> {
            synchronized(lock){ // 锁放在循环外面
                for(int i = 0; i < 100; i++){
                    count++;
                    log.debug("最终结果{}", count);
                }
            }
        }, "t1");

Thread t2 = new Thread(() -> {
            synchronized(lock){ // 锁放在循环外面
                for(int i = 0; i < 100; i++){
                    count--;
                    log.debug("最终结果{}", count);
                }
            }
        }, "t2");

2. 如果 t1 synchronized(lock1) 而 t2 synchronized(lock2) 会怎样运作?-- 锁对象

        对象不同,相当于不同的临界区,它们互不影响;最终结果与不加锁一样

Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                synchronized(lock1){ // 不同锁,相当于 t1 进入的是临界区 1
                    count++;
                }
            }
        }, "t1");

Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                synchronized(lock2){ // 不同锁,相当于 t2 进入的是临界区 2
                    count--;
                }
            }
        }, "t2");

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

        相当于 t2 不受临界区限制,t2 可以随意进入相对于 t1 的临界区;结果与不加锁一样

Thread t1 = new Thread(() -> { // 这个临界区只是针对于 t1
            for(int i = 0; i < 50000; i++){
                synchronized(lock){
                    count++;
                }
            }
        }, "t1");

Thread t2 = new Thread(() -> { // 这个所谓的"临界区"对 t2 来说相当于没有
            for(int i = 0; i < 50000; i++){
                count--;
            }
        }, "t2");

线程共用哪个资源,就在哪个位置加锁;细粒度的锁,对性能影响更小(大部分情况)。

面向对象改进

package com.juc.ppy.synchronize;

import lombok.extern.slf4j.Slf4j;

public class CriticalSection {
    private int count = 0;

    public void increase(){
        synchronized(this){
            count++;
        }
    }

    public void decrease(){
        synchronized(this){
            count--;
        }
    }

    public int get(){
        synchronized(this){
            return count;
        }
    }
}

@Slf4j(topic = "c.Test")
class Test{
    public static void main(String[] args) throws InterruptedException {
        CriticalSection cs = new CriticalSection();

        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                cs.increase();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++){
                cs.decrease();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("最终结果为 {}", cs.get());
    }
}

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值