并发编程之深入学习与面试

1.Unsafe类

通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。其源码如下:

public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:

1.1、内存管理包括分配内存、释放内存等。

该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。

利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。

1.2、数组操作。

这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。

由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。

1.3、多线程同步。包括锁机制、CAS操作等。

这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。

其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。

Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。compareAndSwap方法是原子的,可以避免繁重的锁机制,提高代码效率。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。

1.4、挂起与恢复。

这部分包括了park、unpark等方法。

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

1.5、内存屏障。

这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。

loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。

2.Exchanger类


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 *  如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,
 *  这两个线程就可以交换数据,将本线程生产出来的数据传递给对方
 * @author lzhcode
 *
 */
public class TestExchanger {

	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		final Exchanger exchanger = new Exchanger();
		service.execute(new Runnable(){
			public void run() {
				try {				

					String data1 = "zxx";
					System.out.println("线程" + Thread.currentThread().getName() + 
					"正在把数据" + data1 +"换出去");
					Thread.sleep((long)(Math.random()*10000));
					String data2 = (String)exchanger.exchange(data1);
					System.out.println("线程" + Thread.currentThread().getName() + 
					"换回的数据为" + data2);
				}catch(Exception e){
					
				}
			}	
		});
		service.execute(new Runnable(){
			public void run() {
				try {				

					String data1 = "lhm";
					System.out.println("线程" + Thread.currentThread().getName() + 
					"正在把数据" + data1 +"换出去");
					Thread.sleep((long)(Math.random()*10000));					
					String data2 = (String)exchanger.exchange(data1);
					System.out.println("线程" + Thread.currentThread().getName() + 
					"换回的数据为" + data2);
				}catch(Exception e){
					
				}				
			}	
		});		
	}
}

3.ForkJoin 框架

在日常的业务需求中,经常出现的批量查询,批量写入等接口的提供,一般来说,最简单最low的方式就是写一个for循环来一次执行,但是当业务方对接口的性能要求较高时,就比较尴尬了

通常可以想到的方式是采用并发操作,首先想到可以实现的方式就是利用线程池来做

通常实现方式如下

// 1. 创建线程池

ExecutorService executorService = new ThreadPoolExecutor(3, 5, 60,
      TimeUnit.SECONDS,
      new LinkedBlockingDeque(10), new DefaultThreadFactory("biz-exec"),
      new ThreadPoolExecutor.CallerRunsPolicy());

// 2. 创建执行任务
List> futureList = new ArrayList<>();
for(Object arg : list) {
        futureList.add(executorService.submit(new Callable() {
            @Override
            public Object call() throws Exception {
              // xxx
            }
        }));
}

// 3. 结果获取
for(Future f: futureList) {
    Object obj = f.get();
}
 
  

用上面的这种方式并没有什么问题,我们接下来考虑的是如何使用ForkJoin框架来实现类似的功能

并发编程之深入学习与面试_第1张图片

任务分割

ForkJoinTask : 基本任务,使用forkjoin框架必须创建的对象,提供fork,join操作,常用的两个子类

  • RecursiveAction : 无结果返回的任务
  • RecursiveTask : 有返回结果的任务

说明:

  1. fork : 让task异步执行
  2. join : 让task同步执行,可以获取返回值
  3. ForkJoinTask 在不显示使用ForkJoinPool.execute/invoke/submit()方法进行执行的情况下,也可以使用自己的fork/invoke方法进行执行

结果合并

ForkJoinPool 执行 ForkJoinTask

  • 任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。
  • 当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务

三中提交方式:

  1. execute 异步,无返回结果
  2. submit 异步,有返回结果 (返回Future
  3. invoke 同步,有返回结果 (会阻塞)
public class CountTask extends RecursiveTask {

    private int start;
    private int end;

    private static final int THRED_HOLD = 30;


    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        boolean canCompute = (end - start) <= THRED_HOLD;
        if (canCompute) { // 不需要拆分
            for (int i = start; i <= end; i++) {
                sum += i;
            }

            System.out.println("thread: " + Thread.currentThread() + " start: " + start + " end: " + end);
        } else {
            int mid = (end + start) / 2;
            CountTask left = new CountTask(start, mid);
            CountTask right = new CountTask(mid + 1, end);
            left.fork();
            right.fork();

            sum = left.join() + right.join();
        }
        return sum;
    }
}
@Test
public void testFork() throws ExecutionException, InterruptedException {
    int start = 0;
    int end = 200;

    CountTask task = new CountTask(start, end);
    ForkJoinPool pool = ForkJoinPool.commonPool();
    Future ans = pool.submit(task);
    int sum = ans.get();
    System.out.println(sum);
}

简单封装代码:https://gitee.com/lzhcode/maven-parent/tree/master/lzh-technology/src/main/java/com/lzhsite/technology/concurrent/framwork/forkjoin

4.LockSupport 

特点:

1、基于UnSafe原语实现

