javase多线程之线程安全

目录

1.Volatile关键字的作用

修饰基本类型的变量保护内存可见性问题

volatile修饰double/long类型变量保证JVM操作的原子性

volatile修饰实例化对象禁止重排序

2.饿汉模式和懒汉模式(StravingMode 、LazyMode)

饿汉模式(StarvingMode)

懒汉模式(LazyMode)

二次判断

3.阻塞队列(BlockingQueue)

wait-notify机制

阻塞队列使用wait-notify机制

实现BlockingQueue


1.Volatile关键字的作用

volatile意为容易改变的,可以用来修饰基本类型的变量和引用类型的变量。

修饰基本类型的变量保护内存可见性问题

volatile修饰的变量表示易变的,禁止了jvm的优化,所有要用到的场景,都必须从主内存中直接读取而不能从工作内存中读取。当发生变化时也是第一时间写回主内存而不是工作内存。

import java.util.concurrent.TimeUnit;

public class Main {
//quit没有被volatile修饰时,线程会一直休眠下去
 // static boolean quit = false;

    volatile static boolean quit = false;

    static class MyThread extends Thread {
        @Override
        public void run() {
            int r = 0;
            while (quit == false) { // 线程自己的工作内存
                r++;
            }
            System.out.println(r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(5);

        quit = true;
    }
}

volatile修饰double/long类型变量保证JVM操作的原子性

JVM默认的操作长度是32位,对于长度小于32位的byte,char,short,int,float,boolean类型不会有原子性被破坏的问题。JVM还保证引用类型的操作不会有原子性问题。但是长度为64位的double和long类型变量在JVM操作时是高32+低32位操作的,因此我们可以用volatile修饰double/long类型变量保证原子性。

volatile修饰实例化对象禁止重排序

实例化一个对象的过程:

1:在堆中申请一片内存空间并全部初始化为0x0

2:初始化对象(构造代码块、属性定义时的初始化、执行构造方法)

3:引用赋值

但是3过程几乎是瞬时的,2过程会消耗一定时间。因此JVM会对此进行优化重排序为1 -> 3 -> 2。这在单线程下没有问题。在多线程下有可能在引用赋值之后还没有完成初始化对象的全部工作,另一个线程插入进来用到了这个引用。

我们用volatile修饰这个引用,确保执行顺序为严格的1 -> 2 -> 3,禁止重排序。 

2.饿汉模式和懒汉模式(StravingMode 、LazyMode)

饿汉模式和懒汉模式都是一种单例模式(Singleton pattern)。一个类只有一个实例化对象,通过代码保护不允许出现第二个对象,不能在别的类中使用构造方法。一般将对象作为静态属性放在一个类中,使用getInstance()方法获取对象。

饿汉模式(StarvingMode)

在类加载时就实例化对象。JVM保证了类的加载时线程安全的,所以饿汉模式是线程安全的

public class StarvingMode {
    // 希望这个类有且仅有一个对象
    private StarvingMode() {}

    // JVM 保证了类加载过程是线程安全的
    // 所以饿汉天生就是线程安全的
    private static final StarvingMode instance = new StarvingMode();  // 类被加载的时候执行

    public static StarvingMode getInstance() {
        return instance;
    }
}

懒汉模式(LazyMode)

在第一次被使用到时实例化对象。由于存在对instance的写操作,且instance是共享数据,因此会有线程安全问题,需要用锁来保护。

public class LazyModeV1 {
    private static LazyModeV1 instance = null;

    public synchronized static LazyModeV1 getInstance() {
        if (instance == null) {
            instance = new LazyModeV1();
        }

        return instance;
    }

    private LazyModeV1() {
    }
}

二次判断

我们注意到,只有instance == null 时才会写入instance 因此,我们不必对整个函数上锁,只用对instance == null 这一种情况上锁。这样可以提高性能。

public class LazyModeV2 {
//  实例化LazyModeV2时有可能重排序导致返回一个错误的引用,需用volatile修饰
    private static volatile LazyModeV2 instance = null;

    public static LazyModeV2 getInstance() {
        if (instance == null) {
            // A\B\C
            synchronized (LazyModeV2.class) {   // 只有 instance 为 null,才有必要用锁保护
                if (instance == null) { // 这里有没有线程可能遇到 instance 不为 null 的情况
                    instance = new LazyModeV2();    // 1->3->2
                }
            }
        }

        return instance;
    }

    private LazyModeV2() {
    }
}

由于有可能多个线程同时进入了第一个 instance == null 判断,他们都会互不冲突地继续执行。例如a和b线程同时进入了,a线程加锁先进行了实例化对象,instance 就不为空了。接着b线程加锁开始实例化对象,但是此时instance 已经不为空了,我们需要用到二次判断 instance == null。

3.阻塞队列(BlockingQueue)

阻塞队列是一种生产者-消费者模式。BlockingQueue是一个接口,直接继承自Queue<E>()接口。主要实现类有ArrayBlockingQueue、LinkedBlockingQueue 和 PriorityBlockingQueue。

wait-notify机制

wait() 和 notify() 是Object 类下的 final 方法。每个对象都有而且不允许子类重写。实现线程之间的协调工作(同步)。

public final void wait() throws InterruptedException {
        wait(0);
    }

//随机唤醒一个等待中的线程
public final native void notify();

//唤醒所有等待中的线程
public final native void notifyAll();

使用之前必须都加锁。

wait-notify机制是无状态的,只能先 wait 后 notify。反之不会有任何效果,是无意义的。

wait醒来的情况:1.被notify唤醒 2.线程被终止 3.超时 4.假唤醒

阻塞队列使用wait-notify机制

当队列为空时,消费者线程阻塞进入wait,等待一个生产者放入队列唤醒notify消费者。

当队列满时,生产者队列进入阻塞wait,等待一个消费者取出队列唤醒生产者。

实现BlockingQueue

public class MyArrayBlockingQueue {
    private long[] array;
    private int frontIndex;
    private int rearIndex;
    private int size;

    public MyArrayBlockingQueue(int capacity) {
        array = new long[capacity];
        frontIndex = 0;
        rearIndex = 0;
        size = 0;
    }

    // put 放入
    // take 取

    // 只有 P 会调用 put
    public synchronized void put(long e) throws InterruptedException {
        // 是不是满了
        while (size == array.length) {
            wait();// 作为 P,在等 C
        }

        // 队列不是满的
        array[rearIndex] = e;
        rearIndex++;
        if (rearIndex == array.length) {
            rearIndex = 0;
        }
        size++;

        notifyAll();   // P -> C
    }

    public synchronized long take() throws InterruptedException {
        while (size == 0) {
            wait(); // 作为 C,在等 P
        }

        // 队列一定不是空的
        long e = array[frontIndex];
        frontIndex++;
        if (frontIndex == array.length) {
            frontIndex = 0;
        }
        size--;

        notifyAll();   // C -> P

        return e;
    }
}

测试:

import java.util.Scanner;

public class Demo1 {
    static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            long e = scanner.nextLong();
            try {
                System.out.println("准备放入: " + e);
                queue.put(e);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        long take = queue.take();
        System.out.println(take);
    }
}

public class Demo2 {
    static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner scanner = new Scanner(System.in);
            scanner.nextLine();

            try {
                System.out.println("准备取走一个元素");
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        queue.put(1);
        queue.put(2);
        queue.put(3);
        System.out.println("3 被放入");
        queue.put(4);   // 阻塞
        System.out.println("4 被放入");
    }
}

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值