Java并发:五种线程安全类型、线程安全的实现、枚举类型

495次阅读  |  发布于3年以前

1 . Java中的线程安全

① 不可变

如何实现不可变?

private final int value;

常见的不可变的类型:

注意:原子类 AtomicInteger 和 AtomicLong 则是可变的。

对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。

例如,如果获得的不可变map对象进行put()、remove()、clear()操作,则会抛出UnsupportedOperationException异常。

② 绝对线程安全

绝对线程安全的实现,通常需要付出很大的、甚至不切实际的代价。

Java API中提供的线程安全,大多数都不是绝对线程安全。

例如,对于数组集合Vector的操作,如get()、add()、remove()都是有synchronized关键字修饰。有时调用时也需要手动添加同步手段,保证多线程的安全。

下面的代码看似不需要同步,实际运行过程中会报错。

import java.util.Vector;

/**
 * @Author: lucy
 * @Version 1.0
 */
public class VectorTest {
    public static void main(String[] args) {
        Vector<Integer> vector = new Vector<>();
        while(true){
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        System.out.println("获取vector的第" + i + "个元素: " + vector.get(i));
                    }
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i=0;i<vector.size();i++){
                        System.out.println("删除vector中的第" + i+"个元素");
                        vector.remove(i);
                    }
                }
            }).start();
            while (Thread.activeCount()>20)
                return;
        }
    }
}

出现ArrayIndexOutOfBoundsException异常,原因:某个线程恰好删除了元素i,使得当前线程无法访问元素i。

Exception in thread "Thread-1109" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1
 at java.util.Vector.remove(Vector.java:831)
 at VectorTest$2.run(VectorTest.java:28)
 at java.lang.Thread.run(Thread.java:745)

需要将对元素的get和remove构造成同步代码块:

synchronized (vector){
    for (int i = 0; i < vector.size(); i++) {
        System.out.println("获取vector的第" + i + "个元素: " + vector.get(i));
    }
}
synchronized (vector){
    for (int i=0;i<vector.size();i++){
        System.out.println("删除vector中的第" + i+"个元素");
        vector.remove(i);
    }
}

③ 相对线程安全

④ 线程兼容

⑤ 线程对立

2 . Java的枚举类型

通过enum关键字修饰的数据类型,叫枚举类型。

public class EnumData {
    public static void main(String[] args) {
        for (Family family : Family.values()) {
            System.out.println(family.name() + ":" + family.ordinal());
        }
    }
}

enum Family {
    GRADMOTHER, GRANDFATHER, MOTHER, FATHER, DAUGHTER, SON;
}

可以将枚举类型看做普通的class,在里面定义final类型的成员变量,便可以为枚举类型中的元素赋初值。

要想获取枚举类型中元素实际值,需要为成员变量添加getter方法。

虽然枚举类型的元素有了自己的实际值,但是通过ordinal()方法获取的元素序号不会发生改变。

public class EnumData {
    public static void main(String[] args) {
        for (Family family : Family.values()) {
            System.out.println(family.name() + ":实际值" + family.getValue() +
                    ", 实际序号" + family.ordinal());
        }
    }
}
enum Family {
    GRADMOTHER(3), GRANDFATHER(4), MOTHER(1), FATHER(2), DAUGHTER(5), SON(6);
    private final int value;
    Family(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}

3 . Java线程安全的实现

① 互斥同步

互斥同步(Mutex Exclusion & Synchronization)是一种常见的并发正确性保障手段。

同步与互斥的关系:

Java中,最基本的实现互斥同步的手段是synchronized关键字,其次是JUC包中的ReentrantLock。

关于synchronized关键字:

关于ReentrantLock:

② 非阻塞同步

(1)CAS概述

互斥同步最大的性能问题是线程的阻塞和唤醒,因此又叫阻塞同步。

互斥同步采用悲观并发策略:

随着硬件指令集的发展,我们可以采用基于冲突检测的乐观并发策略:

乐观并发策略的许多实现都不需要将线程阻塞,这种同步操作叫做非阻塞同步。

非阻塞同步依靠的硬件指令集:前三条是比较久远的指令,后两条是现代处理器新增的。

什么是CAS?

原子操作:所谓的原子操作是指一个或一系列不可被中断的操作。

Java中的CAS操作:

除了偏向锁,Java中其他锁的实现方式都是用了循环的CAS操作。学习资料:[Java进阶视频资源]

(2)通过循环的CAS实现原子操作

通过++i或者i++可以实现计数器的自增,在多线程环境下,这样使用是非线程安全的。

public class UnsafeCount {
    private int i = 0;
    private static final int THREADS_COUNT = 200;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        UnsafeCount counter = new UnsafeCount();
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        counter.count();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println("多线程调用计数器i,运行后的值为: " + counter.i);
    }

