美文网首页JavaJava 杂谈Java学习之路
关于多线程访问并操作共享数据的解决方案概述

关于多线程访问并操作共享数据的解决方案概述

作者: 椰子奶糖 | 来源:发表于2019-08-05 09:40 被阅读22次

内存可见性

  • 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态 而另一个线程在同时修改该状态,需要确保当一个线程修改了对象 状态后,其他线程能够看到发生的状态变化。
  • 可见性错误是指当读操作与写操作在不同的线程中执行时,我们无 法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚 至是根本不可能的事情。


    image.png
  • 上图展现了当内存不可见是可能发生的问题

原子性:

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

synchronized同步代码块

  • 执行流程大约如下,它可以保证再同一时间只有一个线程操作数据,知道操作完成,别的线程才能够访问
image.png
  • 缺点:效率不高,由于线程访问的时候加了隐式的同步锁,导致其他线程无法访问,可能会发生线程阻塞。

volatile变量

  • 一种轻量级的同步机制,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。


    image.png
  • 在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
    也正因为没有加锁这个操作,volatile 不具备“互斥性",不能保证变量的“原子性”,关于原子性,上面提到过a++的问题,当一个操作不是原子操作的时候,volatile就无法保证其内存可见性是否是正确的(一个线程没操作完,另一个线程就直接操作,导致了不同步,存在着线程安全问题)

CAS算法

CAS(Compare-And-Swap) 算法保证数据变量的原子性
CAS 算法是硬件对于并发操作的支持
CAS 包含了三个操作数:
①内存值 V
②预估值 A
③更新值 B
当且仅当 V == A 时, V = B; 否则,不会执行任何操作。

image.png
  • 上图的什么都不做并不是什么都不做,而是进入下一次循环,重新执行一次操作,这样一来就不存在就没有同步锁,也就没有了线程阻塞的问题,线程不阻塞提高了CPU的利用率也就提升了速度(比synchronized快上不少)

Lock显示同步锁

  • 引用一道编程题:
 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。 *   如:ABCABCABC…… 依次递归 
package JUC;

import org.junit.Test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by CHEN on 2019/8/5.
 */
/*
 * 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。
 *  如:ABCABCABC…… 依次递归
 */
public class Lock {

    //普通写法
    @Test
    public void test01() {
        Demo demo = new Demo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    demo.LoopA(i);
                }
            }
        }, "ThreadA").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    demo.LoopB(i);
                }
            }
        }, "ThreadB").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    demo.LoopC(i);
                    System.out.println("-----------------------------");
                }
            }
        }, "ThreadC").start();
    }

    //Lambda表达式
    @Test
    public void test02() {

        Demo demo = new Demo();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++)
                demo.LoopA(i);
        },"ThreadAWriteByLambda").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++)
                demo.LoopB(i);
        },"ThreadAWriteByLambda").start();
        new Thread(() -> {
            for (int i = 1; i <= 10; i++){
                demo.LoopC(i);
                System.out.println("--------------------------");
            }
        },"ThreadAWriteByLambda").start();
    }
}

class Demo {

    private int number = 1;
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void LoopA(int totalLoop) {

        //上锁
        lock.lock();
        try {

            if (number != 1) {
                //等待
                condition1.await();
            }
            //打印
            System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);

            //唤醒
            number = 2;
            condition2.signal();
        } catch (Exception e) {

        } finally {
            //开锁
            lock.unlock();
        }
    }

    public void LoopB(int totalLoop) {

        //上锁
        lock.lock();
        try {

            if (number != 2) {
                //等待
                condition2.await();
            }
            //打印
            System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);

            //唤醒
            number = 3;
            condition3.signal();
        } catch (Exception e) {

        } finally {
            //开锁
            lock.unlock();
        }
    }

    public void LoopC(int totalLoop) {

        //上锁
        lock.lock();
        try {
            if (number != 3) {
                //等待
                condition3.await();
            }
            //打印
            System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);

            //唤醒
            number = 1;
            condition1.signal();
        } catch (Exception e) {

        } finally {
            //开锁
            lock.unlock();
        }
    }
}
  • 在主类中我写了两中写法,主要是为了练练Lambda表达式
  • 这道题的实现思路很简单:顺序打印,线程又是同时执行的,因此需要加一个标记(number),来标记那个线程可以打印了,否则等待,打印完了唤醒下一个线程(唤醒等待机制)
  • 最后重点是,这里用了Lock显式锁,观察代码可以发现其实我们可以在任何地方加锁(只要最后能打开),这种方法比较灵活,可以由我们决定的空间比较大。
  • 去掉锁是必要的,因此加在finally这类必须执行的代码块中。
写在最后,在Java8中开始大量的去锁化,而采用CAS算法这类高效安全的算法。

相关文章

  • 关于多线程访问并操作共享数据的解决方案概述

    内存可见性 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态 而另一个线程在同时修改...

  • 线程封闭

    概述 多线程访问共享数据时,涉及到线程间数据同步问题。当场景不需要用到数据共享时,提出了线程封闭的概念,数据封闭在...

  • 关于CoreData和SQLite多线程访问时的线程安全问题

    关于CoreData和SQLite多线程访问时的线程安全问题 数据库读取操作一般都是多线程访问的。在对数据进行读取...

  • java并发之线程封闭

      JVM运行时数据区分为线程共享部分、线程独占部分。多线程程访问共享可变数据时,涉及到线程间数据同步的问题。但并...

  • 要点提炼| 理解JVM之内存模型&线程

    本篇将介绍虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。 概述 Java内存模型...

  • Linux内核同步介绍

    1 临界区指的是访问和操作共享数据的代码段。多线程访问同一个资源是不安全的,因此要避免在临界区中并发访问,做到这点...

  • 2020-09-15 Java线程安全

    一、线程安全简介 单线程程序不会产生线程安全问题。多线程程序没有访问共享数据,也不会产生问题。多线程程序访问共享数...

  • 五 异常与多线程——第三节 线程同步机制

    1、线程安全问题的概述 多线程访问了共享的数据,就会产生线程安全问题 2、线程安全问题的代码实现 输出:会出现重复...

  • 线程安全

    概述 多线程环境下用于保证共享的、可修改的数据的正确性. 特性 原子性当前线程对数据的操作,不允许被其他线程干扰。...

  • 同步函数需要注意的事项

    1,明确哪些代码是多线程运行代码。2,明确共享数据。3,明确多线程运行代码中哪些语句是操作共享数据的。 同步函数需...

网友评论

    本文标题:关于多线程访问并操作共享数据的解决方案概述

    本文链接:https://www.haomeiwen.com/subject/oirpdctx.html