不可变对象设计(String保护性拷贝)、final的使用、享元模式
不可变类设计String
如果一个对象在不能够修改器内部状态(属性),那么它就是线程安全的,因为不存在并发修改
类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
不可变
- 如果一个对象在
不能够修改其内部状态(属性)
,那么它就是线程安全
的,因为不存在并发修改
。
日期转换的问题
问题提出
下面代码在运行时,由于SimpleDateFormat不是线程安全的
``
package com.finaltest;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author HillCheung
* @version 1.0
* @date 2021/4/15 18:58
*/
@Slf4j
public class Test {
public static void main(String[] args) {
SimpleDateFormat sdf =new SimpleDateFormat("YYYY-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
log.debug("{}",sdf.parse("1951-04-21"));
} catch (ParseException e) {
log.error("{}",e);
}
}).start();
}
}
}
- 有很大几率出现
java.lang.NumberFormatException
或者出现不正确的日期解析结果,例如:
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.finaltest.Test.lambda$main$0(Test.java:20)
at java.lang.Thread.run(Thread.java:748)
-
思路:使用同步锁
这样虽能解决问题,但是带来的是
性能
上的损失,并不算很好,加锁耗性能
@Slf4j
public class Test2 {
public static void main(String[] args) {
SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
synchronized (sdf){
try {
log.debug("{}",sdf.parse("2020-12-19"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
- 思路-使用JDK8中
不可变日期格式化类
如果一个对象在不能工修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改!
例如:DateTimeFormatter
@Slf4j
public class TestDateTimeFormatter {
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor date = dtf.parse("2020-12-29");
log.debug("{}", date);
}).start();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JbjjdEco-1627542088297)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210415190619727.png)]
不可变对象
,实际是另一种避免竞争
的方式。
final使用
- Integer、Double、String、DateTimeFormatter以及基本类型包装类, 都是使用
final
来修饰的 - 另一个大家更为熟悉的
String 类
也是不可变的,以它为例,说明一下不可变类设计的要素
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // 在JDK9 使用了byte[] 数组
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
- 发现该类、类中所有属性都是
final
的,属性用 final 修饰保证了该属性是只读
的,不能修改,类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
。
保护性拷贝
- 使用字符串时,也有一些跟修改相关的方法啊,比如
substring、replace
等,那么下面就看一看这些方法是 如何实现的,就以 substring 为例:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 上面是一些校验,下面才是真正的创建新的String对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
- 发现其方法最后是调用
String的构造方法创建了一个新字符串
,在进入这个构造看看,是否对final char [] value 做出了修改;结果发现也没有,构造新字符串对象时,会出现新的 char[] value,对内容进行复制。 - 这种通过创建副本对象来避免共享的手段称之为
[保护性拷贝(defensive copy)]
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 上面是一些安全性的校验,下面是给String对象的value赋值,新创建了一个数组来保存String对象的值
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
final原理
-
设置final变量的原理
- 理解了
volatile原理(读写屏障)
,在对比fianl的实现就比较简单了。
- 理解了
public class TestFinal {
final int a = 20;
}
字节码
0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: bipush 207: putfield #2 // Field a:I <-- 写屏障10: retu
- 发现final变量的复制也会通过 putfield指令来完成,同样在这条指令之后也会加入
写屏障
;保证在其他线程读到它的值不会出现未0的情况。
享元设计模型
-
简介定义英语名称: Flyweight pattern,
重用数量有限的同一类对象。
- 结果型模式
-
享元模式的提现
- 在JDK中
Boolean,Byte,Short,Integer,Long,Character
等包装类提供了valueOf
方法,例如Long的valueOf
会缓存-128-127
之间的Long对象, 在这个范围之间会重用对象,大于这个范围,才会创建Long对象
public static Long valueOf(long l) { final int offset = 128; if (l >= -128 && l <= 127) { // will cache return LongCache.cache[(int)l + offset]; } return new Long(l);}
- 在JDK中
注意:
- Byte、Short、Long缓存的范围都是
-128-127
- Character的缓存的范围是
0-127
- Boolean缓存了
TRUE
和FALSE
- Integer的默认范围是-128-127,最小值不能变,但最大值可以通过调整虚拟机参数
"-Djava.lang.Integer.IntegerCache.high"来改变
- Sting串池
- BigDecimal,BigInteger
实现 简单的连接池
例如: 一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能就会受到极大的影响。这时候预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还会连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
public class Test2 { public static void main(String[] args) { /*使用连接池*/ Pool pool = new Pool(2); for (int i = 0; i < 5; i++) { new Thread(() -> { Connection conn = pool.borrow(); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } pool.free(conn); }).start(); } }}@Slf4j(topic = "guizy.Pool")class Pool { // 1. 连接池大小 private final int poolSize; // 2. 连接对象数组 private Connection[] connections; // 3. 连接状态数组: 0 表示空闲, 1 表示繁忙 private AtomicIntegerArray states; // 4. 构造方法初始化 public Pool(int poolSize) { this.poolSize = poolSize; this.connections = new Connection[poolSize]; this.states = new AtomicIntegerArray(new int[poolSize]);//使用AtomicIntegerArray保证states的线程安全 for (int i = 0; i < poolSize; i++) { connections[i] = new MockConnection("连接" + (i + 1)); } } // 5. 借连接 public Connection borrow() { while (true) { for (int i = 0; i < poolSize; i++) { // 获取空闲连接 if (states.get(i) == 0) { if (states.compareAndSet(i, 0, 1)) {//使用compareAndSet保证线程安全 log.debug("borrow {}", connections[i]); return connections[i]; } } } // 如果没有空闲连接,当前线程进入等待, 如果不写这个synchronized,其他线程不会进行等待, // 一直在上面while(true), 空转, 消耗cpu资源 synchronized (this) { try { log.debug("wait..."); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 6. 归还连接 public void free(Connection conn) { for (int i = 0; i < poolSize; i++) { if (connections[i] == conn) { states.set(i, 0); synchronized (this) { log.debug("free {}", conn); this.notifyAll(); } break; } } }}class MockConnection implements Connection { private String name; public MockConnection(String name) { this.name = name; } @Override public String toString() { return "MockConnection{" + "name='" + name + '\'' + '}'; } // Connection 实现方法略}
运行结果如下:
22:01:07.000 guizy.Pool [Thread-2] - wait... 22:01:07.000 guizy.Pool [Thread-0] - borrow MockConnection{name='连接1'} 22:01:07.005 guizy.Pool [Thread-4] - wait... 22:01:07.000 guizy.Pool [Thread-1] - borrow MockConnection{name='连接2'} 22:01:07.006 guizy.Pool [Thread-3] - wait... 22:01:07.099 guizy.Pool [Thread-0] - free MockConnection{name='连接1'} 22:01:07.099 guizy.Pool [Thread-2] - wait... 22:01:07.099 guizy.Pool [Thread-3] - borrow MockConnection{name='连接1'} 22:01:07.099 guizy.Pool [Thread-4] - wait... 22:01:07.581 guizy.Pool [Thread-3] - free MockConnection{name='连接1'} 22:01:07.582 guizy.Pool [Thread-2] - borrow MockConnection{name='连接1'} 22:01:07.582 guizy.Pool [Thread-4] - wait... 22:01:07.617 guizy.Pool [Thread-1] - free MockConnection{name='连接2'} 22:01:07.618 guizy.Pool [Thread-4] - borrow MockConnection{name='连接2'} 22:01:07.955 guizy.Pool [Thread-4] - free MockConnection{name='连接2'} 22:01:08.552 guizy.Pool [Thread-2] - free MockConnection{name='连接1'}
共享模型之工具
线程池
ThreadPoolExecutor
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q1AfwwCp-1627542088300)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210415204710499.png)]
1) 线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBqliiW6-1627542088302)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210415204732043.png)]
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl
中,目的是将线程池状态与线程个数合二为一
,这样就可以用一次 cas 原子操作进行赋值
// c 为旧值, ctlOf 返回结果为新值ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们private static int ctlOf(int rs, int wc) { return rs | wc; }
2) 构造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize
:核心线程数目(最多保留的线程数)maximumPoolSize
:最大线程数目keepAliveTime
:生存时间-针对救急线程unit时间单位
:针对救急线程workQueue
:阻塞队列threadFactory
:线程工厂-可以为线程创建时起一个好名字handler
:拒绝策略
工作方式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FhiTD0Nv-1627542088306)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210415205319786.png)]
-
线程池刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
-
当线程数达到
CorePoolSize
并没有线程空闲
,这时加入任务,新加入的任务会被加入到workQueue对了排队,直至有空闲的线程。 -
如果队选择了有界队列,那么任务超过了队列大小时,会创建
maximumPoolSize -corePoolSize
数目的线程来救急。 -
如果线程到达了
maximumPoolSize
仍然有新任务时这是会执行拒绝策略
,拒绝策略jdk提供了4中实现,其他著名的框架也提供了实现-
AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略CallerRunsPolicy 让调用者运行任务
-
DiscardPolicy 放弃本次任务
-
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
-
Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
便定位问题
-
Netty 的实现,是创建一个新线程来执行任务
-
ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
-
PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
-
-
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZDYMYuW-1627542088308)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210415205745755.png)]
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}
特点
核心线程数==最大线程数
(没有救急线程被创建),因此也无需超时时间阻塞队列是无界的
,可以放任意数量的任务。
小结 适用于任务量已知,相对耗时的任务
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());}
特点
核心线程数是
,最大线程是Integer.MAX_VALUE
,救急线程的空闲时间是60s,意味着- 全部都是救急线程(60s可以回收)
- 救急线程可以无限创建
- 对了采用了
SynchronousQueue
实现特点是: 它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
@Slf4jpublic class SynchronousQueueTest { @SneakyThrows public static void main(String[] args) { SynchronousQueue<Integer> integers =new SynchronousQueue<>(); new Thread(()->{ try { log.debug("putting{}", 1); integers.put(1); log.debug("{}puuted...", 1); log.debug("putting。。。{}", 2); integers.put(2); log.debug("{},putted...", 2); }catch (InterruptedException e){ e.printStackTrace(); } },"t1").start(); Thread.sleep(1000); new Thread(()->{ try { log.debug("talking{}",1); integers.take(); }catch (InterruptedException e){ e.printStackTrace(); } },"t2").start(); Thread.sleep(1000); new Thread(()->{ try { log.debug("taking{}",2); integers.take(); }catch (InterruptedException e){ e.printStackTrace(); } },"t3").start(); }}结果如下:DEBUG [t1] - putting1DEBUG [t2] - talking1DEBUG [t1] - 1puuted...DEBUG [t1] - putting。。。2DEBUG [t3] - taking2DEBUG [t1] - 2,putted...
评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));}
使用场景:
希望多个任务排队执行。线程数
固定为1,任务数
多于1时,**会放入无界队列排队。**任务执行完毕,这唯一的线程也不会释放。
区别:
-
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
-
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService 应用的是
装饰器模式
,只对外暴露了 ExecutorService 接口
,因此不能调用 ThreadPoolExecutor 中特有的方法
- FinalizableDelegatedExecutorService 应用的是
-
Executors.newFixedThreadPool(1) 初始时为1
,以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
提交任务
// 执行任务void execute(Runnable command);// 提交任务 task,用返回值 Future 获得任务执行结果<T> Future<T> submit(Callable<T> task);// 提交 tasks 中所有任务<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;// 提交 tasks 中所有任务,带超时时间<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
关闭线程池
shutdown
/*线程池状态变为 SHUTDOWN- 不会接收新任务- 但已提交任务会执行完- 此方法不会阻塞调用线程的执行*/void shutdown();
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(SHUTDOWN); // 仅会打断空闲线程 interruptIdleWorkers(); onShutdown(); // 扩展点 ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等) tryTerminate();}
shutdownNow
/*线程池状态变为 STOP- 不会接收新任务- 会将队列中的任务返回- 并用 interrupt 的方式中断正在执行的任务*/List<Runnable> shutdownNow();
public List<Runnable> shutdownNow() { 其它方法 * 模式之 Worker Thread /* 8) 任务调度线程池 在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。*/ List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(STOP); // 打断所有线程 interruptWorkers(); // 获取队列中剩余任务 tasks = drainQueue(); } finally { mainLock.unlock(); } // 尝试终结 tryTerminate(); return tasks; }
模式之 Worker Thread
在线程调度线程池
功能加入之前,可以使用java.util.Timer
来实现定时功能,Timer的优点就在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都会影响到之后的任务。
@Slf4jpublic class TestTimer { public static void main(String[] args) { Timer timer =new Timer(); TimerTask task1 = new TimerTask() { @SneakyThrows @Override public void run() { log.debug("task 1"); Thread.sleep(2000); } }; TimerTask task2 = new TimerTask() { @Override public void run() { log.debug("task 2"); } }; //使用timer添加两个文物,希望它们都在1s后执行 //但由于timer内只有一个线程顺序执行队列中的任务,因为 任务1的延时,会影响任务2的执行 timer.schedule(task1,1000); timer.schedule(task2,1000); }} c.TestTimer [main] - start... 20:46:10.447 c.TestTimer [Timer-0] - task 1 20:46:12.448 c.TestTimer [Timer-0] - task 2
改用ScheduleExecutorService
改写:
executor.schedule(() -> { System.out.println("任务1,执行时间:" + new Date()); try { Thread.sleep(2000); } catch (InterruptedException e) { }}, 1000, TimeUnit.MILLISECONDS);executor.schedule(() -> { System.out.println("任务2,执行时间:" + new Date());}, 1000, TimeUnit.MILLISECONDS);任务1,执行时间:Thu Jan 03 12:45:17 CST 2019 任务2,执行时间:Thu Jan 03 12:45:17 CST 2019
评价整个线程池表现为:线程数固定,任务数多余线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会释放。用来执行延迟或反复执行的任务。
tomcat线程池
Tomact在那里用到了线程池呢?
LimitLatch
用来限流,可以控制最大连接个数,类似JUC中的Semaphore
Acceptor
只负责【接受新的socket连接】- Poller只负责听socket channel 是否有【可读I/O事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
Fock/Join
概念
- Fork/Join是JDk1.7加入的新的线程池实现,它体现的是一种分治思想,
适用于能够进行任务拆分的CPU密集型运算
。 - 所谓的任务拆分,
是将一个大任务差费为算法上相同的小任务,直至不能拆分可以直接求解
。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。 - Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提高了运算效率
- Fork/Join默认会创建与CPU核心数大小相同的线程池
使用
提交给Fork/Join线程池的任务需要继承RecursiveTask(有返回值)
或RecursiveAction(没有返回值)
,例如下面定义了一个对1-n之间的整数求和的任务
int n ;public AddTask(int n ){ this.n=n;}@Overrideprotected Integer compute() { //如果n已经为1,可以求得结果了。 if(n==1){ log.debug("join(() {}",n); return n; } //将任务拆分(fork) AddTask t1 =new AddTask(n-1); t1.fork(); log.debug("fork() {} + {}",n,t1); //合并结果 int result = n+t1.join(); log.debug("joing() {} + {} = {}",n,t1,result); return result;}public static void main(String[] args) { ForkJoinPool pool =new ForkJoinPool();; System.out.println(pool.invoke(new AddTask(5)));}DEBUG [ForkJoinPool-1-worker-3] - fork() 3 + AddTask(n=2)DEBUG [ForkJoinPool-1-worker-1] - fork() 5 + AddTask(n=4)DEBUG [ForkJoinPool-1-worker-4] - fork() 2 + AddTask(n=1)DEBUG [ForkJoinPool-1-worker-2] - fork() 4 + AddTask(n=3)DEBUG [ForkJoinPool-1-worker-5] - join(() 1DEBUG [ForkJoinPool-1-worker-4] - joing() 2 + AddTask(n=1) = 3DEBUG [ForkJoinPool-1-worker-3] - joing() 3 + AddTask(n=2) = 6DEBUG [ForkJoinPool-1-worker-2] - joing() 4 + AddTask(n=3) = 10DEBUG [ForkJoinPool-1-worker-1] - joing() 5 + AddTask(n=4) = 1515Process finished with exit code 0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UI883qeI-1627542088309)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210416210459623.png)]
JUC
AQS原理
概述
- 全程是
AbstractQueuedSynchronizer
,是阻塞式锁和相关的同步器工具的框架
特点:
- 用
state
属性来表示资源的状态(分独占模式和共享模式
),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
getState
:-获取state状态setState
:-设置state状态compareAndSetState
-cas机制设置state状态独占模式
是只有一个线程能够访问资源,而共享模式
可以允许多个线程访问资源
- 提供了基于
FIFO
的等待队列,类似于Monitor
的EntryList
条件变量
来实现等待、唤醒机制,支持多个条件变量,类似于Monitor
的WaitSet
子类主要实现这样的一些方法(默认抛出 UnsupporteOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusivey
获取锁的姿势
// 如果获取锁失败if (!tryAcquire(arg)) { // 入队, 可以选择阻塞当前线程 park unpark }
释放锁的姿势
// 如果释放锁成功if (tryRelease(arg)) { // 让阻塞线程恢复运行 }
实现不可重入锁
自定义同步器
@Slf4jfinal public class MySync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int acquire) { if(acquire==1){ if(compareAndSetState(0,1)){ setExclusiveOwnerThread(Thread.currentThread()); return true; } } return false; } @SneakyThrows @Override protected boolean tryRelease(int acquire) { if(acquire==1){ if(getState()==0){ throw new IllegalAccessException(); } setExclusiveOwnerThread(null); setState(0); return true; } return false; } @Override protected boolean isHeldExclusively() { return getState()==1; } protected Condition newcondition(){ return new ConditionObject(); }}
自定义锁
有了自定义同步器,很容易复用AQS,实现一个功能完备的自定义锁
class MyLock implements Lock { static MySync sync =new MySync(); @Override // 尝试,不成功,进入等待队列 public void lock() { sync.acquire(1); } @Override // 尝试,不成功,进入等待队列,可打断 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // 尝试一次,不成功返回,不进入队列 public boolean tryLock() { return sync.tryAcquire(1); } @Override // 尝试,不成功,进入等待队列,有时限 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1,unit.toNanos(time)); } @Override // 释放锁 public void unlock() { sync.release(1); } @Override // 生成条件变量 public Condition newCondition() { return sync.newcondition(); }}
测试一下
public static void main(String[] args) { MyLock lock =new MyLock(); new Thread(()->{ lock.lock(); try { log.debug("locking..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { log.debug("unlocking"); lock.unlock(); } },"t1").start(); new Thread(()->{ lock.lock(); try { log.debug("locking..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { log.debug("unlocking"); lock.unlock(); } },"t2").start();}
输出
DEBUG [t1] - locking... DEBUG [t1] - unlocking DEBUG [t2] - locking... DEBUG [t2] - unlocking
不可重入测试
如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)
lock.lock(); log.debug("locking..."); lock.lock(); log.debug("locking...");
心得
起源
早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如可重入锁去实现信号量,或反之。这显然不够优雅,于是在JSR166(java规范提案)中创建了AQS、提供了这种通用的同步器机制。
目标
AQS要实现的功能目标
- 阻塞版本获取锁acquire和非阻塞的版本尝试获取锁tryAcquire
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时等待机制
AQS设计思路
AQS的基本思想其实很简单
- 获取锁的逻辑
while(state状态不允许获取){
if(队列中还没有此线程){
入队并阻塞
}
}
- 释放锁的逻辑
if(state状态允许了){
恢复阻塞的线程(s)
}
要点
-
原子维护state状态
-
阻塞及恢复线程
-
维护队列
state设计
state
使用volatile
配合cas
保证其修改的原子性state
使用了32bite int 来维护同步状态,因为当时使用long在很多平台测试的结果并不理想。
阻塞恢复设计
- 早期的控制线程暂停和恢复的api有suspend和resume,但它们是不可用的,因为如果先调用的resume那么suspend将感知不到。
- 解决方法时使用
park&unpark
来实现线程的暂停和恢复。 park&unpark
是针对线程的,而不是针对同步器的,因此控制粒度更为精细park
线程还可以通过interrupt
打断
队列设计
- 使用了FIFO先入先出队列,并不支持优先级队列
- 设计时借鉴了CLH队列,它是一种单向无锁队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O4ssOokO-1627542088310)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417113933783.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wnx2O5Ut-1627542088311)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417113942835.png)]
队列中有head
和tail
两个指针节点,都用volatile修饰配合cas使用,每个节点state维护节点状态入队伪代码,只需要考虑tail赋值的原子性
do{
//原来的tail
Node prev =tail;
//用cas在原来的tail的基础上该为node
}while(tail.compareAndSet(prev,node))
出队伪代码
//prev是上一个节点
while((Node prev =node.prev).state!=唤醒状态){
}
//设置头节点
head =node;
CLH好处:
- 无锁,使用自旋
- 快速,无阻塞
AQS在一些方面改进了CLH
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列中还没有元素 tail 为 null
if (t == null) {
// 将 head 从 null -> dummy
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将 node 的 prev 设置为原来的 tail
node.prev = t;
// 将 tail 从原来的 tail 设置为 node
if (compareAndSetTail(t, node)) {
// 原来 tail 的 next 设置为 node
t.next = node;
return t;
}
}
}
}
主要用到 AQS 的并发工具类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B0K90Orp-1627542088312)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417114431954.png)]
ReentrantLock原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PB4NtqfB-1627542088312)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417140453015.png)]
非公平锁实现原理
加锁解锁流程
先从构造器开始看,默认是非公平锁实现
public ReentranLock(){
sync =new NonfairSync();
}
NonfaiySync继承AQS
没有竞争时
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLRtvCqq-1627542088313)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417140711374.png)]
第一个竞争出现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWqTiqRa-1627542088314)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417140727273.png)]
Threa-1执行了
- CAS尝试将state由0改为1,结果失败
- 进入
tryAquire
逻辑,这时state已经是1
,结果仍然失败 - 接下来进入addWaiter逻辑,构造Node队列
- 图中黄色三角表示该Node的waitSatus状态,其中0位默认正常状态
- Node的创建是懒惰的
- 其中第一个Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1JpSAeMa-1627542088314)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417141013139.png)]
当前线程进入acquireQueued逻辑
-
acquireQueued
会在一个死循环中不断尝试获取锁,失败后进入park
阻塞 -
如果自己是紧邻
head
(排第二个),那么在此tryAcquire尝试获得锁,当然这是state仍为1,事变 -
进入
shouldParkAfterFailedAcquire
逻辑,将前驱node,即head的waitStatus
改为-1,这次返回false
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yom03D3-1627542088315)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417143614390.png)]
-
shouldParkAfterFailedAcquire
执行完毕回到acquireQueued**,在此tryAcquire尝试获得锁,当然这时state仍为1,失败。** -
当在此进入
shouldParkAfterFailedAcquire
,这时因为其前驱node的waitStatus已经为-1,这次返回true -
进入
parkAndCheckInterrupt,
Thread-1park(灰色表示)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAHTrby4-1627542088316)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417143838028.png)]
再次有多个线程经历上述过程竞争失败,变成这个样子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HEbqUiI-1627542088316)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417143851773.png)]