本文快速回顾了常考的的知识点,用做面试复习,事半功倍。html
已发布知识点复习手册java
本文内容参考自CyC2018的Github仓库:CS-Notesgit
有删减,修改,补充额外增长内容github
本做品采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可。面试
若是多个线程对同一个共享数据进行访问而不采起同步操做的话,那么操做的结果是不一致的。算法
如下代码演示了 1000 个线程同时对 cnt 执行自增操做,操做结束以后它的值有可能小于 1000。数据库
public class ThreadUnsafeExample {
private int cnt = 0;
public void add() {
cnt++;
}
public int get() {
return cnt;
}
}
复制代码
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample example = new ThreadUnsafeExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
复制代码
997
复制代码
Java 内存模型试图屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。segmentfault
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。设计模式
加入高速缓存带来了一个新的问题:缓存一致性。若是多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,须要一些协议来解决这个问题。
全部的变量都存储在主内存中,每一个线程还有本身的工做内存,工做内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操做工做内存中的变量,不一样线程之间的变量值传递须要经过主内存来完成。
Java 内存模型定义了 8 个操做来完成主内存和工做内存的交互操做。
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操做具备原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操做具备原子性,例如对一个 int 类型的变量执行 assign 赋值操做,这个操做就是原子性的。可是 Java 内存模型容许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操做划分为两次 32 位的操做来进行,即 load、store、read 和 write 操做能够不具有原子性。
有一个错误认识就是,int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中,cnt 属于 int 类型变量,1000 个线程对它进行自增操做以后,获得的值为 997 而不是 1000。
使用 AtomicInteger 重写以前线程不安全的代码以后获得如下线程安全实现:
public class AtomicExample {
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}
public int get() {
return cnt.get();
}
}
复制代码
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicExample example = new AtomicExample(); // 只修改这条语句
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
复制代码
1000
复制代码
除了使用原子类以外,也可使用 synchronized 互斥锁来保证操做的原子性。它对应的内存间交互操做为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
public class AtomicSynchronizedExample {
private int cnt = 0;
public synchronized void add() {
cnt++;
}
public synchronized int get() {
return cnt;
}
}
复制代码
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
AtomicSynchronizedExample example = new AtomicSynchronizedExample();
final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(() -> {
example.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(example.get());
}
复制代码
1000
复制代码
可见性指当一个线程修改了共享变量的值,其它线程可以当即得知这个修改。
Java 内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
主要有有三种实现可见性的方式:
有序性是指:在本线程内观察,全部操做都是有序的。在一个线程观察另外一个线程,全部操做都是无序的,无序是由于发生了指令重排序。
在 Java 内存模型中,容许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字经过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障以前。
能够经过 synchronized 来保证有序性,它保证每一个时刻只有一个线程执行同步代码,至关因而让线程顺序执行同步代码。
happens-before是判断数据是否存在竞争、线程是否安全的重要依据
定义:
若是操做A happens-before 于 操做B,那么就能够肯定,操做B执行完以后,j 的值必定为 1;由于happens-before关系能够向程序员保证:在操做B执行以前,操做A的执行后的影响[或者说结果](修改 i 的值)操做B是能够观察到的[或者说可见的]
这里列举几个常见的Java“自然的”happens-before关系
① 程序顺序规则: 一个线程中的每一个操做,happens-before于该线程中的任意后续操做(也就是说你写的操做,若是是单线程执行,那么前面的操做[程序逻辑上的前]就会happens-before于后面的操做) 这里的影响指修改了 i 变量的值
② 监视器锁规则: 对一个锁的解锁,happens-before 于随后对这个锁的加锁
③ volatile变量规则: 对一个 volatile域的写,happens-before于任意后续对这个volatile域的读
④ 传递性:若是 A happens-before B,且 B happens-before C,那么A happens-before C
上面提到了能够用 volatile 和 synchronized 来保证有序性。除此以外,JVM 还规定了先行发生原则,让一个操做无需控制就能先于另外一个操做完成。
主要有如下这些原则:
Single Thread rule
在一个线程内,在程序前面的操做先行发生于后面的操做。
Monitor Lock Rule
一个 unlock 操做先行发生于后面对同一个锁的 lock 操做。
Volatile Variable Rule
对一个 volatile 变量的写操做先行发生于后面对这个变量的读操做。
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每个动做。
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过 interrupted() 方法检测到是否有中断发生。
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
Transitivity
若是操做 A 先行发生于操做 B,操做 B 先行发生于操做 C,那么操做 A 先行发生于操做 C。
采用Lock-Free算法替代锁,加上原子操做指令实现并发状况下资源的安全、完整、一致性
而关于Lock-Free算法,则是一种新的策略替代锁来保证资源在并发时的完整性的,Lock-Free的实现有三步:
一、循环(for(;;)、while)
二、CAS(CompareAndSet)
三、回退(return、break)
复制代码
经过关键字sychronize能够防止多个线程进入同一段代码,在某些特定场景中,volatile至关于一个轻量级的sychronize,由于不会引发线程的上下文切换
对于普通变量
对于volatile变量
内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提升执行性能,编译器和处理器会对指令进行重排序
JMM为了保证在不一样的编译器和CPU上有相同的结果,经过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:无论什么指令都不能和这条Memory Barrier指令重排序。
通常来讲,volatile大多用于标志位上(判断操做),
可是因为操做上的优点,只须要简单的声明一下便可,并且被它声明的代码块也是具备操做的原子性。
ThreadLocal提供了线程的局部变量,每一个线程均可以经过set()和get()来对这个局部变量进行操做,但不会和其余线程的局部变量进行冲突,实现了线程的数据隔离。
而ThreadLocal的设计,并非解决资源共享的问题,而是用来提供线程内的局部变量,这样每一个线程都本身管理本身的局部变量,别的线程操做的数据不会对我产生影响,至关于封装在Thread内部了,供线程本身管理。
它有三个暴露的方法,set、get、remove。
若是ThreadLocal不设为static的,因为Thread的生命周期不可预知,这就致使了当系统gc时将会回收它,而ThreadLocal对象被回收了,此时它对应key一定为null,这就致使了该key对应得value拿不出来了,而value以前被Thread所引用,因此就存在key为null、value存在强引用致使这个Entry回收不了,从而致使内存泄露。
避免内存泄露的方法:
threadLocal可以实现当前线程的操做都是用同一个Connection,保证了事务!
关于Volatile关键字具备可见性,但不具备操做的原子性,而synchronized比volatile对资源的消耗稍微大点,但能够保证变量操做的原子性,保证变量的一致性,最佳实践则是两者结合一块儿使用。
一、synchronized:解决多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。同步机制是提供一份变量,让全部线程均可以访问。
二、对于Atomic的出现,是经过原子操做指令+Lock-Free完成,从而实现非阻塞式的并发问题。
三、对于Volatile,为多线程资源共享问题解决了部分需求,在非依赖自身的操做的状况下,对变量的改变将对任何线程可见。
四、对于ThreadLocal的出现,并非解决多线程资源共享的问题,而是用来提供线程内的局部变量,省去参数传递这个没必要要的麻烦,ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。ThreadLocal是为每个线程都提供了一份独有的变量,各个线程互不影响。
等待IO的方式:阻塞,非阻塞
得到通知的方式:异步,非异步
多个线程无论以何种方式访问某个类,而且在主调代码中不须要进行同步,都能表现正确的行为。
线程安全有如下几种实现方式:
不可变(Immutable)的对象必定是线程安全的,不须要再采起任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽可能使对象成为不可变,来知足线程安全。
不可变的类型:
对于集合类型,可使用 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。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,所以这种同步也称为阻塞同步(Blocking Synchronization)。
从处理问题的方式上说,互斥同步属于一种悲观的并发策略,老是认为只要不去作正确的同步措施(例如加锁),那就确定会出现问题。
随着硬件指令集的发展,咱们有了另一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操做,若是没有其余线程争用共享数据,那操做就成功了;若是共享数据有争用,产生了冲突,那就再采起其余的补偿措施(最多见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不须要把线程挂起,所以这种同步操做称为非阻塞同步(Non-Blocking Synchronization)。
乐观锁须要操做和冲突检测这两个步骤具有原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操做最典型的是:比较并交换(Compare-and-Swap,CAS)。
CAS 指令须要有 3 个操做数,分别是:
当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。
可是不管是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操做。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改成B),而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试(不然什么都不作)
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操做。
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 * by the ThreadLocal class. */
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),能够在代码执行的任什么时候刻中断它,转而去执行另一段代码(包括递归调用它自己),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
这里的锁优化主要是指虚拟机对synchronized的优化。
锁竞争是kernal mode下的,会通过user mode(用户态)到kernal mode(内核态) 的切换,是比较花时间的。
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,若是在这段时间内能得到锁,就能够避免进入阻塞状态。
它只适用于共享数据的锁定状态很短的场景。
自旋次数的默认值是 10 次,用户可使用虚拟机参数 -XX:PreBlockSpin 来更改。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数再也不固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。检测到某段代码是线程安全的(言外之意:无锁也是安全的),JVM会安全地原有的锁消除掉!
逃逸分析:若是堆上的共享数据不可能逃逸出去被其它线程访问到,那么就能够把它们当成私有数据对待,也就能够将它们上的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了不少锁。例以下面的字符串拼接代码就隐式加了锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
复制代码
String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 以前,会转化为 StringBuffer 对象的连续 append() 操做,在 JDK 1.5 及之后的版本中,会转化为 StringBuilder 对象的连续 append() 操做,即上面的代码可能会变成下面的样子:
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
复制代码
虚拟机观察变量 sb,很快就会发现它的动态做用域被限制在 concatString() 方法内部。也就是说,sb 的全部引用永远不会“逃逸”到 concatString() 方法以外,其余线程没法访问到它。所以,虽然这里有锁,可是能够被安全地消除掉。
若是一系列的连续操做都对同一个对象反复加锁和解锁,频繁的加锁操做就会致使性能损耗。
上一节的示例代码中连续的 append() 方法就属于这类状况。若是虚拟机探测到由这样的一串零碎的操做都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操做序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操做以前直至最后一个 append() 操做以后,这样只须要加锁一次就能够了。
可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,频繁地进行互斥同步操做也会致使没必要要的性能损耗。
总结:在无竞争环境下,把整个同步都消除,CAS也不作。
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在以后获取该锁就再也不须要进行同步操做,甚至连 CAS 操做也再也不须要。
可使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
当锁对象第一次被线程得到的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操做将线程 ID 记录到 Mark Word 中,若是 CAS 操做成功,这个线程之后每次进入这个锁相关的同步块就不须要再进行任何同步操做。
当有另一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操做来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,所以也就不须要都使用互斥量进行同步,能够先采用 CAS 操做进行同步,若是 CAS 失败了再改用互斥量进行同步。(乐观锁)
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
若是 CAS 操做失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,若是是的话说明当前线程已经拥有了这个锁对象,那就能够直接进入同步块继续执行,不然说明这个锁对象已经被其余线程线程抢占了。若是有两条以上的线程争用同一个锁,那轻量级锁就再也不有效,要膨胀为重量级锁。
但若是存在锁竞争,除了互斥量的开销外,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁会比传统的重量级锁更慢。
简单来讲:若是发现同步周期内都是不存在竞争,JVM会使用CAS操做来替代操做系统互斥量。这个优化就被叫作轻量级锁。
缩小同步范围,例如对于 synchronized,应该尽可能使用同步块而不是同步方法。
多用同步类少用 wait() 和 notify(),多用CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类。他们简化了编码操做,而用 wait() 和 notify() 很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序能够不费吹灰之力得到优化。
多用并发集合少用同步集合。
使用本地变量ThreadLocal和不可变类来保证线程安全。
使用线程池而不是直接建立 Thread 对象,这是由于建立线程代价很高,线程池能够有效地利用有限的线程来启动任务。
使用 BlockingQueue 实现生产者消费者问题。
无论是同步集合仍是并发集合他们都支持线程安全,他们之间主要的区别体如今性能和可扩展性,还有他们如何实现的线程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他们并发的实现(好比:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)会慢得多。形成如此慢的主要缘由是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是经过使用先进的和成熟的技术像锁剥离。好比ConcurrentHashMap 会把整个Map 划分红几个片断,只对相关的几个片断上锁,同时容许多线程访问其余未上锁的片断。
java.util.concurrent包中包含的并发集合类以下:
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
复制代码
常见逸出的有下面几种方式:
具体解释见:segmentfault.com/a/119000001…
安全发布对象有几种常见的方式:
上面transferMoney()发生死锁的缘由是由于加锁顺序不一致而出现的~
例子中,改造为获得对应的hash值来固定加锁的顺序,这样咱们就不会发生死锁的问题了!
若是在调用某个方法时不须要持有锁,那么这种调用被称为开放调用!
使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过期限的时候,tryLock()不会一直等待,而是返回错误信息。
我是蛮三刀把刀,目前为后台开发工程师。主要关注后台开发,网络安全,Python爬虫等技术。
来微信和我聊聊:yangzd1102
Github:github.com/qqxx6661
同步更新如下博客
1. Csdn
拥有专栏:Leetcode题解(Java/Python)、Python爬虫开发
2. 知乎
拥有专栏:码农面试助攻手册
3. 掘金
4. 简书
本人长期维护的我的项目,彻底免费,请你们多多支持。
实现功能
网站地址
若是文章对你有帮助,不妨收藏起来并转发给您的朋友们~