    public void count() {
        i++;
    }
}

运行以上的代码发现:当线程数量增加,每个线程调用计数器的次数变大时,每次运行的结果是错误且不固定的。

为了实现实在一个多线程环境下、线程安全的计数器,需要使用AtomicInteger的原子自增运算。

import java.util.concurrent.atomic.AtomicInteger;
public class SafeCount {
    private AtomicInteger atomic = new AtomicInteger(0);
    private static final int THREAD_COUNT = 200;
    public static void main(String[] args) {
        SafeCount counter = new SafeCount();
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j=0;j<10000;j++){
                        counter.count();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println("多线程调用线程安全的计数器atomic:"+counter.atomic);
    }
    public void count() {
        // 调用compareAnSet方法,使用循环的CAS操作实现计数器的原子自增
        for (; ; ) {
            int expect = atomic.get();
            int curVal = expect + 1;
            if (atomic.compareAndSet(expect, curVal)) {
                break;
            }
        }
    }
}

与非线程安全的计数器相比,线程安全的计数器有以下特点:

上面的count方法实现的AtomicInteger原子自增,可以只需要调用incrementAndGet()一个方法就能实现。

public void count() {
    // 调用incrementAndGet方法,实现AtomicInteger的原子自增
    atomic.incrementAndGet();
}

因为incrementAndGet()方法,封装了通过循环的CAS操作实现AtomicInteger原子自增的代码。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}
(3)CAS操作存在的问题

1. ABA问题

2. 循环时间过长,开销大

循环的CAS操作如果长时间不成功,会给CPU带来非常大的执行开销。

3. 只能保证一个共享变量的原子操作

③ 无同步方案

同步只是保证共享数据争用时正确性的一种手段,如果不存在共享数据,自然无须任何同步措施。

(1)栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题。

因为方法中的局部变量不会逃出该方法而被其他线程访问,因此可以看做JVM栈中数据,属于线程私有。

(2)可重入代码(Reentrant Code)

可重入代码又叫纯代码(Pure Code),可在代码执行的任何时候中断他它,转去执行另外一段代码(包括递归调用它本身),控制权返回后,原来的程序不会出现任何错误。

所有可重入的代码都是线程安全,并非所有线程安全的代码都是可重入的。

可重入代码的共同特征:

如何判断代码是否具备可重入性?如果一个方法,它的返回结果是可预测的。只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性,当然也就是线程安全的。

(3)线程本地存储(TLS)

线程本地存储(Thread Local Storage):

TLS的重要应用实例:经典的Web交互模型中,一个请求对应一个服务器线程,使得Web服务器应用可以使用。学习资料:[Java进阶视频资源]

Java中没有关键字可以将一个变量定义为线程所独享,但是Java中创建了java.lang.ThreadLocal类提供线程本地存储功能。

ThreadLocal的编程实例:

public class TLS {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置当前线程的本地线程变量
                threadLocal1.set("thread1");
                threadLocal2.set(1);
                System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                // 使用完毕后要删除,避免内存泄露
                threadLocal1.remove();
                threadLocal2.remove();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal1.set("thread2");
                threadLocal2.set(2);
                System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                threadLocal1.remove();
                threadLocal2.remove();
            }
        });
        thread1.start();
        thread2.start();
        // 没有通过ThreadLocal为主线程添加过本地线程变量,获取到的内容都是null
        System.out.println(threadLocal1.get()+": "+threadLocal2.get());
    }
}

对ThreadLocal的正确理解:

ThreadLocal的实现:

最原始的想法:ThreadLocal维护线程与实例的映射。既然通过ThreadLocal对象为线程添加本地线程变量,那就将ThreadLocalMap放在ThreadLocal中。

原始想法存在的缺陷:多线程并发访问ThreadLocal中的Map,需要添加锁。这是, JDK 未采用该方案的一个原因。

优化后的方法:Thread维护ThreadLocal与实例的映射。Map是每个线程所私有,只能在当前线程通过ThreadLocal对象访问自身的Map。不存在多线程并发访问同一个Map的情况,也就不需要锁。

优化后存在内存泄露的情况:JDK1.8中,ThreadLocalMap每个Entry对ThreadLocal对象是弱引用,对每个实例是强引用。当ThreadLocal对象被回收后,该Entry的键变成null,但Entry无法被移除。使得实例被Entry引用无法回收,造成内存泄露。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8