并发编程-05线程安全性之原子性【锁之synchronized】

简介: 并发编程-05线程安全性之原子性【锁之synchronized】

脑图

20190216205207715.png

概述


举个例子:

【多线程场景】假设有个变量a在主内存中的初始值为1,线程A和线程B同时从主内存中获取到了a的值,线程A更新a+1,线程B也更新a+1,经过线程AB更新之后可能a不等于3,而是等于2。因为A和B线程在更新变量a的时候从主内存中拿到的a都是1,而不是等A更新完刷新到主内存后,线程B再从主内存中取a的值去更新a,所以这就是线程不安全的更新操作.


解决办法


使用锁 1. 使用synchronized关键字synchronized会保证同一时刻只有一个线程去更新变量. 2、Lock接口 【篇幅原因先不讨论lock,另开篇介绍】。

使用JDK1.5开始提供的java.util.concurrent.atomic包,见 并发编程-04线程安全性之原子性Atomic包详解


先简单说下synchronized和lock

  • synchronized 依赖jvm
  • lock 依赖特殊的cpu指令,代码实现,比如ReentranLock

这里我们重点来看下synchronized关键字是如何确保线程安全的原子性的。


原子性synchronized 修饰的4种对象


  • 修饰代码块
  • 修饰方法
  • 修饰静态方法
  • 修饰类


修饰代码块

作用范围及作用对象


被修饰的代码被称为同步语句块,作用范围为大括号括起来的代码,作用于调用的对象, 如果是不同的对象,则互不影响


Demo

多线程下 同一对象的调用

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedDemo {
  public void test() {
    // 修饰代码块 ,谁调用该方法synchronized就对谁起作用   即作用于调用的对象 。 如果是不同的对象,则互不影响
    synchronized (this) {
      for (int i = 0; i < 10; i++) {
        log.info("修饰代码块 i = {} ",i);
      }
    }
  }
  public static void main(String[] args) {
    // 同一个调用对象
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    ExecutorService executorService = Executors.newCachedThreadPool();
    // 启动两个线程去 【使用同一个对象synchronizedDemo】调用test方法 
    for (int i = 0; i < 2; i++) {
      executorService.execute(() ->{
        synchronizedDemo.test();
      });
    }
 //     使用Thread  可以按照下面的方式写  
//    for (int i = 0; i < 2; i++) {
//      new Thread(()-> {
//        synchronizedDemo.test2();
//      }).start();
//    }
    // 最后 关闭线程池
    executorService.shutdown();
  }
}



我们先思考下执行的结果是什么样子的?


上述代码,我们通过线程池,通过循环开启了2个线程去调用含有同步代码块的test方法 , 我们知道 使用synchronized关键字修饰的代码块作用的对象是调用的对象(同一个对象)。 因此这里的两个线程都拥有同一个对象synchronizedDemo的引用,两个线程,我们命名为线程A 线程B。 当线程A调用到了test方法,因为有synchronized关键字的存在,线程B只能等待线程A执行完。 因此A会输出0~9,线程A执行完之后,A释放锁,线程Bh获取到锁后,继续执行。


实际执行结果:


20190217195008998.png

符合我们分析和预测。

如果我们把 test 方法的synchronized关键字去掉会怎样呢? 来看下


20190217195854514.png


执行结果


20190217195926968.png


可知,synchronized关键字修饰的代码块,确保了同一调用对象在多线程的情况下的执行顺序


多线程下不同对象的调用

