【并发编程】线程安全性问题

简介: 【并发编程】线程安全性问题

1.什么是线程安全性

  • 当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。
  • 线程不安全的原因
  • 线程是抢先执行的
  • 原子性操作,当CPU执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束。
  • 多个线程尝试修改同一个变量。
  • 内存可变性
  • 指令重排

2.原子性操作

  • 一个操作或这多个操作,要么全部执行并且执行过程中不被任何因素打断,要么就都不执行。
  • 如何把非原子性操作变成原子性
  • volatile关键字仅仅保证可见性,并不保证原子性,synchronized关键字使得操作具有原子性。

3.深入理解synchronized

(1)内置锁

  • 每个java对象都可以做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或者方法的时候会自动获得锁,在退出同步代码块或者方法的时候释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或者方法。
  • (2)互斥锁
  • 内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

(3)synchronized修饰普通方法

  • synchronzied修饰普通方法锁住的是当前调用的对象,假如开两个线程两个实例去掉方法,那么两个实例各自持有一个锁,互相不干扰,但是如果是两个线程用同一实例去调用,那么就持有一个锁,第一个线程释放锁后,第二个才会拿到锁。
public class SyncDemo {
  public synchronized void exampleOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();
    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();
    new Thread(() -> {
        syncDemo2.exampleOut();
    }).start();
}

519dd3fca2704402b5ad4585c6f50bdd.jpg

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();
    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();
    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();
}


c5fb9a1769904df7b56b78dfea51292e.jpg(4)synchronized修饰静态方法

  • synchronized修饰静态方法锁住的是整个类的对象,无论有多少个实例,只要是当前类的实例,都持有一个锁,一般生产不建议用静态同步方法,可能会导致程序运行阻塞。
public class SyncDemo {
    public static synchronized void staticOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();
    new Thread(() -> {
        syncDemo1.staticOut();
    }).start();
    new Thread(() -> {
        syncDemo2.staticOut();
    }).start();
}

c30a29c290374bfd9d13bf5f6cf6c252.jpg(5)synchronized修饰代码块

  • synchronized修饰代码块锁住的是当前对象,用法和synchronized修饰普通方法一样,但是更细粒度确定锁的位置,比synchronized修饰普通方法的效率要高。
public class SyncDemo {
  private Object lock = new Object();
    public void blockOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();
    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();
    new Thread(() -> {
        syncDemo2.blockOut();
    }).start();
}

05974801ac0e4b4cb238c723a6033f61.jpg

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();
    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();
    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();
}

033138fb1a8448cd9d0923cc0a507f44.jpg

3.4.volatile关键字

(1)volatile关键字的作用

  • 保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。

(2)为什么会出现脏读

  • Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。
  • 并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。

(3)volatile案例实战

public class VolatileDemo {
    private volatile boolean flag = false;
    public void work(){
        while (!flag) {
            System.out.println("线程开始工作");
        }
    }
    public void down(){
        flag = true;
        System.out.println("线程停止工作");
    }
    public static void main(String[] args) {
        VolatileDemo work = new VolatileDemo();
        new Thread(work::work).start();
        new Thread(work::work).start();
        new Thread(work::down).start();
        new Thread(work::work).start();
        new Thread(work::work).start();
    }
}
  • 不加volatile关键字的运行结果

302841af662c41dd9d092ecdb696eb48.jpg

  • 加volatile关键字的运行结果

bf7256d5d9654276a2b16e3716c794ad.jpg

(4)volatile只能保证变量的可见性,不能保证对volatile变量操作的原子性

  • 案例实战,分别对num进行+1000,+2000,+3000的操作
