本系列笔记主要是读《java并发编程之美》所记录的笔记,其内容不仅包括书中内容精华摘录,也包括自己一些所感所想。
1.线程基础
1.1 线程相关基本概念
1.1.1 什么是进程
-
是代码在数据集合上的一次运行活动。
-
是系统进行资源分配和调度的基本单位。
-
一个进程至少有一个线程,进程中的多个线程共享进程资源。
1.1.2 什么是线程
-
线程是进程中的一个实体。
-
线程本身是不会独立存在的。
-
线程是
CPU
分配的基本单位。
1.1.3 进程和线程关系
-
一个进程中有至少一个线程(多个线程),多个线程之间共享进程的堆和方法区。
-
每个线程有自己的程序计数器和栈区域。
-
程序计数器是一块内存区域,用来记录线程当期要执行的指令地址。
-
程序计数器是为了记录线程让出
CPU
时的执行地址,待再次分配到时间片时线程就可从原来私有的计数器指定地址继续执行。 -
每个线程拥有自己的栈区域,用于存储线程私有的局部变量。
-
1.1.4 堆与方法区
-
堆是进程中最大的一个内存,用于进程中所有线程共享的,是进程创建时分配的,里面主要存放使用
new
操作创建的对象实例。 -
方法区则用来存放
JVM
加载的常量、静态变量、类等信息,也是线程共享。
1.2 线程创建与运行
线程创建方式有三种:实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。
1.2.1 继承Thread
类方式的实现
-
创建及运行
/** * @author huangdq * @version 1.0.0 * @ClassName NewThreadDemo.java * @Description 继承Thread 类并重写run,启动线程 * @createTime 2022年02月22日 10:42:00 */ public class NewThreadDemo { public static class myThread extends Thread{ @Override public void run() { System.out.println("我是一个子线程!"); } } // 主线程 public static void main(String[] args) { // 创建线程 myThread myThread = new myThread(); // 启动线程 myThread.start(); } }
-
注意:在创建(
new
)完thread
对象后,该线程并没有被启动执行,直到调用start
方法后才算真在被启动执行。-
而在调用
start
方法后线程并没有立马执行(处于就绪状态),而是获取到CPU
资源后才真在执行。 -
执行完毕,该线程就处于终止状态。
-
-
-
优点
- 使用该方式创建,可直接在重写的
run
方法内可直接使用this
来操作当前线程即可,而无需使用Thread.currentThread()
方法。
- 使用该方式创建,可直接在重写的
-
缺点
-
使用该方法已继承了
Thread
类,而java
并支持多继承,则其不能在继续继承其它类。 -
该方法任务与代码没有分离,执行多线程需要写多次任务与代码。
-
1.2.2 Runnable
接口的run
方法方式
-
创建及运行
/** * @author huangdq * @version 1.0.0 * @ClassName NewRunnableDemo.java * @Description 实现Runnable接口的run方法方式 * @createTime 2022年02月22日 11:11:00 */ public class NewRunnableDemo implements Runnable{ public void run() { System.out.println("我是一个子线程!"); } // 主线程 public static void main(String[] args) { // 创建线程 NewRunnableDemo task = new NewRunnableDemo(); // 启动线程 new Thread(task).start(); // 线程1 new Thread(task).start(); // 线程2 } }
-
优点
-
可继承
-
任务与代码分离
-
-
缺点
- 任务没有返回值
1.2.3 FutureTask
的方式
-
创建及运行
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; /** * @author huangdq * @version 1.0.0 * @ClassName NewFutureTaskDemo.java * @Description TODO * @createTime 2022年02月22日 11:24:00 */ public class NewFutureTaskDemo implements Callable<String> { public String call() throws Exception { return "hello"; } // 主线程 public static void main(String[] args) throws InterruptedException{ FutureTask<String> futureTask = new FutureTask<>(new NewFutureTaskDemo()); // 启动线程 new Thread(futureTask).start(); try { String s = futureTask.get(); System.out.println(s); }catch (ExecutionException e){ e.printStackTrace(); } } }
-
优点
- 能拿到多线程执行结果。
1.3 线程通知与等待
1.3.1 wait()
函数
-
当一个线程调用一个共享变量的
wait()
方法时,该调用线程会被阻塞挂起。直到发生以下情况才返回:-
其它线程调用了该共享对下的
notify()
或者notifyAll()
方法。 -
其他线程调用了该线程的
interrupt()
方法,该线程抛出InterruptedException
异常返回。
-
-
注意:如果调用
wait()
方法的线程没有事先获取该对象的监视器锁,则调用wait()
方法时调用线程会抛出IllegalMonitorStateException
异常。-
那么一个线程如何获取一个共享变量的监视器锁呢?
- 执行
synchronized
同步代码块时,使用该共享变量作为参数。
synchonized(共享变量){ // doSomething }
- 调用该共享变量的方法,并且该方法使用了
synchronized
修饰。
synchronized void add(int a, int b){ // doSomething }
- 执行
-
-
虚假唤醒
- 一个线程从挂起状态变为可以运行状态(也是被唤醒),即使该线程没有被其他线程调用
notify()
、notifyAll()
方法进行通知,或者被中断,或者等待超时。
- 一个线程从挂起状态变为可以运行状态(也是被唤醒),即使该线程没有被其他线程调用
1.3.2 wait(long timeout)
函数
-
该函数与
wait()
函数相比,多了一个超时参数,其不同之处在于,如果一个线程调用共享对象的该方法挂起,没有在指定的timeout ms
时间内被其它线程调用该共享的notify()
或者notifyAll()
方法唤醒,那么该函数会因为超时而返回。 -
如果将
timeout
设置为0,其效果与wait()
方法一致。 -
如果将
timeout
设置为负数,则抛出illegalArgumenException
异常。
1.3.3 wait(long timeout, int nanos)
函数
- 该函数与
wait(long timeout)
函数相同,与其不同之处在于,多了一个nanos
参数,这个参数表示额外时间,即纳秒(范围0-999999)
1.3.4 notify()
函数
-
该函数用于唤醒在共享变量上调用
wait()
系列方法后所挂起的线程,而在一个共享变量上可能会有多个线程在等待,具体唤醒哪个线程是随机的。 -
此外,被唤醒的线程并不是立刻执行,而是必须获取共享对象锁才能继续执行。
1.3.5 notifyAll()
函数
-
该函数与
notify()
函数功能一致,用于唤醒调用wait()
方法后被挂起的线程,但是与之不同的是,notifyAll()
函数会唤醒所有挂起线程,而notify()
函数只唤醒当前线程。 -
实例
/** * @author huangdq * @version 1.0.0 * @ClassName NotifyDemo.java * @Description 线程唤醒 * @createTime 2022年02月25日 15:35:00 */ public class NotifyDemo { // 资源A private static volatile Object resourceA = new Object(); public static void main(String[] args) throws InterruptedException { // 创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { try { // 获取资源A共享资源的监视器锁 synchronized (resourceA) { System.out.println("线程A 获取资源A的锁"); System.out.println("线程A 开始挂起"); resourceA.wait(); System.out.println("线程A 结束挂起"); } } catch (InterruptedException e) { e.printStackTrace(); } } }); // 创建线程B Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA){ try { System.out.println("线程B 获取资源A 锁"); System.out.println("线程B 开始挂起"); resourceA.wait(); System.out.println("线程B 结束挂起"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); // 创建线程C Thread threadC = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA){ try { System.out.println("线程C 获取资源A 锁"); System.out.println("线程C 开始挂起"); resourceA.wait(); System.out.println("线程C 结束挂起"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); // 创建线程D Thread threadD = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA) { System.out.println("线程D 开始唤醒"); // resourceA.notify(); resourceA.notifyAll(); } } }); // 启动线程 threadA.start(); threadB.start(); threadC.start(); Thread.sleep(1000); threadD.start(); // 等待线程结束 threadA.join(); threadB.join(); threadC.join(); threadD.join(); System.out.println("主线程结束......"); } }
1.4 join
方法
-
该方法是等待线程终止的方法。
-
该方法是
Thread
直接提供。 -
该方法是无参且返回值为
void
的方法。 -
实例
/** * @author huangdq * @version 1.0.0 * @ClassName JoinDemo.java * @Description 线程等待demo * @createTime 2022年02月25日 17:21:00 */ public class JoinDemo { public static void main(String[] args) throws InterruptedException { Thread threadOne = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程1 结束!"); } }); Thread threadTwo = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程2 结束!"); } }); threadOne.start(); threadTwo.start(); System.out.println("等待所有子线程结束!"); threadOne.join(); threadTwo.join(); System.out.println("所有线程结束!"); } }
1.5 sleep
方法
-
该方法是
Thread
类中有的一个静态的sleep
方法,当一个执行中的线程调用了该方法,调用线程会暂时让出指定时间的执行权,并且这段时间不参与CPU
的调度。 -
但该线程有拥有的监视器资源,比如锁依旧持有且不让出,指定睡眠结束后,该函数正常返回,线程处于就绪状态,获取
CPU
后正常运行。 -
实例
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author huangdq * @version 1.0.0 * @ClassName SleepDemo.java * @Description 睡眠sleep方法 * @createTime 2022年02月28日 10:15:00 */ public class SleepDemo { private static final Lock lock = new ReentrantLock(); public static void main(String[] args) { // 创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { // 获取独占锁 lock.lock(); try { System.out.println("子线程A 开始睡眠"); Thread.sleep(1000); System.out.println("子线程A 醒来!"); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 释放锁 lock.unlock(); } } }); // 创建线程B Thread threadB = new Thread(new Runnable() { @Override public void run() { // 独占锁 lock.lock(); try { System.out.println("子线程B 开始睡眠"); Thread.sleep(1000); System.out.println("子线程B 醒来!"); } catch (InterruptedException e) { e.printStackTrace(); }finally { // 释放锁 lock.unlock(); } } }); // 启动线程 threadA.start(); threadB.start(); } }
1.6 yield
方法
-
该方法是让线程让出
CPU
的执行权(让出自己剩余时间片),让线程处于就绪状态, 但是并非强制性。 -
我们都知道操作系统为每个线程分配一个时间片来占有
CPU
,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度;而当一个线程调用Thread
类的静态方法yield
时,是在告诉线程调度器自己占有的时间片还没有使用完,自己的部分不想使用,安装线程调度器可以进行下一轮的线程调度了。 -
实例
/** * @author huangdq * @version 1.0.0 * @ClassName YieldDemo.java * @Description yield 线程让出 * @createTime 2022年02月28日 10:58:00 */ public class YieldDemo implements Runnable{ YieldDemo(){ // 创建线程并启动 Thread thread = new Thread(this); thread.start(); } @Override public void run() { for (int i = 0; i < 5; i++){ if ((i % 5) == 0){ System.out.println(Thread.currentThread() + "让出 CPU..."); // 让出自己时间片 // Thread.yield(); } } System.out.println(Thread.currentThread() + "线程 结束"); } public static void main(String[] args) { new YieldDemo(); new YieldDemo(); new YieldDemo(); } }
1.7 线程中断
-
java
线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。 -
线程中断方法:
-
void interrupt()
方法-
中断线程,该方法设置线程中断标志(
true
)并立即返回,该方法仅仅只是设置标准,线程并没有被中断,会继续往下执行。 -
当线程调用
wait
、join
、sleep
等方法而被阻塞,线程A
立即抛出异常而返回。
-
-
boolean isInterrupted()
方法- 该方法检测当期线程是否被中断,中断则返回
true
,否则返回false
。
- 该方法检测当期线程是否被中断,中断则返回
-
boolean interrupted()
方法- 该方法检测当期线程是否被中断,中断则返回
true
,否则返回false
;与boolean isInterrupted()
方法不同的是,该方法如果线程被中断,则会清除中断标志。
- 该方法检测当期线程是否被中断,中断则返回
-
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName InterruptedDemo.java * @Description 线程中断 * @createTime 2022年02月28日 13:34:00 */ public class InterruptedDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { // 如果当前线程被中断,则退出循环 while (!Thread.currentThread().isInterrupted()){ System.out.println(Thread.currentThread() + "hello"); } } }); // 启动子线程 thread.start(); // 主线程休眠1毫秒,以便中断前让子线程输出 Thread.sleep(1); System.out.println("主线程中断线程"); thread.interrupt(); // 等待子线程执行完毕 thread.join(); System.out.println("主线程执行完毕!"); } }
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName InterruptDemo.java * @Description 线程中断异常处理 * @createTime 2022年02月28日 13:47:00 */ public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { try { System.out.println("线程A 开始休眠2000S"); Thread.sleep(2000000); System.out.println("线程A 开始工作!"); } catch (InterruptedException e) { System.out.println("线程A 被中断休眠"); return; } System.out.println("线程A 离开"); } }); threadA.start(); Thread.sleep(1000); threadA.interrupt(); threadA.join(); System.out.println("主线程执行结束!"); } }
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName IsInterrupted.java * @Description 判断线程是否被中断 * @createTime 2022年02月28日 14:00:00 */ public class IsInterruptedDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { for (;;){} } }); // 启动线程 thread.start(); System.out.println("子线程名称:" + thread.getName()); System.out.println("当期线程:" + Thread.currentThread()); // 设置中断标志 thread.interrupt(); // 获取中断标志 System.out.println("isInterrupted:" + thread.isInterrupted()); // true // 获取中断标志并重置(interrupted() 获取的是当期调用线程的中断标志,而非调用线程) System.out.println("interrupted:" + thread.interrupted()); // false // 获取中断标志 System.out.println("isInterrupted:" + Thread.interrupted()); // false // 获取中断标志 System.out.println("interrupted:" + thread.isInterrupted()); // true thread.join(); System.out.println("主线程结束!"); } }
1.8 上下文切换
-
当期线程使用完后,就会处于就绪状态并让出
CPU
让其他的线程占用. -
上下文切换时,需要保存当期线程的执行线程,当再次执行时根据保存的执行现场恢复执行现场。
1.9 线程死锁
1.9.1 线程死锁概念
-
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
-
产生死锁必须具备以下四个条件:
-
互斥条件
-
请求并持有条件
-
不可剥夺条件
-
环路等待条件
-
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName DeadLockDemo.java * @Description 线程死锁 * @createTime 2022年02月28日 15:32:00 */ public class DeadLockDemo { // 创建资源 private static Object resourceA = new Object(); private static Object resourceB = new Object(); public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread() + "获取资源A"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "在等待资源B"); synchronized (resourceB) { System.out.println(Thread.currentThread() + "获取资源B"); } } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resourceB){ System.out.println(Thread.currentThread() + "获取资源B"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "等待获取资源A"); synchronized (resourceA){ System.out.println(Thread.currentThread() + "获取资源A"); } } } }); threadA.start(); threadB.start(); } }
1.9.2 如何避免线程死锁
-
要想避免死锁,只需要破坏掉构造死锁的一个必要条件即可。
-
而产生死锁的必要条件中,只有请求并持有和环路等待条件可被破坏。
-
方法
- 资源申请资源有序性原则
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName DeadLockDemo.java * @Description 线程死锁 * @createTime 2022年02月28日 15:32:00 */ public class DeadLockDemo { // 创建资源 private static Object resourceA = new Object(); private static Object resourceB = new Object(); public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread() + "获取资源A"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "在等待资源B"); synchronized (resourceB) { System.out.println(Thread.currentThread() + "获取资源B"); } } } }); // 产生死锁 // Thread threadB = new Thread(new Runnable() { // @Override // public void run() { // synchronized (resourceB){ // System.out.println(Thread.currentThread() + "获取资源B"); // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // System.out.println(Thread.currentThread() + "等待获取资源A"); // synchronized (resourceA){ // System.out.println(Thread.currentThread() + "获取资源A"); // } // } // } // }); // 破坏死锁 Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA){ System.out.println(Thread.currentThread() + "获取资源B"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "等待获取资源A"); synchronized (resourceB){ System.out.println(Thread.currentThread() + "获取资源A"); } } } }); threadA.start(); threadB.start(); } }
1.10 守护线程与用户线程
-
java
线程分为两类,分别为daemon
线程(守护线程)和user
线程(用户线程)。JVM
启动调用main
函数,main
函数所在线程就是一个用户线程。
-
区别
- 当最后一个非守护线程(用户线程)结束时,
JVM
会正常退出,而不管当前是否有守护线程。
- 当最后一个非守护线程(用户线程)结束时,
-
如何创建守护线程
- 只需要将线程
daemon
参数设置为true
即可。
- 只需要将线程
-
实例
/** * @author huangdq * @version 1.0.0 * @ClassName DaemonDemo.java * @Description 守护线程 * @createTime 2022年02月28日 16:26:00 */ public class DaemonDemo { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { for (;;){} } }); // 设置为守护进程 thread.setDaemon(true); // 启动子线程 thread.start(); // System.out.println(thread.isDaemon()); System.out.println("主线程结束"); } }
1.11 ThreadLocal
-
顾名思义,本地线程。
-
让程序创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量。即创建一个
ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本,而多个线程操作这个变量,实际操作的是自己本地内存里的变量,从而避免线程的安全问题。 -
创建
ThreadLocal
变量后,每个线程都会复制一个变量到自己的本地内存。 -
实例
/** * @author huangdq * @version 1.0.0 * @ClassName ThreadLocalDemo.java * @Description 本地线程demo * @createTime 2022年03月01日 09:16:00 */ public class ThreadLocalDemo { // 创建ThreadLocal变量 static ThreadLocal<String> localVariable = new ThreadLocal<>(); static void print(String str){ // 打印当前线程本地内存中localVariable变量的值 System.out.println(str + ":" + localVariable.get()); // 清除当前线程本地内存中的变量localVariable变量 localVariable.remove(); } public static void main(String[] args) { // 创建线程A Thread threadA = new Thread(new Runnable() { @Override public void run() { // 设置线程中本地变量localVariable的值 localVariable.set("本地线程A"); print("线程A"); System.out.println("线程A移除之后:" + localVariable.get()); } }); //创建线程B Thread threadB = new Thread(new Runnable() { @Override public void run() { // 设置本地线程 localVariable.set("本地线程B"); print("线程B"); System.out.println("线程A移除之后:" + localVariable.get()); } }); // 启动线程 threadA.start(); threadB.start(); } }
-
实现原理
-
当前线程第一次调用
ThreadLocal
的set
和get
才会创建Thread
类的threadLocals
和inheritableThreadLocals
变量,它们都是ThreadLocalMap
类型的变量,ThreadLocalMap
是一个定制化的HashMap
。 -
ThreadLocal
是一种以空间换时间的做法。在每个线程里维护一个ThreadLocal.ThreadLOcalMap
,把数据进行隔离,让每个线程的数据不共享。 -
ThreadLocal
就是一个工具壳,通过set
方法把value
值放入调用线程的threadLocals
里面并存放起来,当调用线程调用它的get
方法时,再从当前线程的threadLocals
变量里面将其拿出来使用。-
如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
threadLocals
变量里面 -
所以当不需要使用本地变量时可以通过调用
ThreadLocal
变量的remove
方法,从当前线程的threadLocals
里面删除该本地变量。
-
-
2.并发基础
2.1 多线程并发
-
概念
-
并发是指单位时间内多个任务同时在执行
-
并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成。所以说并发的多个任务在单位时间内不一定在同时执行。
-
-
共享资源
- 资源能被多个线程所持有或者说多个线程可以去访问该资源。
2.2 共享变量的内存可见性问题
-
概念
- 在
java
内存模型规定,将上游的变量存放在主内存中,当线程使用变量时,将会把主内存里的变量复制到自己的工作空间,或者说是工作内存,而线程读写变量时,操作的是自己工作内存中的变量。
- 在
-
造成内存不可见分析
-
每个线程(每个核)都有自己的一组寄存器和操作控制器以及一级缓存,而有些架构里还有共享的二级缓存。
-
当线程操作共享变量时,首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理后将变量值更新到主内存。
-
2.3 synchronized
关键字
-
java
提供的一种原子性内置锁,可看成同步锁。 -
内置锁
-
在
java
中内置,使用者看不到的锁被称为内部锁,即监视器锁。 -
内置锁是排它锁。
-
-
语义
-
进入
synchronized块
的内存语义是把在synchronized
块内使用到的变量从线程的工作内存中清除,这样在synchronized
块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。 -
退出
synchronized
块的内存语义是把在synchronized
块内对共享变量的修改刷新到主内存。
-
-
问题
- 使用锁太笨重,并且会带来线程上下文的切换开销。
2.4 volatile
关键字
-
该关键字可确保对于一个变量的更新能让其它的线程实时可见;
-
当一个变量被声明为
volatile
时,线程在陷入变量时,不会把值写在其它的地方,而是直接写入主内存中。 -
使用场景
-
写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而volatile不保证原子性。
-
读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。
-
2.5 CAS
操作
2.5.1 什么是CAS
-
CAS
机制是一种数据更新方式。 -
在多线程环境下,对共享变量进行数据更新的有两种模式:
-
悲观锁
-
synchronized
就是最典型的悲观锁实现。 -
悲观锁在阻塞状态与运行状态来回切换比较慢,如果线程之间执行的速度非常快,那么可能在状态切换所需时间比更新还长。
-
-
乐观锁:
-
CAS
机制就是乐观锁典型的实现。 -
乐观锁认为不会出现线程争抢,所以不会对共享数据加锁,但在线程对共享数据进行操作时,会检查共享数据是否被其它线程改变过。如果未改变,则改变共享数据为最新数据,如果发现改变了,则重试,直到最新值为预期值为止。
-
-
2.5.2 原理
-
CAS
有三个核心参数-
主内存中存放的共享变量的值:
V
- 一般情况下这个
V
是内存的地址值,通过这个地址可以获得内存中的值。
- 一般情况下这个
-
工作内存中共享变量的副本值:
A
-
需要将共享变量更新到最新的值:
B
-
-
步骤
-
主存中保存
V
值,线程中要使用V
值要先从主存中读取V
值到线程的工作内存A
中。 -
然后计算后变成
B
值,最后再把B
值写回到内存V
值中。 -
多个线程共用
V
值都是如此操作。 -
CAS
的核心是在将B
值写入到V
之前要比较A
值和V
值是否相同。 -
如果不相同证明此时
V
值已经被其他线程改变,重新将V
值赋给A
,并重新计算得到B
,如果相同,则将B
值赋给V
。
-
2.5.3 存在问题
-
ABA
问题-
解决方案
-
每次操作多加两个值,一个版本号和某个值。
-
jdk
中提供了AtomicStampedReference
类解决ABA
问题。
-
-
-
会消耗较高
CPU
-
只能保证共享变量原子性,不能保证代码块原子性。
2.5.4 使用场景
-
统计网站访问量
-
Atomic
类操作 -
数据库乐观锁更新
2.6 Unsafe
类
2.7 指令重排序
2.8 锁的概念
2.8.1 悲观锁 与 乐观锁
悲观锁与乐观锁是数据库提供的锁机制
-
悲观锁
- 悲观锁指对数据被外界修改保持保守态度,认为数据很容易就会被其它的线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态(加排它锁)。
-
乐观锁
-
乐观锁指对数据被外界修改保持乐观态度,在一般情况下不会造成冲突,访问记录前不会加排它锁,而是进行数据提交更新时,才会对数据冲突与否进行检测。
-
乐观锁一般不会锁机制,一般是在表中添加
version
字段或使用业务状态来实现。
-
2.8.2 公平锁与非公平锁
公平锁与非公平锁根据线程获取锁的抢占机制
-
公平锁
-
公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁,即先到先得。
-
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
-
ReentrantLock pairLock = new ReentrantLock(true)
-
-
非公平锁
-
非公平锁并非先到先得
-
ReentrantLock pairLock = newReentrantLock(false)
-
2.8.3 独占锁与共享锁
根据锁只能被单个线程持有还是同时能被多个线程共同持有,锁可分为独占锁与共享锁。
-
独占锁
-
独占锁保证任何时候都只有一个线程能得到锁。
-
独占锁是一种悲观锁,每次访问资源都要加上互斥锁,但这也限制了并发性。
-
-
共享锁
-
共享锁能同时被多个线程持有。
-
共享锁是一种乐观锁。
-
2.8.4 可重入锁
-
当一个线程要多次或无限次进入自己获取的独占锁。
-
synchronized
内部锁是可重入锁。
2.8.5 自旋锁
- 略
扩展
如何阅读源码
-
了解源码的前世今生。
-
写
demo
,了解源码的具体作用。 -
debug
自己写的demo
-
第一遍,走马观花,了解其具体调用逻辑,以及涉及哪些类。
-
第二遍,了解每个类担任哪些功能,使用了哪些设计模式。
-
第三遍,掌握把类的调用时序图和类图结构画出来,画好后,对时序图分析其调用流程,清楚了解类之间的调用关系,以及相互之间的依赖关系。
-