进程和线程

进程是资源分配的基本单位。
线程是独立调度的基本单位。

进程和线程的关系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  • 处理机分给线程,即真正在处理机上运行的是线程。
  • 线程是指进程内的一个执行单元,也是进程内的可调度实体。

线程与进程的区别:

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
  • 系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统 的明显大于创建或撤销线程时的开销。
  • 进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而 线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。

总结

线程的划分尺度小于进程,使得多线程程序的并发性高。 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序运行效率。 线程在执行过程中,每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口。 但是线程不能够独立执行,必须依存在应用程序中,有应用程序提供多个线程执行控制。 从逻辑角度看,多线程的意义是在一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

线程的生命周期

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):

PS:操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。

进程的状态

三态模型

在多道程序系统中,进程在处理器上交替运行,状态也不断地发生变化。进程一般有3种基本状态:运行、就绪和阻塞。

(1)运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

(2)就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

(3)阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。

五态模型

对于一个实际的系统,进程的状态及其转换更为复杂。引入新建态终止态构成了进程的五态模型。

新建态: 对应于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。 进程正在创建过程中,还不能运行。操作系统在创建状态要进行的工作包括分配和建立进程控制块表项、建立资源表格(如打开文件表)并分配资源、加载程序并建立地址空间表等。创建进程时分为两个阶段,第一个阶段为一个新进程创建必要的管理信息,第二个阶段让该进程进入就绪状态。由于有了新建态,操作系统往往可以根据系统的性能和主存容量的限制推迟新建态进程的提交。

终止态:进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息(如记帐和将退出代码传递给父进程)。类似的,进程的终止也可分为两个阶段,第一个阶段等待操作系统进行善后处理,第二个阶段释放主存。

由于进程的不断创建,系统资源特别是主存资源已不能满足所有进程运行的要求。这时,就必须将某些进程挂起,放到磁盘对换区,暂时不参加调度,以平衡系统负载;进程挂起的原因可能是系统故障,或者是用户调试程序,也可能是需要检查问题。

活跃就绪:是指进程在主存并且可被调度的状态。

静止就绪(挂起就绪):是指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。

活跃阻塞:是指进程已在主存,一旦等待的事件产生便进入活跃就绪状态。

静止阻塞:是指进程对换到辅存时的阻塞状态,一旦等待的事件产生便进入静止就绪状态。

进程/线程的通信

进程间的通信方式

  • 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是 针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量, 配合使用,来实现进程间的同步和通信。
  • 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。 它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  • 信号 ( sinal ) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生

线程间的通信方式

  • 使用 volatile 关键字:使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式。
  • 使用 Object 类的 wait() 、 notify() 和 notifyaAll(),它们是多线程通信的基础,而这 种实现方式的思想自然是线程间通信。注意: wait 和 notify 必须配合 synchronized 使用,wait 方法释放锁,notify 方法不释放锁
  • 使用 JUC 工具类 CountDownLatch
    jdk1.5 之后在 java.util.concurrent 包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书写,CountDownLatch基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。

线程模型

使用用户线程实现(多对一模型 M:1)

广义上来讲,一个线程只要不是内核线程,都可以任务是用户线程(User Threa,UT)的一种。多个用户线程映射到一个内核线程,用户线程建立在用户空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完成,对内核透明。果程序实现得当,不需要切换内核态,因此操作可以是非常快且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。

优点:

  1. 线程的上下文切换都发生在用户空间,避免了模态切换(mode switch),减少了性能的开销。
  2. 用户线程的创建不受内核资源的限制,可以支持更大规模的线程数量。

缺点:

  1. 所有的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器可以被利用,浪费了其它处理器资源,不支持并行,在多处理器环境下这是不能够被接受的,如果线程因为 I/O 操作陷入了内核态,内核态线程阻塞等待 I/O 数据,则所有的线程都将会被阻塞。
  2. 增加了复杂度,所有的线程操作都需要用户程序自己处理,而且在用户空间要想自己实现 “阻塞的时候把线程映射到其他处理器上” 异常困难。