public class VolatileDemo {
    private volatile int num = 0;
    //执行方法加上synchronized
    public synchronized void addNum(){
        num++;
    }
  //执行方法加上synchronized
    public synchronized int getNum(){
        return num;
    }
    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();
        new Thread(()->{
            for (int i = 0; i < 2000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();
        new Thread(()->{
            for (int i = 0; i < 3000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();
    }
}

6de39b9502f0472aa2c9628b65cfc13f.jpg

  • 加上synchronized之后

32a935a369ca4fcba4c8a1423011381b.jpg

3.5.happens-before规则

(1)理解happens-before

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作执行顺序排在第二个操作之前。

两个操作之间存在happens-before关系,并不意味着java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。

如果操作A happens-before操作B,那么操作A在内存上所做的所有操作对于操作B都是可见的,不管它们在不在同一个线程。

happens-before关系保证正确同步的多线程程序执行的结果不被重排序改变。

(2)happens-before六大规则

  • 前三个规则用这个例子来看
class VolatileExample{
    int a=0;
    volatile boolean flag=false;
    public void writer(){
        a=1;                                  // 操作1
        flag=true;                            // 操作2
    }
    public void reader(){
        if(flag){                             // 操作3
            int i=a;                          // 操作4
            //这里i会是多少呢?
        }
    }
}

程序顺序规则

一个线程中的每一个操作,happens-before于该线程中的任意后续操作可见。

程序前面对某个变量的修改一定是对后续操作可见的。

例如上面代码块,按照程序顺序执行规则,1 happens-before 2,3 happens-before 4。

volatile变量规则

  • 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 例如上面代码块,按照volatile变量规则,2 happens-before 3。
  • 传递性规则
  • 如果A appens-before B,B happens-before C,那么A happens-before C。
  • 例如上面代码块,按照传递性规则,1 happens-before 4。
  • 管程中锁的规则
  • 对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • 管程是一种通用的同步原语,在java中指的就是synchronized,synchronized是java里对管程的实现。
synchronized(this){
    if(this.x < 12){
        this.x = 12;
    }
}
//根据管程中锁的规则,线程A执行完成后x会变成12,执行完释放锁,线程B进入代码块的时候,能够看到线程A对x的操作,也就睡说B看到的x值为12。
  • 线程start规则
  • 父线程A启动后,启动子线程B,子线程B能够看到主线程在启动B前的所有操作(指共享变量的操作)。
public class StartDemo {
    private static int num = 0;
    public static void main(String[] args) {
        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                System.out.println("B线程中读取num:"+num); //操作2
            });
            num = 1;   //操作1
            B.start();
        });
        A.start();
    }
}
//根据线程start规则,1 happens-before 2,线程A对共享变量a=1的操作对于线程B是可见的。

efe742693dec4a24a4dab4ba8e8e13f4.jpg

  • 线程join规则
  • 父线程A等待子线程B完成, 当子线程B完成后 ,父线程A能够看到子线程B的操作(指的是对共享变量的操作)。
public class JoinDemo {
    private static int num = 0;
    public static void main(String[] args) {
        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                num = 2;
            });
            num = 1;
            B.start();
            try {
                B.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A线程中读取num:"+num);
        });
        A.start();
    }
}

6287becbf789411186de7e66572eb525.jpg

3.6.如何避免线程安全性问题

(1)线程安全性问题成因

  • 多线程环境
  • 多个线程操作同意共享资源
  • 对该共享资源进行了非原子性操作

(2)如何避免线程安全性问题

  • 多线程环境–将多线程改为单线程(加锁)
  • 多个线程操作同一共享资源–让其资源不进行共享(ThreadLocal、资源不可变、操作无状态化)
  • 对该共享资源进行了非原子性操作–将非原子性的操作改成原子性的操作(加锁)


相关文章
|
11天前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
51 11
|
5月前
|
Java 程序员 调度
【JAVA 并发秘籍】进程、线程、协程:揭秘并发编程的终极武器!
【8月更文挑战第25天】本文以问答形式深入探讨了并发编程中的核心概念——进程、线程与协程,并详细介绍了它们在Java中的应用。文章不仅解释了每个概念的基本原理及其差异,还提供了实用的示例代码,帮助读者理解如何在Java环境中实现这些并发机制。无论你是希望提高编程技能的专业开发者,还是准备技术面试的求职者,都能从本文获得有价值的见解。
90 1
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
257 6
|
5月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
102 0
|
2月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
2月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
150 0
|
3月前
|
数据挖掘 程序员 调度
探索Python的并发编程:线程与进程的实战应用
【10月更文挑战第4天】 本文深入探讨了Python中实现并发编程的两种主要方式——线程和进程,通过对比分析它们的特点、适用场景以及在实际编程中的应用,为读者提供清晰的指导。同时,文章还介绍了一些高级并发模型如协程,并给出了性能优化的建议。
51 3
|
4月前
|
负载均衡 Java 调度
探索Python的并发编程:线程与进程的比较与应用
本文旨在深入探讨Python中的并发编程,重点比较线程与进程的异同、适用场景及实现方法。通过分析GIL对线程并发的影响,以及进程间通信的成本,我们将揭示何时选择线程或进程更为合理。同时,文章将提供实用的代码示例,帮助读者更好地理解并运用这些概念,以提升多任务处理的效率和性能。
78 3
|
4月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
5月前
|
数据采集 Java Python
Python并发编程:多线程(threading模块)
Python是一门强大的编程语言,提供了多种并发编程方式,其中多线程是非常重要的一种。本文将详细介绍Python的threading模块,包括其基本用法、线程同步、线程池等,最后附上一个综合详细的例子并输出运行结果。