进程就是程序的一次动态执行过程,通俗来讲,进程就是正在运行的程序,它是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。但进程的开启是非常耗费时间的,所以有必要对其进行进一步的划分以提高性能。一个进程可以同时有多个线程,相当于一个程序中同时进行多个任务,多个线程共享同一个进程的资源(堆内存和方法区)。所有的线程一定要依附于进程才能够存在,一旦进程消失,线程一定也会消失。多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。线程是处理器调度和分派的基本单位,而且多线程具有随机性,抢占CPU执行权的概率完全是随机的。
任何线程一般都具有五种状态,即:创建,就绪,运行,堵塞与终止。
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
java命令会启动java虚拟机,相当于启动了一个应用程序,相当于启动了一个进程。虚拟机会开启一个主线程去寻找main方法,所以说main方法是运行在主线程中的。但是虚拟机在工作时还会启动垃圾回收机制,也就相当于开启了另一个线程。所以说,我们的JVM虚拟机是多线程的。
在Java中,要想实现多线程,就必须依靠一个线程的主类,在主类中重写run()方法作为线程的主体。不管是以三种方式中的哪一个实现多线程,都是为了定义这个主类。
三种实现方式的选择:尽量避免继承Thread类,优先考虑实现接口(Runnable或Callable)的方法。因为Java采用的是单继承的模式,继承Thread类就会带来这种局限性,没法再继承其他类;另外,实现接口可以更方便的实现数据共享的概念。
申明一点,多线程启动的唯一方法就是Thread类中的start()
方法。
public class MyThread extends Thread {
// 重写run方法,作为线程的主操作方法
@Override
public void run() {
...
}
}
MyThread threadA = new MyThread("ThreadA");
MyThread threadB = new MyThread("ThreadB");
threadA.start();
threadB.start();
Thread类也是Runnable类的接口。使用Runnable接口实现多线程:
public class MyThread implements Runnable {
// 重写run方法,作为线程的主操作方法
@Override
public void run() {
...
}
}
public Thread(Runnable target)
)MyThread mt1 = new MyThread();
new Thread(mt1).start(); // 多线程调用同一个Runnable对象,就可以实现数据共享
new Thread(mt1).start();
使用Runnable接口实现的多线程可以避免单继承的局限,但是Runnable接口存在一个问题就是没有办法返回run方法的操作结果(public void run()
)。为了解决这个问题,从JDK1.5开始,引入了这个接口java.util.concurrent.Callable
:
@FunctionalInterface
public interface Callable<V> { V call() throws Exception; }
这个接口中只定义了一个**call()**方法,而且在call()方法上可以实现线程操作数据的返回,返回类型由Callable接口上的泛型决定。但是注意,Callable接口并非Runnable接口的子类,意味着不能直接传入Thread构造器并开启线程。
为了开启线程并获取这个返回值,靠Thread类是不可以的。为了解决这个问题,从JDK1.5起,引入了java.util.concurrent.FutureTask
类,定义如下:
public class FutureTask<V> extends Object implements RunnableFuture<V>{...}
FutureTask类提供了Callable接口子类的构造方法,并定义了专门的方法来获取多线程中的返回值:public FutureTask(Callable
,public V get()
。
同时,FutureTask是Runnable接口子类,可以使用public Thread(Runnable target)构造。所以开启Callable对象线程的步骤一般是 callable - FutureTask(callable) - Thread(futuretask).start()。
import java.util.concurrent.Callable;
public class MyThread implements Callable<String> {
private int ticket = 10;
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
if (this.ticket > 0)
System.out.println("ticket=" + this.ticket--);
}
return "售完";
}
}
// 实例化多线程对象
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
// 使用public FutureTask(Callable callable)实例化FutureTask
FutureTask<String> task1 = new FutureTask(myThread1);
FutureTask<String> task2 = new FutureTask(myThread2);
// FutureTask是Runnable接口子类,可以使用public Thread(Runnable target)构造
new Thread(task1).start();
new Thread(task2).start();
// 调用获取返回值
String msg1 = task1.get();
String msg2 = task2.get();
System.out.println("线程1返回的结果是:" + msg1 + "\t线程2返回的结果是:" + msg2);
多线程启动的唯一方法就是Thread类中的start()
方法。start()方法里面会调用一个start0()的方法,而且这个方法是用native声明的。java中调用本机操作系统提供的函数的技术叫做JNI(Java Native Interface ),这个技术离不开特定的操作系统,因为多线程必须由操作系统来分配资源。这项操作是根据JVM负责根据不同的操作系统实现的。start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并执行,所以线程并不会会立即运行。而run()方法是线程启动后要进行回调(callback)的方法。
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。定时器在实际开发中应用场景不多,一般由第三方框架实现。在Java中,可以通过Timer + TimerTask 来实现定时器功能。
Timer用于在后台线程中计划执行任务,可安排任务执行一次,或者定期重复执行。
TimerTask是一个抽象类,实现了Runnable接口。它的子类代表一个可以被Timer计划的任务,具体的任务在TimerTask中run方法中实现(TimerTask相当于专门用来制定定时任务的Runnable对象)。
定时器的启动必须通过Timer的schedule()方法(相当于start方法开启线程):
schedule(TimerTask task, long delay); 指定任务执行的延迟时间
schedule(TimerTask task,long delay,long period); 指定任务执行的延迟时间和周期
schedule(TimerTask task, Date time); 指定任务执行的准确时间
schedule(TimerTask task, Date firstTime, long period);指定任务执行的准确时间和周期
由于多线程执行的异步性,会给系统造成混乱,比如当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错。因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系:
间接相互制约可以称为互斥,直接相互制约可以称为同步。同步包括互斥,互斥其实是一种特殊的同步。
所谓死锁,指两个线程都在等待彼此先完成,造成程序的停滞,一般程序的死锁都在运行期产生的。
死锁产生的必要条件:
如何避免死锁:
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。临界区指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。
Java中实现同步的方式:
@Override
public void run() {
synchronized (this) {
...
}
}
Java的内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。 线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁。
@Override
public void run() { //调用同步方法 }
// 同步方法
private synchronized void A() {...}
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:a.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;b.禁止进行指令重排序(在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序,多线程下会影响正确性)。
注意 volatile 没有原子性,仅仅实现了对变量操作的可见性。volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。
Lock比使用synchronized方法和语句可以获得的更广泛的锁定操作。使用Lock锁需要自己手动释放锁,灵活性更高。
所谓重入锁,是针对同一个线程而言的,指已经获得锁的情况下可以再次请求获取锁,但相应的也要释放。
ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
private static final Lock lock = new ReentrantLock(); //ReentrantLock是Lock的实现类
@Override
public void run() {
lock.lock(); // 上锁,请求不成功则等待
try{
//处理任务
}catch(Exception ex){
//处理异常
}finally{
lock.unlock(); //释放锁
}
}
java.util.concurrent 下的 atomic 包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。其实现基于CAS非阻塞算法。
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;
AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素
AtomicReference:原子更新引用类型;
AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
AtomicMarkableReference:原子更新带有标记位的引用类型;
AtomicIntegeFieldUpdater:原子更新整型字段类;
AtomicLongFieldUpdater:原子更新长整型字段类;
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。
ThreadLocal是一个本地线程副本变量工具类。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路,同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。
ThreadLocal类提供了三个public方法:
常见操作是将ThreadLocal设置为public static修饰的全局共享的,在多线程中的run()里面调用ThreadLoca对象的set(T value)方法,T是泛型,value就表示要设置的变量。
set(T value)方法会自动获取当前线程的 ThreadLocalMap 对象,然后往这个 map 中插入一条记录, key 其实是 ThreadLocal 对象(一个线程可能会遇到多个 ThreadLocal 形式的变量,用以区分), value 的 set 方法传进去的值。
在线程结束时可以调用 remove() 方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,在ThreadLocalMap中也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。ThreadLocalMap中使用了独特的开发地址法来解决hash冲突。
公平锁就是指线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。
非公平锁是一种获取锁的抢占机制,是随机获取锁的,和公平锁的区别就是先来的不一定先得到锁,导致某些线程可能一直拿不到锁,所以是不公平的。
Semaphore(信号量) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池限制,或者对容器施加边界。
Semaphore两个构造器:
通过构造器可以指定锁的个数。
获取锁:acquire() 如果请求不到就一直阻塞,直到请求通过或者线程被中断。
释放锁:release() 使用完毕,让出资源。
分为两种情况:
如果每个线程执行的代码相同,可以使用同一个 Runnable 对象,共享数据直接定义在这个 Runnable 对象中。之后创建多个Thread对象传入同一个Runnable对象开启线程即可。卖票系统就是这么做的。
如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,比如存款和取款。有两种方式实现数据共享:
a. 将共享数据封装在另外一个对象中逐一传递给各个 Runnable 对象(Runnable的构造器接收)。每个Runnable对象通过传入的这个对象来操作共享数据。这样容易实现针对该数据进行的各个操作的互斥和通信。
b. 将这些 Runnable 对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,外部类实现针对共享数据的操作方法来供内部类Runnable对象调用,以便实现对共享数据进行的各个操作的互斥和通信。
volatile实现了针对变量操作的可见性和禁止指令重排序,其本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,使用完之后再归还给线程池。线程池作用就是限制系统中执行线程的数量,这样可以节省了开辟子线程的时间,提高的代码执行效率。
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法:
调用他们的 execute 方法即可。
参照上个问题。
线程池的关键在于限制系统中执行线程的数量,合理使用线程池可以带来以下好处:
线程池刚刚创建好的时候,里面并没有线程。任务队列是作为参数传进来的,执行任务前,线程池会进行一系列的判断,而不是立刻执行任务。当使用execute方法提交一个任务时:
当一个线程的任务执行完之后,它会从任务队列中取下一个任务来执行;当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于
corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
悲观锁:假设并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。共即享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:假定并发环境是乐观的,虽然有可能发生并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用CAS(乐观锁的一种实现)实现的。
乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。
“使用 CAS 控制并发”与“使用乐观锁”并不等价。 CAS 只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。
在多线程高并发编程的时候,最关键的问题就是保证临界区(指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性)的对象的安全访问。
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。
CAS(compare and swap)基于乐观策略,有如下优势:
CAS的实现往往需要硬件的支持,多数处理器都都实现了一个 CAS 指令,实现“Compare And Swap”的语义(这里的 swap 是“换入”,也就是 set),构成了基本的乐观锁。
CAS 包含 3 个操作数:内存位置(V)、预期原值(A)和新值(B) 。
A表示要进行比较的预期旧值 ,B是拟写入的新值 ,当且仅当位置 V 的值等于 A 时, CAS 才会通过原子方式用新值 B 来更新位置 V 的值;如果V不等于A,就说明值已经被其他线程修改过了,直接将V值返回。
CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”
通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。
类似于 CAS 的指令允许算法执行读-修改-写操作,而无需担心线程安全问题。
有一个特殊情况是,比如 V 本来是 = A的,但是经过了 A - B - A的过程,就会误以为其他线程没有改动过这个值,从而影响下一步决策。一个解决方法就是给值加上版本号,比如1A - 1B - 2A 这种,这样就很容易判断了。
java.util.concurrent
自JDK 5之后加入Java平台,使得Java下的并发编程变得更加简单,强大。java.util.concurrent
包含许多线程安全、测试良好、高性能的并发构建块:
java.util.concurrent.atomic (多线程的原子性操作提供的工具类)
java.util.concurrent.lock (多线程的锁机制)
工厂和工具方法Executor , ExecutorService , ScheduledExecutorService ,ThreadFactory和Callable在此包中定义。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
线程池作用就是限制系统中执行线程的数量。
减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存导致服务器死机,或过少导致效率低下。
Executors 类提供工厂方法用来创建不同类型的线程池:
返回类型均为ExecutorService类或者它的子类。
Executor 是 Java 线程池的顶级接口,仅仅提供了execute(Runnable command)方法用来提交任务,且没有返回值。
ExecutorService接口继承了Executor接口,提供了线程生命周期管理的方法,常用该接口来实现和管理多线程。
ExecutorService接口的实现类有:
可以选择实例化其实现类,也可以选择使用Executors的静态方法(上述)。
ExecutorService 中的execute(Runnable command)方法与Executor并无区别,但是由于无法接受返回值,ExecutorService提供了特有的提交任务的方法submit,是基于其父接口Executor的execute(Runnable command)扩展而来。不同于execute只能接受Runnable对象,submit方法可以接收Callable对象,这就意味着submit可以接收线程执行完毕的返回值(返回值用Future类封装)。submit方法的定义如下:
Future
submit(Callable task)Future>
submit(Runnable task) Future
submit(Runnable task, T result)如果传入的是Runnable对象,则返回的Future为null,但是可以用它来检测run()方法是否执行完毕。
ExecutorService 中同样提供了针对Callable任务列表的提交方法,然后可以等待全部任务或者部分执行完毕:
invokeAll(Collection extends Callable> tasks)
invokeAny(Collection extends Callable> tasks)
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已执行结束。如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。 invokeAll() 返回一系列的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。
记住,一个任务可能会由于一个异常而结束,因此它可能没有 “成功”。无法通过一个 Future 对象来告知我们任务是否是正常结束还是因为异常而结束。
使用 shutdown() 和 shutdownNow() 可以关闭线程池。二者的区别:
shutdown 只是将空闲的线程 interrupt 了, shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为 interrupt()方法能力有限。
ThreadPoolExecutor类是 ExecutorService 接口的一个实现。ThreadPoolExecutor 中的连接池大小可以动态变化。
池中线程的数量由以下变量决定:corePoolSize 和maximumPoolSize,称为核心线程数和最大线程数。这两个值可以通过有参构造指定,也可以通过set方法改变。
当一个新任务被提交时,如果池中正在运行的线程数小于corePoolSize,那么将会有一个新的线程被创建去执行这个任务;如果正在运行的线程数大于corePoolSize但是小于maximumPoolSize,任务进入队列等待分配线程,并且只有在任务队列已满的情况下才会去创建新线程。
这种机制的核心在于维护核心线程数的数量。当核心线程数和最大线程数相等时,等同于设置了一个固定大小的线程池。
ScheduledExecutorService 是 ExecutorService 接口的子接口。它能够将任务延后执行,或者间隔固定时间多次执行,完全可以用来代替定时器。
ScheduledThreadPoolExecutor 是它的实现类。
通过Executors的内置静态方法 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 来构造;
直接构造ScheduledExecutorService的实现类对象(ScheduledThreadPoolExecutor)来实例化。
没有什么特别的方法,都使用继承自ExecutorService的 shutdown() 或 shutdownNow()方法来关闭线程。
常用的并发队列有阻塞队列和非阻塞队列。前者使用锁实现,后者使用CAS非阻塞算法实现。都是Java并发库Java util.concurrent 下的重要组成部分。
BlockingQueue 提供了线程安全的队列访问方式:
并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。BlockingQueue是一个接口,所有方法使用内部锁或其他形式的并发控制在原子上实现其效果。
BlockingQueue实现被设计为主要用于生产者 - 消费者队列,但另外支持Collection接口。BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景:
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
方法\处理方式 | 抛出异常 | 返回特殊值 true / false | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
BlockingQueue不接受null元素。 使用其实现类尝试插入null值时会抛出NullPointerException异常。
介绍几个实现类常用的方法:
与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。在底层,非阻塞队列使用的是 CAS(compare and swap)来实现线程执行的非阻塞。
ConcurrentLinkedQueue 是一个线程安全的队列,基于链表结构实现,是一个无界队列,采用的也是先进先出(FIFO)入队规则。ConcurrentLinkedQueue 使用 CAS 非阻塞算法实现使用 CAS 解决了当前节点与 next 节点之间的安全链接和对当前节点值的赋值。由于使用 CAS 没有使用锁,所以获取 size 的时候有可能进行 offer, poll 或者 remove 操作,导致获取的元素个数不精确,所以在并发情况下 size 函数不是很有用。
ConcurrentLinkedQueue 中有两个 volatile 类型的 Node 节点分别用来存在列表的首尾节点,其中 head 节点存放链表第一个 item (为 null) 的节点, tail 则并不是总指向最后一个节点。 Node 节点内部则维护一个变量 item 用来存放节点的值, next 用来存放下一个节点,从而链接为一个单向无界列表。
public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}
初始化时候会构建一个 item 为 NULL 的空节点作为链表的首尾节点。
可知入队出队函数都是操作 volatile 变量: head, tail。所以要保证队列线程安全只需要保证对这两个 Node 操作的可见性和原子性,由于 volatile 本身保证可见性,所以只需要看下多线程下如果保证对着两个变量操作的原子性:对于 offer 操作是在 tail 后面添加元素,也就是调用 tail.casNext 方法,而这个方法是使用的 CAS 操作,只有一个线程会成功,然后失败的线程会循环一下,重新获取 tail,然后执行 casNext 方法。对于 poll 也是这样的。
ConcurrentHashMap 是 Java 并发包中提供的一个线程安全且高效的 HashMap 实现, ConcurrentHashMap在并发编程的场景中使用频率非常之高。
HashMap的底层数据结构是数组+链表,数组(table)充当索引,链表解决冲突。
put(key,value):
——HashMap在根据hash值来计算索引时,为了尽可能的使元素分布均匀且提高运算效率,使用了位运算:
static int indexFor(int h, int length) {
return h & (length-1);
}
h是通过K的hashCode最终计算出来的哈希值,length是目前容量。使用 & 运算(同为1得1),当容量是2^n时,h & (length - 1) == h % length (取余),得到的结果即为索引 i。
可以看出,如果有元素根据hash值计算出了相同的索引 i ,并且key值不一致,就会产生冲突。冲突的元素会以链表的形式放在同一索引下,这种解决方法叫做链表法,另一种解决冲突的方式是开放地址法,由ThreadLocal采用。
在最坏的情况下,如果所有元素都存在冲突,那么HashMap就会变成由数组变为链表,复杂度由 O(1)变成O(n),性能变差。JDK8对此进行了改进,当链表的长度超过8时,此后就会变成红黑树结构(复杂度为O(logn)),使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。
从上文分析看出,resize()方法对于HashMap来说非常重要。因为table的大小直接影响冲突发生的概率,越小就越容易发生冲突,链表长度越大,查找时性能越差。
resize意味着需要重新建一张大小是当前2倍的新table,然后遍历数组,遍历链表,把元素再挨个重新放入新table中。这是一个相当耗费资源的事情。JDK1.8以前的版本中不管是put还是resize使用的都是头插法(作者认为越靠后插入的元素被查找的概率越大,放在前面可以提高查找效率),也就是说,新元素会放在链表的头部。
头插法是造成多线程下HashMap产生死循环的原因,死循环是在get()时发生的,但是却是在put后扩容时产生错误的。
问题出在如果是多线程同时操作同一个hashMap,在都需要resize的情况下,由于头插法会改变next指针,多线程操作就有可能导致新table中某处形成一个循环链表,也就是链表尾部的元素的next直接指向头部元素而非null值。
HashMap的get(key)会根据key值先返回value所在索引 i ,然后去遍历 table[i] 处的链表(如果有的话),直到找出key对应的value值。
但是如果这个链表是循环链表,而查找的key值不存在,就会发生死循环(next一直不为null,相当于遍历无限长的全是重复元素的链表),导致cpu 空转。
JDK1.8之后对这一问题进行了修复,作者统一使用尾插法(不管是put还是resize),HashMap死锁的问题得到解决。
HashTable是线程安全版的HashMap,但是,也仅仅是给所有的关键方法加上synchronized关键字,相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁只负责锁一段数据,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样便可 以有效地提高并发效率。这就是ConcurrentHashMap 所采用的"分段锁"思想:
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁 ReentrantLock ,扮演锁的角色,HashEntry 用于存储键值对数据。
整个 ConcurrentHashMap 由一个 Segment 数组组成。Segment 是子哈希表,也是数组+链表结构。一个 Segment 里维护了一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素。
当对 HashEntry 的数据进行修改时,必须首先获得它对应的 Segment 锁。并发环境下,对于不同 Segment 的数据进行操作是不用考虑锁竞争的。所以,对于同一个 Segment 的操作才需考虑线程同步,不同的 Segment则无需考虑。以默认的 concurrencyLevel = 16 来说,相当于可以同时支持16个线程并发。
HashEntry是目前我们提到的最小的逻辑处理单元了,其内部维护着链表结构:
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
//其他省略
}
Segment 类似哈希表,也会有负载因子loadFactor,和阈值 threshold ,以及特有的 concurrencyLevel 变量。这些值都可以选择根据构造方法指定,否则使用默认的 0.75,16,16。
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel){}
Segment 数组的大小 ssize 是由 concurrentLevel 来决定的,但是却不一定等于concurrentLevel,ssize 一定是大于或等于 concurrentLevel 的最小的 2 的次幂,这种处理方式与HashMap中定义initialCapacity类似,都是为了通过按位与的散列算法来定位 Segment 的 index。
ConcurrentHashMap 的 get 方法没有加锁 ,其中涉及到的共享变量都使用 volatile 修饰, volatile 可以保证内存可见性,所以不会读取到过期数据。
ConcurrentHashMap 的 put 方法加锁,只不过是锁粒度更细。
总的来说,ConcurrentHashMap 作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,相比HashTable 的全表锁在性能上的提升非常之大。
不同于内置同步和监视器,lock框架允许更灵活地使用锁(锁是用于通过多个线程控制对共享资源的访问的工具)和条件。本包下有三大接口:
java.util.concurrent 下的 atomic 包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据。
atomic类是通过自旋CAS操作volatile变量实现的。
在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值 load 到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
要保证多线程操作最后得到正确的变量值,就要保证操作的原子性,atomic 的存在意义就在于此。
具体使用-参考博客