Java、Ruby等予以都曾经使用过用户线程,最终又都放弃了使用它。

使用内核线程实现(一对一模型 1:1)

使用内核线程实现的方式被称为1:1实现。内核线程(Kernel Levvel Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。其实程序一般不会直接使用内核线程,程序使用的是内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,轻量级进程也是属于用户线程。

所以这里统一称为用户线程。

每个用户线程都映射到一个内核线程,每个线程都成为一个独立的调度单元,由内核调度器独立调度,一个线程的阻塞不会影响到其他线程,从而保障整个进程继续工作。

优点:

  1. 每个线程都成为一个独立的调度单元,使用内核提供的线程调度功能及处理器映射,可以完成线程的切换,并将线程的任务映射到其他处理器上,充分利用多核处理器的优势,实现真正的并行。

缺点:

  1. 每创建一个用户级线程都需要创建一个内核级线程与其对应,因此需要消耗一定的内核资源,而内核资源是有限的,所以能创建的线程数量也是有限的。
  2. 模态切换频繁,各种线程操作,如创建、析构及同步,都需要进行系统调用,需要频繁的在用户态和内核态之间切换,开销大。

使用用户线程加轻量级进程混合实现(多对多模型 M:N)

内核线程和用户线程的数量比为 M : N,这种模型需要内核线程调度器和用户空间线程调度器相互操作,本质上是多个线程被映射到了多个内核线程。

综合了前面两种模型的优点:

  1. 用户线程的创建、切换、析构及同步依然发生在用户空间,能创建数量更多的线程,支持更大规模的并发。
  2. 大部分的线程上下文切换都发生在用户空间,减少了模态切换带来的开销。
  3. 可以使用内核提供的线程调度功能及处理器映射,充分利用多核处理器的优势,实现真正的并行,并降低了整个进程被完全阻塞的风险。

Java采用的线程模型

一句话总结:Java 的线程是映射到操作系统的原生线程之上的。

JVM 没有限定 Java 线程需要使用哪种线程模型来实现, JVM 只是封装了底层操作系统的差异,而不同的操作系统可能使用不同的线程模型,例如 Linux 和 windows 可能使用了一对一模型,solaris 和 unix 某些版本可能使用多对多模型。所以一谈到 Java 语言的多线程模型,需要针对具体 JVM 实现。

比如 Sun JDK 1.2开始,线程模型都是基于操作系统原生线程模型来实现,它的 Window 版和 Linux 版都是使用系统的 1:1 的线程模型实现的。

Java创建线程方式

继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.thread;

public class FirstThreadTest extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}

通过Runnable接口创建线程类

  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.thread;

public class RunnableThreadTest implements Runnable {
private int i;

public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt, "新线程1").start();
new Thread(rtt, "新线程2").start();
}
}
}
}

通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

