并发编程(下)

不可变对象设计(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, 重用数量有限的同一类对象。

    • 结果型模式
  • 享元模式的提现

    1. 在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);}
    

注意:

  • Byte、Short、Long缓存的范围都是-128-127
  • Character的缓存的范围是0-127
  • Boolean缓存了TRUEFALSE
  • 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 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTimeunit 来控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 中特有的方法
  • 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的等待队列,类似于MonitorEntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于MonitorWaitSet

子类主要实现这样的一些方法(默认抛出 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)]

队列中有headtail两个指针节点,都用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执行了

  1. CAS尝试将state由0改为1,结果失败
  2. 进入tryAquire逻辑,这时state已经是1,结果仍然失败
  3. 接下来进入addWaiter逻辑,构造Node队列
    • 图中黄色三角表示该Node的waitSatus状态,其中0位默认正常状态
    • Node的创建是懒惰的
    • 其中第一个Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1JpSAeMa-1627542088314)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417141013139.png)]

当前线程进入acquireQueued逻辑

  1. acquireQueued会在一个死循环中不断尝试获取锁,失败后进入park阻塞

  2. 如果自己是紧邻head(排第二个),那么在此tryAcquire尝试获得锁,当然这是state仍为1,事变

  3. 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1,这次返回false

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yom03D3-1627542088315)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210417143614390.png)]

  4. shouldParkAfterFailedAcquire执行完毕回到acquireQueued**,在此tryAcquire尝试获得锁,当然这时state仍为1,失败。**

  5. 当在此进入shouldParkAfterFailedAcquire,这时因为其前驱node的waitStatus已经为-1,这次返回true

  6. 进入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)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值