LockSupport类在jdk源码中基本定义就是创建锁和其他同步类的 基本线程阻塞,直接与UnSafe原语类打交道

2、重入性
LockSupport是非重入锁,如果一个线程连续2次调用 LockSupport .park(),那么该线程一定会一直阻塞下去

3、面向线程锁
面向线程锁是LockSupport很重要的一个特征,这样也就没有公平锁和非公平的区别了的,同时面向线程锁的特征在一定程度上降低代码的耦合度。

LockSupport比Object的wait/notify有两大优势

①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。

②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

LockSupport在Java的工具类用应用很广泛,咱们这里找几个例子感受感受。以Java里最常用的类ThreadPoolExecutor为例。先看如下代

public class TestObjWait {

    public static void main(String[] args)throws Exception {
        ArrayBlockingQueue queue = new ArrayBlockingQueue(1000);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);

        Future future = poolExecutor.submit(new Callable() {
            @Override
            public String call() throws Exception {
                TimeUnit.SECONDS.sleep(5);
                return "hello";
            }
        });
        String result = future.get();
        System.out.println(result);
    }
}

 代码中我们向线程池中扔了一个任务,然后调用Future的get方法,同步阻塞等待线程池的执行结果。

这里就要问了:get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?

咱们跟着源码一步步分析,先看线程池的submit方法的实现:

并发编程之深入学习与面试_第2张图片

在submit方法里,线程池将我们提交的基于Callable实现的任务,封装为基于RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。

进入newTaskFor方法,就一句话:return new FutureTask(callable);

所以,咱们主线程调用future的get方法就是FutureTask的get方法,线程池执行的任务对象也是FutureTask的实例。

接下来看看FutureTask的get方法的实现:

并发编程之深入学习与面试_第3张图片 比较简单,就是判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。

并发编程之深入学习与面试_第4张图片

awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程

上边已经说完了阻塞等待任务结果的逻辑,接下来再看看线程池执行完任务,唤醒等待线程的逻辑实现。

前边说了,咱们提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:

并发编程之深入学习与面试_第5张图片

c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,想必唤醒线程的工作就在这里边了,看看代码实现吧: 

并发编程之深入学习与面试_第6张图片

总结:线程池里的submit返回FutureTask,而FutureTask的get方法里调用了LockSupport的park和unpark

 

5.自定义线程池

@Bean(destroyMethod="shutdown")
    public ThreadPoolTaskExecutor defaultThreadPool() {

        // CPU可用核心数
        int cpuNum = Runtime.getRuntime().availableProcessors();

        //1.IO密集型 估算线程池大小
        //线程数 = CPU可用核心数/(1-阻塞系数)
        //计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近于1。一个完全阻塞的任务是注
        //定要挂掉的,所以我们无须担心阻塞系数会达到1。
        //阻塞系数可以采用一些性能分析工具或java.lang.managenment API来确定线程话在系统I/O操作
        //上的时间与CPU密集任务所消耗的时间比值。

        //2.计算密集型估算线程池大小threadNum = cpuNum +1 或则  计算密集型估算线程池大小threadNum = cpuNum * 2;
        int threadNum = cpuNum * 2;

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //核心线程数目
        executor.setCorePoolSize(threadNum-1);
        //指定最大线程数
        executor.setMaxPoolSize(threadNum);
        //队列中最大的数目
        executor.setQueueCapacity(300);
        executor.setThreadFactory(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                //如果为守护线程,恢复为常规
                if(t.isDaemon()) {
                    t.setDaemon(false);
                }
                //如果有设置线程优先级,恢复为常规
                if(Thread.NORM_PRIORITY != t.getPriority()) {
                    t.setPriority(Thread.NORM_PRIORITY);
                }
                return t;
            }
        });
        //线程名称前缀
        executor.setThreadNamePrefix("defaultThreadPool_");
        //rejection-policy:当pool已经达到max size的时候,如何处理新任务
        //CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        //对拒绝task的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //线程空闲后的最大存活时间
        executor.setKeepAliveSeconds(60);
        //加载
        executor.initialize();
        return executor;
    }

 6.AQS架构

   6.1它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)

并发编程之深入学习与面试_第7张图片

 6.2AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

 

 6.3不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种模板方法

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int): 独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

6.4以ReentrantLock的源码为例来深入理解AQS

ReentrantLock是基于AQS的独占锁来实现的
6.4.1.state初始化时为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1
6.4.2.此后,其他线程再tryAcquire()会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获得该锁

并发编程之深入学习与面试_第8张图片 ReentrantLock类结构

 下面看下Sync类源代码:

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

     
        abstract void lock();
 
        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) 
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            //releasesy一般传1
            int c = getState() - releases;
            //当前线程不是独占的不满足条件抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
           //如果state - releasesd等于0把空闲状态free置为true,同时把独占线程置为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
             //设置新的state
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            //判断Thread是否和当前线程相等从而判断是否为独占的
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

       

        final Thread getOwner() {
            //等于0表示没有任何线程占用这个资源,有的话返回占用的线程
            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
        }
    }

