多个线程无论以何种方式访问某个类,而且在主调代码中不须要进行同步,都能表现正确的行为。java
线程安全有如下几种实现方式:安全
不可变
不可变(Immutable)的对象必定是线程安全的,不须要再采起任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽可能使对象成为不可变,来知足线程安全。服务器
不可变的类型:数据结构
final 关键字修饰的基本数据类型
String
枚举类型
Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,可使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。多线程
public class ImmutableExample {架构
public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map); unmodifiableMap.put("a", 1); }
}
Exception in thread "main" java.lang.UnsupportedOperationException并发
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) at ImmutableExample.main(ImmutableExample.java:9)
Collections.unmodifiableXXX() 先对原始的集合进行拷贝,须要对集合进行修改的方法都直接抛出异常。分布式
public V put(K key, V value) {高并发
throw new UnsupportedOperationException();
}
互斥同步
synchronized 和 ReentrantLock。性能
非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,所以这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,老是认为只要不去作正确的同步措施,那就确定会出现问题。不管共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分没必要要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程须要唤醒等操做。
随着硬件指令集的发展,咱们可使用基于冲突检测的乐观并发策略:先进行操做,若是没有其它线程争用共享数据,那操做就成功了,不然采起补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不须要将线程阻塞,所以这种同步操做称为非阻塞同步。
乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操做最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令须要有 3 个操做数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操做时,只有当 V 的值等于 A,才将 V 的值更新为 B。
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操做。
如下代码使用了 AtomicInteger 执行了自增的操做。
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
如下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
如下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操做须要加的数值,这里为 1。经过 getIntVolatile(var1, var2) 获得旧的预期值,经过调用 compareAndSwapInt() 来进行 CAS 比较,若是该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
能够看到 getAndAddInt() 在一个循环中进行,发生冲突的作法是不断的进行重试。
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;
}
若是一个变量初次读取的时候是 A 值,它的值被改为了 B,后来又被改回为 A,那 CAS 操做就会误认为它历来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它能够经过控制变量值的版原本保证 CAS 的正确性。大部分状况下 ABA 问题不会影响程序并发的正确性,若是须要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
无同步方案
要保证线程安全,并非必定就要进行同步。若是一个方法原本就不涉及共享数据,那它天然就无须任何同步措施去保证正确性。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,由于局部变量存储在虚拟机栈中,属于线程私有的。
public class StackClosedExample {
public void add100() { int cnt = 0; for (int i = 0; i < 100; i++) { cnt++; } System.out.println(cnt); }
}
public static void main(String[] args) {
StackClosedExample example = new StackClosedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> example.add100()); executorService.execute(() -> example.add100()); executorService.shutdown();
}
100
100
若是一段代码中所须要的数据必须与其余代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。若是能保证,咱们就能够把共享数据的可见范围限制在同一个线程以内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特色的应用并很多见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽可能在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的普遍应用使得不少 Web 服务端应用均可以使用线程本地存储来解决线程安全问题。
可使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
对于如下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间以后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
public class ThreadLocalExample {
public static void main(String[] args) { ThreadLocal threadLocal = new ThreadLocal(); Thread thread1 = new Thread(() -> { threadLocal.set(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadLocal.get()); threadLocal.remove(); }); Thread thread2 = new Thread(() -> { threadLocal.set(2); threadLocal.remove(); }); thread1.start(); thread2.start(); }
}
1
为了理解 ThreadLocal,先看如下代码:
public class ThreadLocalExample1 {
public static void main(String[] args) { ThreadLocal threadLocal1 = new ThreadLocal(); ThreadLocal threadLocal2 = new ThreadLocal(); Thread thread1 = new Thread(() -> { threadLocal1.set(1); threadLocal2.set(1); }); Thread thread2 = new Thread(() -> { threadLocal1.set(2); threadLocal2.set(2); }); thread1.start(); thread2.start(); }
}
它所对应的底层结构图为:
每一个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
/* ThreadLocal values pertaining to this thread. This map is maintained
ThreadLocal.ThreadLocalMap threadLocals = null;
当调用一个 ThreadLocal 的 set(T value) 方法时,先获得当前线程的 ThreadLocalMap 对象,而后将 ThreadLocal->value 键值对插入到该 Map 中。
public void set(T value) {
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value);
}
get() 方法相似。
public T get() {
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();
}
ThreadLocal 从理论上讲并非用来解决多线程并发问题的,由于根本不存在多线程竞争。
在一些场景 (尤为是使用线程池) 下,因为 ThreadLocal.ThreadLocalMap 的底层数据结构致使 ThreadLocal 有内存泄漏的状况,应该尽量在每次使用 ThreadLocal 后手动调用 remove(),以免出现 ThreadLocal 经典的内存泄漏甚至是形成自身业务混乱的风险。
这种代码也叫作纯代码(Pure Code),能够在代码执行的任什么时候刻中断它,转而去执行另一段代码(包括递归调用它自己),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
免费Java高级资料须要本身领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。
传送门:https://mp.weixin.qq.com/s/Jz...