一、创建线程有几种方式
看了好多博文,都说三种或者三种之上的,其实本质只有两种,有Java源码Thread类的上面的注释为证:
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread.
...
The other way to create a thread is to declare a class that implements the Runnable interface.
1.列举一下大多数博文所说的四种创建线程的方式
(1).继承使用Thread类
public class MyThread extends Thread{
//重写run方法
@Override
public void run() {
System.out.println("继承Thread类创建线程");
}
public static void main(String[] args) {
//创建继承Thread类的对象调用start方法来启动线程
MyThread mythread = new MyThread ();
MyThread .start();
}
}
(2).实现Runnable接口
public class MyThread implements Runnable{
//重写run方法
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
public static void main(String[] args) {
//创建实现Runnable接口的类的对象
MyThread mythread= new MyThread ();
//使用Thread类的匿名对象调用start方法
new Thread(mythread).start();
}
}
发现其实本质还是使用Thread类,只不过线程执行的代码的位置不同。
(3).实现Callable接口,和FutureTask连用
public class UseCallableCreateThread implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000L);
System.out.println("当前线程:" + Thread.currentThread().getName());
return "执行完毕";
}
public static void main(String[] args) {
System.out.println("主线程:" + Thread.currentThread().getName());
FutureTask<String> futureTask = new FutureTask<>(new UseCallableCreateThread());
System.out.println("开始时间戳为:" + System.currentTimeMillis());
String result = null;
try {
new Thread(futureTask).start();
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
}
}
执行结果:
主线程:main
开始时间戳为:1654503947542
当前线程:Thread-0
结束时间戳为:1654503950546,result = 执行完毕
(3.1).分析一下FutureTask和Callable
1).Callable是一个接口类似于Runnable,只不过比Runnable多出了能返回结果和抛出异常
2).Runnable作为Thread的构造方法的参数,就能开启线程,Callable不行,Callable需要借助FutureTask,把Callable作为FutureTask的构造方法的参数构建一个FutureTask的对象,然后再把FutureTask作为Thread的构造方法的参数就能开启线程。FutureTask为什么能够作为Thread的参数,下面详细分析一下FutureTask
3).FutureTask的两个构造方法
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
上面的构造方法是以callable类型为参数
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
可以看到有Runnable为参数的构造,还有一个参数是返回结果result,然后主要是把runnable转成了Callable,所以最后实际还是在操作callable
(4).FutureTask为什么能够作为Thread的参数
这就要看FutureTask的类的关系了:
public class FutureTask<V> implements RunnableFuture<V> {
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
从上面继承关系,可以看出FutureTask实现了Runnable接口,所以FutureTask既能直接作为Thread的参数,也可以交给线程池去执行开启一个线程。
FutureTask还实现了Future接口:
cancel(boolean mayInterruptIfRunning)方法尝试取消执行此任务,实质是调用的Thread的interrupt()方法
isCancelled()如果此任务在正常完成之前取消,则返回 true,实现是通过维护的状态的判断
isDone()如果此任务已完成返回ture
get() 主线程阻塞等待计算完成,然后返回其结果
get(long timeout, TimeUnit unit) 主线程阻塞,如果在给定的时间完成,然后返回结果。
(3.2).Callable和线程池连用
1).使用FutureTask,交给线程池执行,阻塞等待结果返回
public class UseCallableCreateThread implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000L);
System.out.println("当前线程:" + Thread.currentThread().getName());
return "执行完毕";
}
public static void main(String[] args) {
System.out.println("主线程:" + Thread.currentThread().getName());
FutureTask<String> futureTask = new FutureTask<String>(new UseCallableCreateThread());
ExecutorService executorService = Executors.newSingleThreadExecutor();
System.out.println("开始时间戳为:" + System.currentTimeMillis());
executorService.submit(futureTask);
String result = null;
try {
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
}
}
执行结果:
主线程:main
开始时间戳为:1653986152199
当前线程:pool-1-thread-1
结束时间戳为:1653986155204,result = 执行完毕
可以看到开启了一个新的线程,并且主线程通过阻塞式接受异步线程返回的内容。
2).如果直接调用futureTask的run方法会怎样?
public class UseCallableCreateThread implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000L);
System.out.println("当前线程:" + Thread.currentThread().getName());
return "执行完毕";
}
public static void main(String[] args) {
System.out.println("主线程:" + Thread.currentThread().getName());
FutureTask<String> futureTask = new FutureTask<String>(new UseCallableCreateThread());
System.out.println("开始时间戳为:" + System.currentTimeMillis());
String result = null;
try {
futureTask.run();
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
}
}
执行结果:
主线程:main
开始时间戳为:1653990338855
当前线程:main
结束时间戳为:1653990341856,result = 执行完毕
并没有开启线程,只是执行了Callable中的call方法。
3).通过线程池执行,返回Future对象
public class UseCallableCreateThread implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000L);
System.out.println("当前线程:" + Thread.currentThread().getName());
return "执行完毕";
}
public static void main(String[] args) {
System.out.println("主线程:" + Thread.currentThread().getName());
ExecutorService executorService = Executors.newSingleThreadExecutor();
System.out.println("开始时间戳为:" + System.currentTimeMillis());
Future<String> future = executorService.submit(new UseCallableCreateThread());
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
}
}
执行结果:
主线程:main
开始时间戳为:1653991286480
当前线程:pool-1-thread-1
结束时间戳为:1653991289484,result = 执行完毕
可以看到也是开启了一个线程,并且主线程通过阻塞式接受异步线程返回的内容,这个用法的实质和第一个没有区别,这个返回的Future就是FutureTask:
从源码看问题:
1).为什么第一种和第三种本质一样
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
从源码中可以看到无论是传的Runnable(FutureTask)还是Callable最后都会生成一个FutureTask执行execute方法和作为返回值返回。
2).future.get方法如何阻塞等待异步线程执行结果
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
// 如果状态还在进行中,或者刚创建,就阻塞等待
s = awaitDone(false, 0L);
// 调用Report,返回结果或者抛出异常
return report(s);
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
// 状态正常,返回结果
return (V)x;
if (s >= CANCELLED)
// 状态取消,抛出取消异常
throw new CancellationException();
// 抛出程序执行异常
throw new ExecutionException((Throwable)x);
}
awaitDone方法阻塞的线程,这个方法比较复下面分析。FutureTask内部维护了任务进行的状态,当异步任务完成时,会将状态码设置为已完成,如果发生异常,会将状态码设置成对应的异常状态码。
3).自己用Runnable实现一个有返回值的,还阻塞主线程的功能。
public class HadedResultRunnable implements Runnable{
private String result;
private AtomicBoolean finished = new AtomicBoolean(); //并发下的boolean类型使用CAS 和 volatile保证并发安全
public void run() {
try {
Thread.sleep(3000L);
System.out.println("当前线程:" + Thread.currentThread().getName());
this.result = "hello world";
finished.compareAndSet(false, true); // 执行完了就改变flag
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String get() {
while (true) {
if (finished.get()) {
return result;
}
}
}
public static void main(String[] args) throws Exception {
System.out.println("开始时间戳为:" + System.currentTimeMillis());
HadedResultRunnable myRunnable = new HadedResultRunnable();
new Thread(myRunnable).start();
String result = myRunnable.get();
System.out.println("结束时间戳为:" + System.currentTimeMillis() + ",result = " + result);
}
}
(4).使用线程池开启线程
使用线程池的优点:
1.减少资源的消耗。重复利用已经创建的线程,避免频繁的创造和销毁线程,减少消耗。
2.提高响应速度。当执行任务时,不需要去创建线程再来执行,只要调动现有的线程来执行即可。
3.提高了线程的管理性。线程是稀缺资源,使用线程池可以进行统一的分配、调优和监控。
线程池的执行原理:
当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建一个固定大小的线程池。 通过将maximumPoolSize设置为基本上无界的值,例如Integer.MAX_VALUE,您可以允许池容纳任意数量的并发任务。 通常,核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize和setMaximumPoolSize进行动态更改
流程图:
1).通过Executors创建,它提供了四种创建线程池的方法
a.newCachedThreadPool 创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。(线程数可变)
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
它是一个可以无限扩大的线程池;
它比较适合处理执行时间比较小的任务;
corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
b.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
它是一种固定大小的线程池;
corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;
由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
c.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
它接收SchduledFutureTask类型的任务,有两种提交任务的方式:
scheduledAtFixedRate
scheduledWithFixedDelay
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
ScheduledFutureTask(Runnable r, V result, long ns, long period) {
super(r, result);
this.time = ns;
this.period = period;
this.sequenceNumber = sequencer.getAndIncrement();
}
从源码中可以看出虽然接受的是runnable但是后面会把runnable包装成SchduledFutureTask
SchduledFutureTask接收的参数:
time:任务开始的时间
sequenceNumber:任务的序号
period:任务执行的时间间隔
它采用DelayQueue存储等待的任务
DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
DelayQueue也是一个无界队列;
工作线程的执行过程:
工作线程会从DelayQueue取已经到期的任务去执行;
执行结束后重新设置任务的到期时间,再次放回DelayQueue
d.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 它只会创建一条工作线程处理任务;
- 采用的阻塞队列为LinkedBlockingQueue;
注意:不建议使用Executors创建线程池。因为newFixedThreadPool 和newSingleThreadExecutor允许的最大请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。newCachedThreadPoo和newScheduledThreadPool允许的创建线程的最大数量为Integer.MAX_VALUE,从而导致OOM。
内存溢出可能分析:
使用Executors内存溢出示例:
public class ExecutorsTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
执行报错信息:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at thread.ExecutorsTest.main(ExecutorsTest.java:11)
在上面的报错信息中,可以看出是LinkedBlockingQueue.offer这个方法报错了,LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,newFixedThreadPool创建的线程池没有设置队列长度,所以最大长度是Integer.MAX_VALUE,相当于无界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。
线程池四种创建方式示例:
public static void main(String[] args) {
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
// ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newScheduledThreadPool(5);
try {
for (int i = 0; i < 30; i++) {
//使用线程池来创建线程
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown(); //线程池使用完毕后需要关闭
}
}
2).正确姿势:通过ThreadPoolExecutor创建
a.ThreadPoolExecutor 和 Executor 和 Executors的关系
Executors是一个生产线程池的工厂,通过上面已经描述的四种方法生产线程池,但是不推荐使用
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
b.ThreadPoolExecutor的构造方法
ThreadPoolExecutor(int corePoolSize, //核心线程池大小,始终存在
int maximumPoolSize, //最大线程数
long keepAliveTime, //空闲线程等待时间,超时则销毁
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //等待阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) //线程拒绝策略
1.最大线程数如何定义:
1).cpu密集型,逻辑处理器个数
int maxPools= Runtime.getRuntime().availableProcessors();
2).io密集型,判断程序十分耗IO的线程,最大线程池大小应该比这个大
2.等待阻塞队列BlockingQueue主要有两种实现方式:
1).ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
2).LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。
3.最后一个参数是拒绝策略,一共有四种
//new ThreadPoolExecutor.AbortPolicy():达到最大承载量,不再处理,并且抛出异常
//new ThreadPoolExecutor.CallerRunsPolicy():达到最大承载量,从哪来的去哪里
//new ThreadPoolExecutor.DiscardPolicy():达到最大承载量,丢掉任务,但不抛出异常
//new ThreadPoolExecutor.DiscardOldestPolicy():达到最大承载量,尝试与最早执行的线程去竞争,不抛出异常
写了个例子,大家可以尝试改变线程池的参数,例如最大线程数、拒绝策略等
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
int maxPools= Runtime.getRuntime().availableProcessors();
System.out.println(maxPools);
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
maxPools,6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 12; i++) {
//使用线程池来创建线程
//最大承载:maximumPoolSize+workQueue,超过执行拒绝策略
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown(); //线程池使用完毕后需要关闭
}
}
}
2.其实官方的说法只有两种创建线程的方式,我自己感觉实质只有一种
1).先看实现callable接口,然后吧callable作为参数创建FutureTask,上面分析了FutureTask实际上也是实现了Runnable接口,最后还是要交给线程Thread去执行,所以可以把它归为通过实现Runnable接口这一类
2).再看用线程池开启线程,ThreadPoolExecutor的构造方法中有一个类型是线程工厂的参数,这个工厂创建线程的方式也是通过new Thread然后以Runnable为参数:
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
3).我的理解
(1)首先从不同的角度看,会有不同的答案。例如从代码实现,还是从本质上说。
(2)通常我们可以分为两种,分别是实现Runnable接口和继承Thread类。Thread类的注释也是这样表述的。
(3)描述Runnable方式和Thread方式的不同(3个角度)。
(4)其实Thread类实现了Runnable接口,类中实现了run方法。可以发现两种方式在创建线程的本质上是一样的,都是调用Thread对象的start方法,主要区别在于run方法内容的来源不同:Runnable方式最终是调用Ruannble对象target的run方法,而Thread方式则是使用了重写的run方法。
(5)还有其他实现线程的方式,例如线程池也能新建线程,但是细看源码,其本质是Runnable方式。
(6)准确的讲,创建线程只有一种方式,那就是创建Thread类,而实现线程的run方法有两种方式。除此之外,从表面上看线程池、定时器等工具类也可创建线程,但是本质没有变。
二、多线程一些常用的方法
1.join方法
顾名思义就是往线程中添加东西;该方法可以用于临时加入线程,例如A线程在运行过程中,我们可以临时加入一个B线程,这时A线程会阻塞,让这个新加入的B线程运行完成后,A线程再继续运行。
(1).Thread有一个点注意下,在Thread.start()执行后已经开始和被调用线程抢时间片了,例如:
public class ThreadTest {
public static int sum = 0;
public static void main(String[] args) {
Runnable mRunnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
sum++;
}
}
};
Thread thread1 = new Thread(mRunnable);
Thread thread2 = new Thread(mRunnable);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
}
}
这个看似执行结果没有问题:
2000
实际上是有问题的,在thread1.start();执行的时候,thread1就开始和主线程抢时间片,那为啥这个结果还是2000呢,因为现在cpu硬件太好了,还没等抢呢就执行完了,所以我们把数调大点就能看出来问题,把数改成10000后执行:
14767
(2).还有一点就是上面的代码:
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
这种写法并不能起到作用,因为thread1和thread2已经开始运行了,首先它可能已经开始互抢时间片了,其次之后的调用的join也并不能起到想要的作用,因为阻塞的是主线程,并不干扰thread1和thread2的执行,应该这样写:
public class ThreadTest {
public static int sum = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
sum++;
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10000; i++) {
sum++;
}
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
}
}
执行结果:
20000
注意这两句代码不能去掉:
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
因为要阻塞主线程等thread1和thread2执行完后输出打印 sum: System.out.println(sum);才能得到想要的结果。
(3).分析join方法的源码:
/**
* 等待该线程终止的时间最长为millis毫秒,超时时间为0意味着要一直等下去
* @param millis 以毫秒为单位的等待时间
* @throws InterruptedException
*/
public final synchronized void join(long millis)
throws InterruptedException {
//获取启动的时间戳,用于计算当前时间
long base = System.currentTimeMillis();
//当前时间
long now = 0;
if (millis < 0) { //等待时间不能小于0 否则抛出IllegalArgumentException异常终止程序
throw new IllegalArgumentException("timeout value is negative");
}
//判断是否携带阻塞的超时时间,等于0则表示没有设置超时时间
//如果超时时间为0 则意味着一直要等待该线程执行完(无限等待)
if (millis == 0) {
//需要注意的是,如果当前线程未被启动或者已经终止,则isAlive方法返回false
//即意味着join方法不会生效
while (isAlive()) {
//isAlive()方法:判断当前线程是否处于活动状态
//活动状态就是线程已经启动并且尚未终止
wait(0);
}
} else { //设置了超时时间
//需要注意的是,如果当前线程未被启动或者已经终止,则isAlive方法返回false
//即意味着join方法不会生效
while (isAlive()) {,
//计算剩余时间
long delay = millis - now;
if (delay <= 0) { //如果剩余等待的时间小于等于0,则终止等待
break;
}
//等待指定时间
wait(delay);
//获取此次循环执行的时间
now = System.currentTimeMillis() - base;
}
}
}
从join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞,调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在很多层面相当于synchronized(this),this就是Thread本身的实例,有很多人不理解join为什么阻塞的是主线程呢?不理解的原因是阻塞主线程的方法是放在Thread这个实例中,让大家误以为应该阻塞thread线程,其实大家把这个Thread看成一个普通的Object对象锁就很好理解为什么阻塞的是主线程。
2.停止线程的方法:stop、interrupt、isInterrupted
(1).stop方法
已经弃用,因为调用stop方法直接停止线程的方法太暴力,可能导致一些必须需要执行的操作没有执行就关闭了线程,例如,IO的流没有关闭,数据库没有关闭等等。
(2).还有一种自己设置一个boolean类型的flag,当满足条件时调用return或者让线程自己执行完毕后退出
这种方法大多数场景下是没有问题的,但是当一个线程执行了sleep、join、wait等这种方法被阻塞了,这时改变这个flag,线程因为被阻塞了并不会退出,那该怎么办?看下面interrupt、isInterrupted
(3).interrupt、isInterrupted、Thread.interrupted()方法
interrupt():非静态方法,修改线程对象的中断标志为true。
interrupted():静态方法,查看并返回当前线程的中断标志,true为中断状态,false为非中断状态。但是源码中给出解释,当第一次调用时返回当前线程状态,并同时将该线程的中断标志复位为false(线程默认状态为false),可与interrupt()方法结合使用。
isInterrupted():非静态方法,查看并返回对应线程对象的中断标志。
其实Thread类给我们提供了类似这种设置flag判断flag的方式,并且当线程调用sleep、join、wait等方法阻塞时也能够监听到flag的改变,它就是interrupt、isInterrupted,interrupt方法的作用就是给Thread是否退出的flag设置为true,我们通过isInterrupted去判断是否要退出,因为在调用join、sleep方法时必须要抓InterruptedException这个异常,所以当这个interrupt方法调用时,如果线程在阻塞状态就会抛出这个异常,我们就知道线程要关闭了。
1).正常使用interrupt、和isInterrupted方法示例:
public class ThreadTest {
static Thread thread2;
public static void main(String[] args) {
thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("正常执行线程工作");
while (true){
if (thread2.isInterrupted()){
System.out.println("外面想要停止线程");
System.out.println("处理收尾工作例如关闭流");
return;
}
}
}
});
thread2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行interrupt");
thread2.interrupt();
}
}
执行结果:
正常执行线程工作
执行interrupt
外面想要停止线程
处理收尾工作例如关闭流
2).示例如果调用sleep方法也能够通过捕获异常的方法知道想要关闭线程:
public class ThreadTest {
public static void main(String[] args) {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
synchronized (ThreadTest.class){
System.out.println("执行到了sleep了");
Thread.sleep(1000000);
}
} catch (InterruptedException e) {
System.out.println("有代码调用了interrupt方法");
System.out.println("thread2.isInterrupted():" + thread2.isInterrupted());
System.out.println("处理一些扫尾工作,例如关闭流");
break;
}
}
}
});
thread2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行interrupt");
thread2.interrupt();
}
}
执行结果:
执行到了sleep了
执行interrupt
有代码调用了interrupt方法
thread2.isInterrupted():false
处理一些扫尾工作,例如关闭流
注意抛出InterruptedException异常后,会重置interrrupt状态为默认值false。
3). isInterrupted和Thread.interrupted()方法的区别
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
isInterrupted是线程对象实例的方法判断调用这个方法的线程的interrupt的flag,interrupted是线程类方法判断当前线程是否调用了interrupt方法,最后都是调用的本地方法isInterrupted(boolean ClearInterrupted)方法,当最后调用的本地方法isInterrupted参数为true时,会清除interrup这个flag,下一次再调用isInterrupted方法时就是false,如果isInterrupted的参数时false时不会清除interrup的flag,下次调用isInterrupted方法时还是现在的结果。
3.wait,notify,notifyAll
(1).执行原理介绍
当线程0获得到了锁, 成为Monitor的Owner, 但是此时它发现自己想要执行synchroized代码块的条件不满足; 此时它就调用obj.wait方法, 进入到Monitor中的WaitSet集合, 此时线程0的状态就变为WAITING
处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:
1)BLOCKED状态的线程是在竞争锁对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态
2)WAITING状态的线程是获得了对象的锁,但是自身的原因无法执行synchroized的临界区资源需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态
处于BLOCKED状态的线程会在锁被释放的时候被唤醒
处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。然后它会进入到EntryList, 重新竞争锁 (此时就将锁升级为重量级锁)
(2).API介绍
下面的都是Object中的方法;必须要获取到锁对象, 才能通过锁对象来调用
wait(): 让获得对象锁的线程释放锁,并且到waitSet中一直等待
wait(long n) : 当该等待线程没有被notify, 等待时间到了之后, 也会自动唤醒
notify(): 让获得对象锁的线程, 使用锁对象调用notify去waitSet的等待线程中挑一个唤醒
notifyAll() : 让获得对象锁的线程, 使用锁对象调用notifyAll去唤醒waitSet中所有的等待线程
注意:notify方法的执行只是唤醒沉睡的线程,并不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁。
(3).wait/notify的正确使用
为什么 if会出现虚假唤醒?
- 因为if只会执行一次,执行完会接着向下执行if(){}后边的逻辑;
- 而while不会,直到条件满足才会向下执行while(){}后边的逻辑
使用while循环去循环判断一个条件,而不是使用if只判断一次条件;即wait()要在while循环中
(4).通过示例比较体验一下
两个线程,轮流打印A、B,典型的生产者消费者的例子:
1)用wait/notify
public class ThreadTest {
public static void main(String[] args) {
MyObject obj = new MyObject();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
while (true) {
if (obj.flag) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
obj.flag = true;
System.out.println("A");
obj.notify();
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
while (true) {
if (!obj.flag) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
obj.flag = false;
obj.notify();
}
}
}
});
thread1.start();
thread2.start();
}
public static class MyObject {
public volatile boolean flag;
}
}
2).只用一个公共的Flag判断
public class ThreadTest {
public static void main(String[] args) {
MyObject obj = new MyObject();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
if (!obj.flag) {
obj.flag = true;
System.out.println("A");
}
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
if (obj.flag) {
obj.flag = false;
System.out.println("B");
}
}
}
}
});
thread1.start();
thread2.start();
}
public static class MyObject {
public volatile boolean flag;
}
}
两种方式都能达到目的,在知道wait、notify的作用后,我们知道第一种更节省CPU,因为第一种自己不满足条件抢到CPU时间片的时候会执行wait方法自动挂起,这时候就不再参与抢CPU时间片了,而第二种会不断的抢,直到正好碰上满足条件能执行的那个线程。
4.守护线程
(1).守护线程简介和使用
在Java中有两类线程:User Thread(用户线程) 和 Daemon Thread(守护线程)
守护线程的功能非常简单,在其本身是一个线程的同时,主要是为了给其他的线程提供服务,比如说计时器,清空高速缓存等等操作,守护线程具有和被守护线程一样的生命周期(这里并不是说守护线程和被守护线程常常是1-1的关系),当被守护线程死亡,守护线程往往也会死亡,当虚拟机中只剩下守护线程时,虚拟机就会退出,因为此时也没有运行程序的必要了
一个比较通俗的解释:任何一个守护线程都是整个JVM中所有非守护线程的保姆
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。
需要注意的点是:
守护线程的优先级比较低
守护线程要注意考虑关机动作
守护线程应该永远不去访问固有资源,比如说文件或者数据库,因为它会在任何时候甚至一个操作的中间发生中断。
不要给守护线程分担读写逻辑或者计算逻辑,因为无法确定守护线程是否已经完成了工作,但是只要User退出守护线程也会立马结束,对于计算机程序来说这样的程序可能多次运行结果不一样,很显然这对于程序来说是毁灭性的。
操作:
通过thread.setDaemon(true) 将线程转换为守护线程
这个方法必须在thread.start()之前进行调用
示例:
1).守护线程的简单使用,体验一下当所有用户线程退出,守护线程也会自动退出
public static void main(String[] args) {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("thread2线程工作");
}
}
});
System.out.println("主线程工作");
thread2.setDaemon(true);
thread2.start();
System.out.println("主线程结束工作");
}
执行结果:
主线程工作
主线程结束工作
thread2线程工作thread2线程工作thread2...
Process finished with exit code 0
可以看到主线程是唯一的用户线程,它结束了守护线程thread2也就结束了
2).如果还存在一个用户线程,守护线程不会退出
public static void main(String[] args) {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
System.out.print("thread2线程工作");
}
}finally {
System.out.println("finally执行了");
}
}
});
System.out.println("主线程工作");
thread2.setDaemon(true);
thread2.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("thread1线程工作");
}
}
});
thread1.start();
System.out.println("主线程结束工作");
}
这个就不给执行结果了,因为thread1是用户线程,虽然主线程退出了,但是仍有用户线程thread1没有结束,所以守护线程也没有退出
(2).守护线程中finally不一定会调用,所以一些重要的逻辑计算和IO等不要放在守护线程中
public static void main(String[] args) {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
System.out.print("thread2线程工作");
}
}finally {
System.out.println("finally执行了");
}
}
});
System.out.println("主线程工作");
thread2.setDaemon(true);
thread2.start();
System.out.println("主线程结束工作");
}
执行结果:
主线程工作
主线程结束工作
thread2线程工作thread2线程工作thread2....
Process finished with exit code 0
可以看到并没有执行finally语句包裹的逻辑
换个写法不用守护线程:
public class ThreadTest {
static Thread thread2;
public static void main(String[] args) throws InterruptedException {
thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
System.out.print("thread2线程工作");
if (thread2.isInterrupted()){
break;
}
}
}finally {
System.out.println("finally执行了");
}
}
});
System.out.println("主线程工作");
thread2.start();
Thread.sleep(1);
thread2.interrupt();
System.out.println("主线程结束工作");
}
}
执行结果:
主线程工作
thread2线程工作thread2线程...主线程结束工作
finally执行了
5.synchronized关键字
synchronize修饰 | 锁定对象 |
---|---|
方法 | 锁定的是调用者对象 |
代码块 | synchronize(被锁定对象){} |
静态代码块 | 当前class对象 |
6.Thread.yield方法
public static native void yield();
看上面的线程状态转换图,yield方法执行后,是使线程从运行状态变为就绪状态,也就是说线程让出了CPU的执行权,重新参与竞争。
7.超时等待模式——多线程
生产常见场景:调用一个方法时等待一段时间(一般来说是给定一个时间),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
public synchronized Object get(long mills) throws InterruptedException{
long future = System.currentTimeMills()+mills; //超时时刻
long remaining = mills; // 还需要等待多久
//当超时大于0并且result返回值不满足需求
while((result == null) && remaining > 0){ //当remaining < 0 说明已经超时了
wait(remaining);
remaining = future -System.currentTimeMills(); //等待结束后可能,没有获取到锁,这时不是重新计算,而是把之前等待的时间也算上
}
return result;
}
三、锁和CAS机制
1.了解锁
2.偏向锁(自旋)、轻量锁、重量锁
先大体总结一下,这些偏向锁、轻量锁、重量锁是synchronized关键字的锁的优化
(1).偏向锁
1).偏向锁的机制
偏向锁是当有线程试图获取锁的时候,只判断和当前持有锁的线程是不是同一个,如果是就直接执行同步代码块儿,如果不是说明有线程竞争了就升级成轻量锁。
2).为什么优化用到偏向锁
因为在JVM虚拟机运行的时候,大多数情况下是没有多线程竞争的,这种情况下使用偏向锁节省了cpu的资源
(2).轻量锁(自旋)
1).轻量锁的机制
当有多线程竞争,但是竞争不激烈且同步代码块要执行的内容很少的时候,synchronized的优化会选择轻量锁,轻量锁是通过CAS机制来加锁和解锁,竞争线程不断地通过CAS指令去获取锁(自旋),当执行了多次CAS指令还没有获取到锁,说明竞争激烈或者同步代码块儿的代码非常多,这时候就升级成重量级锁
2).为什么优化机制采用轻量锁
因为重量锁会涉及到阻塞挂起和唤醒线程,挂起和唤醒会造成线程的上下文切换,这个是比较耗费cpu时间的,如果竞争不激烈且同步代码块要执行的内容很少的的情况下,我们宁可让竞争线程在竞争到cpu时间片之后做一些无用功,也不要它因为获取不到锁挂起发生线程上下文切换,当然这个有一个程度,如果线程竞争的非常激烈,导致很多线程一直在作无用功,甚至超过了上下文切换的时间的时候,JVM就会把轻量锁升级成重量锁
(3).重量级锁
1).重量锁的机制
当线程竞争激烈,且同步代码块的代码量很大时,synchronized优化机制选择使用重量级锁,它在竞争锁时,如果竞争不到就把自己挂起,等待获得锁的线程执行完同步代码块之后唤醒它,再去竞争。
2).为什么优化机制采用重量级锁
当线程竞争激烈,同步代码块的执行比较耗时的时候,如果还采用轻量锁,消耗的cpu时间甚至会超过挂起和唤醒线程造成的线程上下文切换时间,这时采用重量锁最合适。
偏向锁、轻量锁、重量锁的理解_CallMeJiaGu的博客-CSDN博客_轻量锁 重量锁
了解Java中的自旋锁, 轻量锁, 重量锁(看不懂找我)_多吃核桃会补脑的博客-CSDN博客_java 轻量锁
3.线程生命周期图 5个生命周期,线程状态转换图 7个状态
- 新建:创建线程对象
- 就绪:有执行资格,没有执行权
- 运行:有执行资格,有执行权
- 阻塞:没有执行资格,没有执行权。(同步阻塞/等待阻塞/其他阻塞)由于一些操作让线程处于了该状态。另外一些操作可以把它激活,激活后处于就绪状态。
- 死亡:线程对象变成垃圾,等待被回收
4.死锁
(1).出现死锁的必要条件
1).多个操作者(线程) M >= 2,争取多个资源 N >= 2,而且 N <= M
2).争夺资源的顺序不对
3).拿到资源不放手
死锁代码:
public class DeadLock {
public static void main(String[] args) {
Object res1 = new Object();
Object res2 = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (res1) {
try {
Thread.sleep(1); //因为现在cpu太快了,所以写个等待,让另一个线程竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拿到res1");
synchronized (res2) {
System.out.println("拿到res2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (res2) {
try {
Thread.sleep(1); //因为现在cpu太快了,所以写个等待,让另一个线程竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拿到res2");
synchronized (res1) {
System.out.println("拿到res1");
}
}
}
}).start();
}
}
执行结果:
明显死锁了。
2).如何解决死锁
我们先看这三个必要条件,只要我们能避免一条就不会发生死锁了。
1).多个操作者(线程) M >= 2,争取多个资源 N >= 2,而且 N <= M
2).争夺资源的顺序不对
3).拿到资源不放手
首先看第一条,这个一般是需求就是如此,改不掉,所以我们想办法修改第二第三条。
修改第二条: 我们把争夺资源的顺序改成一样的
public class DeadLock {
public static void main(String[] args) {
Object res1 = new Object();
Object res2 = new Object();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (res1) {//改成一样的
try {
Thread.sleep(1); //因为现在cpu太快了,所以写个等待,让另一个线程竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拿到res1");
synchronized (res2) {
System.out.println("拿到res2");
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (res1) { //改成一样的
try {
Thread.sleep(1); //因为现在cpu太快了,所以写个等待,让另一个线程竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("拿到res2");
synchronized (res2) {
System.out.println("拿到res1");
}
}
}
}).start();
}
}
执行结果:
修改第二条有效果。
修改第三条:
拿到资源不放手
public class DeadLock {
public static void main(String[] args) {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("A线程获尝试获取lock1锁");
if (lock1.tryLock()) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A线程获取到lock1锁");
try {
System.out.println("A线程尝试获取lock2锁");
if (lock2.tryLock()) {
try {
System.out.println("A线程获取到lock2锁");
Thread.sleep(1);
System.out.println("AA");
break;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("A线程释放lock2锁");
lock2.unlock();
}
}
} finally {
System.out.println("A线程释放lock1锁");
lock1.unlock();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("B线程获尝试获取lock2锁");
if (lock2.tryLock()) {
System.out.println("B线程获取到lock2锁");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("B线程获尝试获取lock1锁");
if (lock1.tryLock()) {
System.out.println("B线程获获取到lock1锁");
try {
Thread.sleep(1);
System.out.println("BB");
break;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("B线程释放lock1锁");
lock1.unlock();
}
}
} finally {
System.out.println("B线程释放lock2锁");
lock2.unlock();
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
5.活锁和线程饥饿
(1).活锁
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
(2).线程饥饿
优先级低的线程,总是拿不到执行时间
6.CAS的原理 compare and swap
(1).CAS机制
CAS是现在机器都支持的一条原子操作的指令。
我的理解:CAS指令的工作原理是在修改内存前,内存里的内容是否符合自己的预期(这个预期一般就是未修改之前的旧值),如果是自己预想的值,则把内存的内容进行修改,修改内存的这个操作也是原子操作,而且一次只能一个线程,如果不是自己预期的值,说明已经有线程修改了旧值,就把当前的值为预期,再进行一次CAS操作。
(2).从问题出发理解原子性
-
我一直有个疑惑,CAS的原子性和使用CAS加锁保证线程安全有什么关系?假设有多个线程同时在对同一块内存进行CAS操作的话,那不就有可能出问题吗:两个线程T1,T2同时对同一对象执行CAS操作加锁,V存储同一块内存地址,A当然也是同样旧的预期值,那么这种情况下T1和T2都可以进行更新,那么CAS操作加锁过程就是无效的,因为CAS操作成功后线程就会进入同步块,此时就会有多个线程同时执行同步块中的代码······那这不就会使同步块线程不安全了吗。
-
后来我明白了CAS原子性和线程安全的关系,在多个线程同时CAS的情况下是不会发生多个线程CAS成功的情况的,因为计算机底层实现保证了V指向内存的互斥性和立即可见性,可以理解为CAS操作是底层保证的线程安全
-
首先说结论,一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。
-
处理器实现原子操作有两种做法
-
一是总线锁,在多CPU 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个
LOCK#
信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据 -
二是缓存锁,如果共享内存已经被缓存,那么锁总线没有意义。缓存锁核心是使用了缓存一致性协议,如
MESI
协议-
MSEI表示缓存行的四种状态
-
M(Modify)
表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致 -
E(Exclusive)
表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改 -
S(Shared)
表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致 -
I(Invalid)
表示缓存已经失效
-
-
在
MESI
协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作 -
CPU在读数据时,如果缓存行状态是I,则需要从内存中读取,并把缓存行状态置为S;如果不是I,则可以直接读取缓存中的值,但在此之前必须要等待对其他CPU的监听结果,如果其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存后再读取
-
CPU可以将状态为
M/E/S
的缓存写入内存,其中如果缓存行状态为S,则其他CPU缓存了相同数据的缓存行会无效化
-
-
-
也就是说不会有多个线程同时访问共享变量,而且共享变量更新是对所有线程可见的,所以原子操作是线程安全的。
6.CAS的问题
(1).ABA问题
如果CAS操作想把A变成B,但是另一个线程在这个CAS操作开始之前,做了一次A变成其他值然后又改回了A,这种情况CAS是兼顾不到的,CAS还是会把A改成B。
(2).线程竞争激烈的时候出现的开销问题
(3).只能保证一个共享变量的原子操作
7.AQS AbstractQueuedSynchronizer
(1).AQS简介
队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。
(2).AQS的使用
1).AQS的实现类围绕state变量,来表达是否上锁了
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里由一个int型的state来代表这个状态,当state是0的时候表示没有上锁,当state不为0的时候表示上锁了,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。
为什么getState()、setState(int newState)和compareAndSetState(int expect,int update)能够保证状态的改变是安全的?
getState()、setState(int newState)的操作就是给state进行赋值和取值
private volatile int state;
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
可以看到这个state是由volatile修饰的,所以getState()、setState(int newState)的操作是能够保证线程安全
compaerAndSetState是由CAS实现的,CAS保证了线程安全
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2).AQS的具体使用方法
在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:
锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
3).通过分析ReentrantLock(可重入锁)源码来分析AQS如何使用
1.先分析构造方法
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认的构造方法使用的是非公平锁,带fair参数构造方法是可以控制是使用公平锁还是非公平锁,这个公平锁还是非公平锁怎么体现后面说。
2.分析比较常用的重要的方法
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isLocked();
}
可以看到都是调用的sync这个对象的方法,这个对象是我们自己继承AQS实现的实现类。
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
下面我们通过非公平锁实现来分析具体是如何使用AQS,然后比较公平锁和非公平锁具体实现的区别在哪里
1.具体是如何使用AQS,主要看lock和unlock方法的实现
(1).lock方法分析,已知我们的锁的Lock方法实际是调用的sync的lock方法:
看一下lock方法的调用链
final void lock() {
if (compareAndSetState(0, 1)) // 想把state从0变成1,其实就是想尝试一下抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 这个acquireQueued方法是AQS的底层实现下面分析
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果state是0说明没有线程获取到锁,非公平锁的实现是立即尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//否则就是已经有线程获取到锁了
else if (current == getExclusiveOwnerThread()) {//如果是当前线程获取到锁了,
//还可以再次上一把锁,这就是可重入锁的实现,如果没有这段current == getExclusiveOwnerThread()逻辑就是不可重入锁
int nextc = c + acquires; //那就把当前的维护的state+1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//到这里说明没有抢到锁,返回false,AQS的底层实现是入队列,先自旋,自旋一定次数挂起,等待其他线程释放锁。
return false;
}
分析代码逻辑:
这个lock方法进来先用compareAndSetState(0, 1)的CAS操作抢了一次锁,如果抢到了就把当前线程标记上,如果没有抢到才执行acquire(1),我们来看这个acquire(1)方法是干啥的,调用了tryAcquire(arg),最后调用的是nonfairTryAcquire方法,我们主要分析nonfairTryAcquire方法,上面注释已经分析了,注意可重入锁的实现本质就是当想获取锁的时候,发现当前持有锁的线程就是自己那直接给state+1,在释放锁的时候注意都要释放
(2).unlock方法的分析
调用链:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //主要是这里,进行一个state的--
Node h = head; //下面的逻辑是AQS维护那些要抢锁的线程队列逻辑
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())//只有持有锁的线程才能释放锁
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //如果state减到0说明这个线程彻底释放了锁,彻底释放锁返回true,否则返回false
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
通过分析lock方法和unlock方法我们发现,AQS把具体如果处理等待线程的逻辑隐藏了起来,我们只需要重写抢锁入口和释放锁出口,这些主要的方法就是tryAcquire、tryRelease
3.分析公平锁和非公平锁的实现区别
主要体现在lock方法和tryAcquire方法上
1).非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {//先尝试获取一次锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2).公平锁
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //在等待队列是空的时候才去尝试获取锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看到非公平锁就是,想要获取锁的时候,直接先尝试获取一次,获取不到再入队列等待,而公平锁是先入队列等待,轮到它获取锁的时候才去获取。
(3).AQS的本质原理
1).原理分析CLH队列锁
CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。
CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。
当一个线程需要获取锁时:
1.创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用
2.线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred
线程B需要获得锁,同样的流程再来一遍
3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)
4.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点
如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。
CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。
Java中的AQS是CLH队列锁的一种变体实现。
2).源码解析AQS
(4).AQS实现的共享锁,典型的例子读写锁
我们知道 ReentrantLock 的同步状态和重入次数,是直接用 state 值来表示的。那么,现在我需要读和写两把锁,怎么才能用一个 int 类型的值来表示两把锁的状态呢?并且,锁是可重入的,重入的次数怎么记录呢?
别急,下面一个一个说。
怎么用一个 state 值表示读、写两把锁?
state 是一个 32 位的 int 值,读写锁中,把它一分为二,高 16 位用来表示读状态,其值代表读锁的线程数,如图中为 3 个,低 16位表示写状态,其值代表写锁的重入次数(因为是独占锁)。 这样,就可以分别计算读锁和写锁的个数了。其相关的属性和方法定义在 Sync 类中。
static final int SHARED_SHIFT = 16;
//表明读锁每增加一个,state的实际值增加 2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//写锁的最大重入次数,读锁的最大个数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//持有读锁的线程个数,参数如的 c 代表 state值
//state 的32位二进制位,无符号右移 16位之后,其实就是高16位的值
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//写锁数量,即写锁的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
读锁的个数计算比较简单,直接无符号右移 16 位即可。我们看下写锁的重入次数是怎么计算的。先看下 EXCLUSIVE_MASK 这个值,是 (1 << 16) - 1,我们用二进制表示计算过程为:
// 1的二进制
0000 0000 0000 0000 0000 0000 0000 0001
// 1左移 16位
0000 0000 0000 0001 0000 0000 0000 0000
//再减 1
0000 0000 0000 0000 1111 1111 1111 1111
//任何一个 32位二进制数 c,和以上值做 “与” 运算都为它本身 c 的低 16 位值
//这个不用解释了吧,这个不会的话,需要好好补充一下基础知识了。。。
锁的重入次数是怎么计算的?
写锁比较简单,直接用计算出来的低16位值就可以代表写锁的重入次数。
读锁,就比较复杂了,因为高16位只能表示持有共享锁的线程个数,实在是分身乏术啊。所以,在 Sync 内部,维护了一个类,用来表示每个线程重入的次数,
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
这里边定义了一个计数器来表示重入次数,tid 来表示当前的线程 id 。但是,这样还不够,我们需要把 HoldCounter 和 线程绑定,这样才可以区分出来每个线程分别持有的锁个数(重入次数),这就需要用到 ThreadLocal 了。
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
//重写此方法,可以在 ThreadLocal 没有当前线程计数的情况下,
//直接使用 的 get 方法,初始化一个,而不必 new 一个对象
public HoldCounter initialValue() {
return new HoldCounter();
}
}
除此之外,Sync 中还定义了一些其他和读锁相关的属性,
//保存了当前线程重入的读锁次数,当重入次数减到 0 时移除
//移除应该是为了性能着想,因为可以随时通过 get 方法初始化 HoldCounter
private transient ThreadLocalHoldCounter readHolds;
//保存了最近一个获取读锁成功的线程计数,这个变量的目的是:
//如果最后一个获取到读锁的线程重复获取读锁,那么就可以直接拿来用,而不用更新。
//相当于缓存,提高效率
private transient HoldCounter cachedHoldCounter;
//第一个获取读锁的线程
private transient Thread firstReader = null;
//第一个获取读锁的线程计数
private transient int firstReaderHoldCount;
//这两个参数,是为了效率问题,当只有一个线程获得读锁时,就避免了查找 readHolds
基本知识讲完啦,那么接下来就是锁的获取和释放了。先说下写锁吧,因为有上一篇独占锁的基础了,理解起来比较容易。
写锁的获取
写锁的获取从 lock 方法开始,
//ReentrantReadWriteLock.WriteLock.lock
public void lock() {
sync.acquire(1);
}
//AbstractQueuedSynchronizer.acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//公平锁和非公平锁调用的是同一个方法,在 Sync 类中定义
//ReentrantReadWriteLock.Sync.tryAcquire
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//获取同步状态 state
int c = getState();
//写锁状态
int w = exclusiveCount(c);
//如果同步状态不为 0,说明有线程获得了读锁或写锁
if (c != 0) {
//如果同步状态不为 0 ,并且写锁状态为 0,说明了读锁被占用,因读写锁互斥,故返回 false
//若写锁状态不为 0,并且不是当前线程获得了写锁,则不能重入,返回 false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//如果超过了最大写锁数量,则抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//若走到这一步,说明当前线程重入了,则计算重入次数,返回true
setState(c + acquires);
return true;
}
//到这说明 c 为 0,读锁和写锁都没有被占用
//如果写锁应该被阻塞或者 CAS 获取写锁失败,则返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//把当前线程设为独占锁的所有者
setExclusiveOwnerThread(current);
return true;
}
写锁的释放
同理,写锁的释放从 unlock 方法开始,
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//若独占锁的持有者不是当前线程,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//每次释放,state 减 1
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
可以看到,写锁的获取和释放和 ReentrantLock 的基本思想是差不多的。下面,着重讲解读锁的获取和释放,相对比较复杂。
读锁的获取
tryAcquireShared
从 ReadLock.lock 方法开始,
public void lock() {
//调用 AQS 的方法
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
//如果 tryAcquireShared 方法返回小于 0,说明获取读锁失败
if (tryAcquireShared(arg) < 0)
//以共享模式加入同步队列,再自旋抢锁
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//如果有线程获取到了写锁,并且不是当前线程,则返回 -1 。
//这是因为,如果线程先获得了写锁,是可以重入再次获取读锁的,此为锁降级。
//否则不可重入。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//读锁数量
int r = sharedCount(c);
//如果同时满足以下三个条件(读线程不应该被阻塞,读锁数量小于最大数量限制,CAS成功),
//则说明获取读锁成功,返回 1。然后再设置相关属性的值。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果读锁状态为 0,说明还没有其他线程获取到读锁
if (r == 0) {
//就把当前线程设置为第一个获取到读锁的线程
firstReader = current;
//第一个读线程计数设置为 1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//如果当前线程是第一个获取读锁的线程,则重入,计数加 1
firstReaderHoldCount++;
} else { //读锁状态不为 0,并且当前线程不是 firstReader
//最近一个成功获取到读锁的线程计数器
HoldCounter rh = cachedHoldCounter;
//如果计数器为空,或者计数器的 tid不是当前线程 id,说明有两种情况
//1.rh 还未被任何线程设置,此时只有 firstReader 一个线程获取到了读锁。
//2.rh 已经被设置了,并且不是当前线程,说明在当前线程之前,除了 firstReader,
//还有其他线程获取到了读锁,那么当前线程就是第三个获取到读锁的(至少)。
if (rh == null || rh.tid != getThreadId(current))
//不管哪种情况,都需要创建并初始化当前线程的计数器,并赋值给 cachedHoldCounter
//因为,当前线程是此时最后一个获取到读锁的线程,需要缓存下来
cachedHoldCounter = rh = readHolds.get();
//如果当前线程是最近一个获取到读锁的线程,并且计数为0,
else if (rh.count == 0)
//就把 rh 线程持有锁的次数信息,放入到本地线程 readHolds
readHolds.set(rh);
//最后把计数加 1
rh.count++;
}
return 1;
}
//若以上三个条件任意一个不满足,则调用此方法,再次全力尝试获取锁
return fullTryAcquireShared(current);
}
fullTryAcquireShared 这个方法和 tryAcquireShared 方法非常相似,只是多了一个自旋的过程,直到返回一个确定值(-1或1),才结束。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//自旋,直到返回一个确定值(1或 -1)
for (;;) {
int c = getState();
//如果写锁状态不为0,说明有线程获取到了写锁
if (exclusiveCount(c) != 0) {
//获取到写锁的线程不是当前线程,则返回 -1
if (getExclusiveOwnerThread() != current)
return -1;
//这里省略了else,到这里说明了当前线程获取到了写锁,因此需要做锁降级处理,
//把写锁降级为读锁。因为如果不这样做的话,线程就会阻塞到这,会导致死锁。
//然后跳转到 ①处继续执行
//===========//
} else if (readerShouldBlock()) { //写锁空闲,并且读锁应该阻塞,说明 head.next正在等待获取写锁
//尽管读锁应该阻塞,但是此处也不应该立即阻塞,因为有可能存在读锁重入,需要再确认一下。
if (firstReader == current) {//当前线程是第一个读锁,可重入
// 将跳转到 ①处
} else {
if (rh == null) { //第一次循环进来时肯定为 null
rh = cachedHoldCounter; //取到缓存中最后一次获取到读锁的计数器
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
//计数为 0,说明当前线程没有获取到过读锁
if (rh.count == 0)
//为了性能考虑,如果计数为 0,需要把它移除掉
readHolds.remove();
}
}
//走到这,说明当前线程不是 firstReader,也没有获取到过读锁,不符合重入条件,
//那么就确定需要阻塞,只能去排队了,返回 -1 。
if (rh.count == 0)
return -1;
}
}
// ①处
//如果读锁数量达到了 MAX_COUNT,则抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS获取读锁,和 tryAcquireShared 的处理逻辑一样,不再赘述
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
doAcquireShared
如果 tryAcquireShared 最终还是失败了,那么就执行 doAcquireShared 方法。
private void doAcquireShared(int arg) {
//以共享模式加入同步队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//如果当前节点的前驱节点是头结点,再次尝试获取读锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//把当前节点设置为头结点,并把共享状态传播下去
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//获取读锁失败,判断是否可挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
//旧的头结点
Node h = head;
//把当前 node 设置为新的头结点
setHead(node);
//propagate 是 tryAcquireShared 方法的返回值
//若大于0,或者旧的头结点为空,或者头结点的 ws 小于0
//又或者新的头结点为空,或者新头结点的 ws 小于0,则获取后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//没有后继节点或者后继节点是共享节点,就执行唤醒
if (s == null || s.isShared())
//释放掉资源,并唤醒后继节点,稍后讲解
doReleaseShared();
}
}
读锁的释放
tryReleaseShared
从 ReadLock.unlock方法开始,
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//当前线程为第一个读线程
if (firstReader == current) {
//若 firstReader 的计数为1,则把它置为 null
if (firstReaderHoldCount == 1)
firstReader = null;
else
//否则,计数减 1,说明重入次数减 1
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
//如果当前线程的计数小于等于 1,则移除
readHolds.remove();
if (count <= 0)
//若计数小于等于 0,则抛出异常
throw unmatchedUnlockException();
}
//计数减 1
--rh.count;
}
for (;;) {
int c = getState();
//读锁状态减 1,其实就是state值减 65536
//因为高16位的读锁实际值,在state中的表现就是相差 65536
int nextc = c - SHARED_UNIT;
// CAS 设置 state 最新状态
if (compareAndSetState(c, nextc))
//如果读锁状态减为 0,就返回true
//释放读锁对其它读线程没有任何影响,
//但是如果读、写锁都空闲,就可以允许等待的写线程继续执行
return nextc == 0;
}
}
doReleaseShared
如果 tryReleaseShared 方法返回 true,说明读锁释放成功,需要唤醒后继节点,
private void doReleaseShared() {
for (;;) {
//头结点
Node h = head;
//说明队列中至少有两个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果头结点的 ws 为 -1 ,则 CAS 把它设置为 0,因为唤醒后继节点后,
//它就不需要做什么了。失败继续自旋尝试
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// CAS 成功,则唤醒后继节点
unparkSuccessor(h);
}
//如果 ws 为 0,则把它设置为 -3 ,表明共享状态可向后传播,失败则继续自旋尝试
//后来我一直在想,为什么需要设置一个 PROPAGATE 这样的状态呢,但是还没头绪
//可以看下这篇文章分析,或许有一定的参考价值:
//https://www.cnblogs.com/micrari/p/6937995.html
//只能说 Doug Lea 大神的逻辑真是太缜密了,等我以后想明白了,再补充吧。
//可以暂时先理解为,这就是一个无条件传播的标志
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果此刻 h 等于头结点,说明头结点未改变,则跳出整个循环
//否则,说明头结点被其他线程修改过了,则继续下一次的循环判断
if (h == head) // loop if head changed
break;
}
}