FairSync 类里的tryAcquire方法:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //队列里找不到等待的其他线程,就设置为当前线程独占,非公平锁(具体实现是Sync类的
                //nonfairTryAcquire方法,见上面代码)下的tryAcquire没有!hasQueuedPredecessors
                //判断其他代码都一样
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //可重入锁主要体现在这段逻辑,占用的线程就是当前线程把state加1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

AbstractQueuedSynchronizer的 acquire方法

    public final void acquire(int arg) {
       //1.addWaiter(Node.EXCLUSIVE)把最后一个线程放在队列尾部
       //2.acquireQueued循环判断新入的节点是否为前驱节点,是前驱节点时
       //3.整个if的判断逻辑是没有获得锁成功的话加入等待队列的的再打断自己
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
并发编程之深入学习与面试_第9张图片 acquire方法调用流程图

 

AbstractQueuedSynchronizer的 acquireQueued方法 

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //不断自旋
            for (;;) {
                final Node p = node.predecessor();
                //是前驱节点时,尝试获取许可tryAcquire
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

6.5以CountDownLatch的原理为例来深入理解AQS

CountDownLatch是基于AQS的共享锁来实现的,由于内部类继承了AQS,所以它内部也是FIFO队列,同时也一样是前驱节点唤醒后继节点,不能像CyclicBarrier那样使用完毕后还可以复用;

6.5.1任务分为N个任子线程去执行,state也初始化为N(注意N要要与线程个数一致) 
6.5.2这N个线程也是并行执行的,每个子线程执行完后后countDown()一次,state会CAS减1 
6.5.3等到所有子线程都执行完成之后即(state==0),会调用LockSupport.unpark(s.thread)
6.5.4然后主调用会从await()函数返回(之前是通过LockSupport.park(this);阻塞),继续后余动作

源码分析: https://www.cnblogs.com/d-homme/p/9375105.html

 

7.ReentrantLock使用场景

场景1:如果发现该操作已经在执行中则不再执行(有状态执行)

ReentrantLock lock = new ReentrantLock();  

if(lock.tryLock()){

    try{

    }finally{

         lock.unlock();
    }
}

场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)

场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

import java.util.concurrent.*;  
import java.util.concurrent.locks.*;  

public class AttemptLocking {  
    private ReentrantLock lock = new ReentrantLock();  

    public void untimed() {  
        boolean captured = lock.tryLock();  
        try {  
            System.out.println("tryLock(): " + captured);  
        } finally {  
            if (captured)  
                lock.unlock();  
        }  
    }  

    public void timed() {  
        boolean captured = false;  
        try {  
            captured = lock.tryLock(2, TimeUnit.SECONDS);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        try {  
            System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);  
        } finally {  
            if (captured)  
                lock.unlock();  
        }  
    }  

    public static void main(String[] args) throws InterruptedException {  
        final AttemptLocking al = new AttemptLocking();  
        al.untimed(); // True -- 可以成功获得锁  
        al.timed(); // True --可以成功获得锁  
        //新创建一个线程获得锁并且不释放  
        new Thread() {  
            {  
                setDaemon(true);  
            }  

            public void run() {  
                al.lock.lock();  
                System.out.println("acquired");  
            }  
        }.start();  
        Thread.sleep(100);// 保证新线程能够先执行  
        al.untimed(); // False -- 马上中断放弃  
        al.timed(); // False -- 等两秒超时后中断放弃  
    }  
}  

场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作(类似于wait())

ReentrantLock lock = new ReentrantLock();  
    
try{

     lock.lockInterruptibly();
 
}finally{
 
     lock.unlock();
}

 另外在ReentrantLock类中定义了很多方法,比如:
  isFair()        //判断锁是否是公平锁
  isLocked()    //判断锁是否被任何线程获取了
  isHeldByCurrentThread()   //判断锁是否被当前线程获取了
  hasQueuedThreads()   //判断是否有线程在等待该锁

8、线程中断

Thread.currentThread().interrupt()
线程中断只是一个状态而已,true表示已中断,false表示未中断

//获取线程中断状态,如果中断了返回true,否则返回false
Thread.currentThread().isInterrupted()
  设置线程中断不影响线程的继续执行,但是线程设置中断后,线程内调用了wait、jion、sleep方法中的一种, 立马抛出一个 InterruptedException,且中断标志被清除,重新设置为false。
 

这个恢复过来就可以包含两个目的:

  一、[可以使线程继续执行],那就是在catch语句中招待醒来后的逻辑,或由catch语句转回正常的逻辑。总之它是从wait,sleep,join的暂停状态活过来了。

  二、[可以直接停止线程的运行],当然在catch中什么也不处理,或return,那么就完成了当前线程的使命,可以使在上面"暂停"的状态中立即真正的"停止"。

 

你可能感兴趣的:(并发编程)