JavaJava 杂谈Java学习之路

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

2019-08-05  本文已影响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

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算法这类高效安全的算法。
上一篇 下一篇

猜你喜欢

热点阅读