为了更好地区分,我们给调用方法加个参数

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedDemo {
  public void test(String flag) {
    // 修饰代码块 ,谁调用该方法synchronized就对谁起作用 即作用于调用的对象 。 如果是不同的对象,则互不影响
    synchronized (this) {
      for (int i = 0; i < 10; i++) {
        log.info("{} 调用 修饰代码块 i = {} ",flag ,i);
      }
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    // 对象 synchronizedDemo
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    // 对象 synchronizedDemo2
    SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
    // synchronizedDemo 调用 test
    executorService.execute(()->{
      synchronizedDemo.test("synchronizedDemo");
    });
    // synchronizedDemo2 调用 test
    executorService.execute(()->{
      synchronizedDemo2.test("synchronizedDemo2");
    });
    // 最后 关闭线程池
    executorService.shutdown();
  }
}


先来猜测下执行结果呢?

两个不同的对象,调用test方法,应该是互不影响的,所以执行顺序是交替执行的。

运行结果:

20190217203141635.png


修饰方法


被修饰的方法称为同步方法,作用的范围是整个方法,作用于调用的对象, 如果是不同的对象,则互不影响


作用范围及作用对象

同 修饰代码块

Demo

增加个方法 test2

  // 修饰方法 谁调用该方法synchronized就对谁起作用 即作用于调用的对象 。 如果是不同的对象,则互不影响
  public synchronized void test2() {
    // 修饰代码块 ,
    for (int i = 0; i < 10; i++) {
      log.info("调用 修饰代码块 i = {} ",  i);
    }
  }



多线程下同一个对象的调用

同 修饰代码块


20190217205526291.png

结果:

20190217205457974.png


多线程下不同对象的调用

同 修饰代码块


20190217205242477.png

结果:


20190217205310799.png


通过上面的测试结论可以知道 修饰代码块和修饰方法

如果一个方法内部是一个完整的synchronized代码块,那么效果和synchronized修饰的方法效果是等同的 。


还有一点需要注意的是,如果父类的某个方法是synchronized修饰的,子类再调用该方法时,是不包含synchronized. 因为synchronized不属于方法声明的一部分。 如果子类想使用synchronized的话,需要在方法上显示的声明其方法为synchronized


修饰静态方法

作用范围及作用对象

整个静态方法, 作用于所有对象


Demo

多线程同一个对象的调用

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedStaticMethodDemo {
  // 修饰静态方法
  public synchronized static void test() {
    for (int i = 0; i < 10; i++) {
      log.info("调用 修饰方法 i = {} ", i);
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 2; i++) {
      executorService.execute(() ->{
        test();
      });
    }
    // 最后 关闭线程池
    executorService.shutdown();
  }
}


结果:


20190217211857392.png


多线程下不同对象的调用

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedStaticMethodDemo {
  // 修饰静态方法
  public synchronized static void test() {
    for (int i = 0; i < 10; i++) {
      log.info("调用 修饰方法 i = {} ", i);
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    SynchronizedStaticMethodDemo demo1 = new SynchronizedStaticMethodDemo();
    SynchronizedStaticMethodDemo demo2 = new SynchronizedStaticMethodDemo();
    // demo1调用
    executorService.execute(() ->{
      // 其实直接调用test方法即可,这里仅仅是为了演示不同对象调用 静态同步方法
      demo1.test();
    });
    // demo2调用
    executorService.execute(() ->{
      // 其实直接调用test方法即可,这里仅仅是为了演示不同对象调用 静态同步方法
      demo2.test();
    });
    // 最后 关闭线程池
    executorService.shutdown();
  }
}


结果:

20190217212015553.png


修饰类

作用范围及作用对象

修饰范围是synchronized括号括起来的部分,作用于所有对象


Demo

多线程下同一对象的调用

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedStaticClassDemo2 {
  // 修饰一个类
  public  void test() {
    synchronized (SynchronizedStaticClassDemo2.class) {
      for (int i = 0; i < 10; i++) {
        log.info("调用 修饰方法 i = {} ", i);
      }
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    SynchronizedStaticClassDemo2 demo = new SynchronizedStaticClassDemo2();
    // demo调用
    executorService.execute(() ->{
      demo.test();
    });
    // demo调用
    executorService.execute(() ->{
      demo.test();
    });
    // 最后 关闭线程池
    executorService.shutdown();
  }
}


结果

20190217212611944.png


多线程下不同对象的调用

package com.artisan.example.sync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SynchronizedStaticClassDemo2 {
  // 修饰一个类
  public  void test() {
    synchronized (SynchronizedStaticClassDemo2.class) {
      for (int i = 0; i < 10; i++) {
        log.info("调用 修饰方法 i = {} ", i);
      }
    }
  }
  public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    SynchronizedStaticClassDemo2 demo1 = new SynchronizedStaticClassDemo2();
    SynchronizedStaticClassDemo2 demo2 = new SynchronizedStaticClassDemo2();
    // demo1调用
    executorService.execute(() ->{
      demo1.test();
    });
    // demo2调用
    executorService.execute(() ->{
      demo2.test();
    });
    // 最后 关闭线程池
    executorService.shutdown();
  }
}


结果


20190217212454414.png


使用Synchronized来保证线程安全

先回顾下 线程不安全的写法


20190217212812871.png


方法一

下面用Synchronized来改造下

我们知道synchronized修饰静态方法,作用的对象是所有对象 , 因此 仅需要将 静态add方法 修改为同步静态方法即可。

20190217213258208.png


多次运算


20190217213312940.png

方法二


假设 add方法不是静态方法呢? 我们知道 当synchronized修饰普通方法,只要是同一个对象,也能保证其原子性

假设 add方法为普通方法

20190217213559604.png

改造如下:

20190217214032912.png

多次运行,结果总是10000


20190217214106569.png

原子性的实现方式小结


  • synchronized
    不可中断锁,适合不激烈的竞争,可读性较好
  • atomic包
    竞争激烈时能维持常态,比Lock性能好,但只能同步一个值
  • lock
    可中断锁,多样化同步,竞争激烈时能维持常态。后面针对lock单独展开。


代码


https://github.com/yangshangwei/ConcurrencyMaster

相关文章
|
10天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
34 3
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
155 6
|
8天前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
2月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
39 6
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
49 4
|
2月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
95 0
|
3月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
54 0
|
11天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
34 1