1
2
3
public interface Callable {
V call() throws Exception;
}

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 20) {
new Thread(ft, "有返回值的线程").start();
}
}
try {
System.out.println("子线程的返回值:" + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}

对比

采用实现Runnable、Callable接口的方式创建多线程时

优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

使用继承Thread类的方式创建多线程时

优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

线程类已经继承了Thread类,所以不能再继承其他父类。

Runnable和Callable的区别

  • Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  • call方法可以抛出异常,run方法不可以。
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

死锁

定义

死锁是指多个进程在运行过程中,因为争夺资源而造成的一种僵局,如果没有外力推进, 处于僵局中的进程就无法继续执行。

四个条件

  1. 互斥条件(Mutual Exclusion):进程对所分配的资源进行排他性的使用
  2. 请求和保持条件(Hold and Wait):进程被阻塞的时候并不释放锁申请到的资源
  3. 不可剥夺条件(No Preemption):进程对于已经申请到的资源在使用完成之前不可以被剥夺
  4. 环路等待条件(Circular Wait):发生死锁的时候存在的一个进程-资源环形等待链

处理

  1. 预防死锁:破坏产生死锁的 4 个必要条件中的一个或者多个;实现起来比较简单, 但是 如果限制过于严格会降低系统资源利用率以及吞吐量 避免死锁:在资源的动态分配中,防止系统进入不安全状态(可能产生死锁的状态)- 如银行家算法。(原理:HashMap/有向无环图)
  2. 检测死锁:允许系统运行过程中产生死锁,在死锁发生之后,采用一定的算法进行检测, 并确定与死锁相关的资源和进程,采取相关方法清除检测到的死锁。实现难度大。
  3. 解除死锁:与死锁检测配合,将系统从死锁中解脱出来(撤销进程或者剥夺资 源)。对 检测到的和死锁相关的进程以及资源,通过撤销或者挂起的方式,释放一 些资源并将其分 配给处于阻塞状态的进程,使其转变为就绪态。实现难度大

volatile关键字

只能保证操作的可见性和有序性,不能保证对变量的操作是原子性的。

当变量被定义为 volatile 后具备两种特性:
(1)保证变量对所有线程可见:当一条线程修改了变量值,新值对于其他线程来说是立即 可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操 作符并非原子操作,导致 volatile 变量运算在并发下仍不安全。
(2)禁止指令重排序优化:使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相 当于一个内存屏障,后面的指令不能重排到内存屏障之前。使用 lock 前缀引发两件事:

  1. 将当前处理器缓存行的数据写回系统内存。
  2. 使其他处理器的缓存无效。相当于对缓存变量 做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见。 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
     它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指
    令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完
    成;
     它会强制将对缓存的修改操作立即写入主存;
     如果是写操作,它会导致其他CPU中对应的缓存行无效。
    静止指令重排非常重要,例如常见的DCL单例模式(Double Check Lock, DCL),即双锁检测
    volatile可以确保生成Singleton对象时不发生指令重排
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
}
}

注意: volatile并不能保证原子性
例如100个线程同时对volatile int i = 0进行加一1000次,结果必然远远小于100000,原因在于在JMM内存模型中,volatile只能保证可见,即线程每次会从主存中获取实时的变量,但是当线程将加一数据写回主存时则会存在并发问题,可能会发生多次覆写,也就是所说的“我改了你马上能看到,但这并不影响我随时修改”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Main test = new Main();
for (int i = 0; i < 100; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
test.increase();
}

;
}.start();
}
System.out.println(test.inc);
}
}

得到结果99000

我们可以使用AtomicInteger类使得变量i的自增操作变为原子操作,其底层原理其实是CAS完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public AtomicInteger inc = new AtomicInteger(0);
public void increase() {
inc.incrementAndGet();
}
public static void main(String[] args) {
final Main test = new Main();
for (int i = 0; i < 100; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
test.increase();
}

;
}.start();
}
System.out.println(test.inc);
}
}

得到结果100000

Synchronized关键字

介绍

可以保证原子性、可见性和有序性。Synchronized本质是一个资源监视器monitor。每个 Java 对象都有一个关联的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被 其他线程获取。

可重入?

根据《Java虚拟机规范》的要求,在执行monitor指令时,首先要尝试获取对象的锁。如果对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的数值增加一,而在执行monitorexit指令时,则会将锁的计数器的值减一。计数器如果为0,则代表锁被释放
由此得出推论:

  • 被synchronized修饰的同步块对同一条线程来说时可重入的,即同一线程反复进入同步块也不会出现把自己锁死的情况
  • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前会无条件阻塞后面其他线程的进入

在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

关键字的使用

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能

PS:构造方法不能使用 synchronized 关键字修饰。构造方法本身就属于线程安全的,不存在同步的构造方法一说。

ReentrantLock重入锁

介绍

ReentrantLock是java.util.concurrent.locks.Lock接口的最常见的一种实现,他和synchronized一样是可重入的

相比synchronized增加

  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理别的事情。可中断特性对处理执行时间非常长的同步块有帮助。
  • 公平锁:多个线程在等待同一锁时,必须按照申请锁的顺序依次获得锁,而非公平锁不保证这一点,锁释放时任何一个线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但是可以通过带boolean的构造函数指定使用公平锁。
  • 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象。

分类

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

独享锁和共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock

乐观锁和悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。(CAS?)
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。