并发核心技术总结(一)

(1)线程安全和锁Synchronized概念

一、Java实现多线程方式

(1)继承Thread并重写run()方法,用start()方法启动线程

(2)实现Runnable接口

二、synchronized

(1)可以在任意对象及方法上加锁,加锁的代码成为“互斥区”或“临界区”

(2)同步的目的是为了线程的安全,需要满足两个最基本的特性:原子性和可见性

(2)可重入锁以及Synchronized的其他基本特性

可重入锁:父子可继承性

synchronized的其他特性

  • 1、出现异常时,锁自动释放
  • 2、将任意对象作为监视器
  • 3、单例模式-双重校验锁
public class DubbleSingleton {

    private static volatile DubbleSingleton instance;

    public static DubbleSingleton getInstance(){
        if(instance == null){
            try {
                //模拟初始化对象的准备时间...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //类上加锁,表示当前对象不可以在其他线程的时候创建
            synchronized (DubbleSingleton.class) { 
                //如果不加这一层判断的话,这样的话每一个线程会得到一个实例
                //而不是所有的线程的到的是一个实例
                if(instance == null){ 
                    instance = new DubbleSingleton();
                }
            }
        }
        return instance;
    }
}
  • volatile:禁止指令重排

(3)线程间的通信机制

wait()、notify()方法总结

  • 都要结合synchronized关键字一起使用,因为他们都需要首先获取该对象的对象锁

  • wait方法是释放锁,notify方法是不释放锁

注意点:执行notify方法之后,当前线程不会立即释放其拥有的该对象锁,而是执行完之后才会释放该对象锁,被通知的线程也不会立即获得对象锁,而是等待notify方法执行完之后,释放了该对象锁,才可以获得该对象锁。

公平锁和非公平锁

  • 公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配,先进先出
  • 非公平锁即是一种抢占机制,随机获得锁
public ReentrantLock(boolean fair){
    sync = fair ? new FairSync() : new NonFairSync();
}

(4)CountDownLatch和CyclicBarrier

public class SummonDragonDemo {

    private static final int THREAD_COUNT_NUM = 7;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    System.out.println("第" + index + "颗龙珠已收集到!");
                    //模拟收集第i个龙珠,随机模拟不同的寻找时间
                    Thread.sleep(new Random().nextInt(3000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //每收集到一颗龙珠,需要等待的颗数减1
                countDownLatch.countDown();
            }).start();
        }
        //等待检查,即上述7个线程执行完毕之后,执行await后边的代码
        countDownLatch.await();
        System.out.println("集齐七颗龙珠!召唤神龙!");
    }
}

CountDownLatch在实时系统中的使用场景

  • 实现最大的并行性

  • 开始执行前等待n个线程完成各自任务

  • 死锁检测

模拟应用程序启动类

代码Thread-Demo2


public class SummonDragonDemo {

    private static final int THREAD_COUNT_NUM = 7;

    public static void main(String[] args) {

        //设置第一个屏障点,等待召集齐7位法师
        CyclicBarrier callMasterBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
            @Override
            public void run() {
                System.out.println("7个法师召集完毕,同时出发,去往不同地方寻找龙珠!");
                summonDragon();
            }
        });
        //召集齐7位法师
        for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    System.out.println("召集第" + index + "个法师");
                    callMasterBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    /**
     * 召唤神龙:1、收集龙珠;2、召唤神龙
     */
    private static void summonDragon() {
        //设置第二个屏障点,等待7位法师收集完7颗龙珠,召唤神龙
        CyclicBarrier summonDragonBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
            @Override
            public void run() {
                System.out.println("集齐七颗龙珠!召唤神龙!");
            }
        });
        //收集7颗龙珠
        for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    System.out.println("第" + index + "颗龙珠已收集到!");
                    summonDragonBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

区别

(1)CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

(2)CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

(3)CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程

(5)线程池

Executors类下常用方法

该方法返回一个固定线程数量的线程池
public static ExecutorService newFixedThreadPool()

该方法返回一个只有一个现成的线程池
public static ExecutorService newSingleThreadExecutor()

返回一个可以根据实际情况调整线程数量的线程池
public static ExecutorService newCachedTreadPool()

该方法和 newSingleThreadExecutor 的区别是给定了时间执行某任务的功能,可以进行定时执行等
public static ScheduledExecutorService newSingleThreadScheduledExecutor()

在4的基础上可以指定线程数量
public static ScheduledExecutorService newScheduledThreadPool()

创建线程池实质调用的还是ThreadPoolExecutor

在Executors类中,我们拿一个方法简单分析一下 可以看出,类似的其他方法一样,在Executors内部创建线程池的时候,实际创建的都是一个ThreadPoolExecutor对象,只是对ThreadPoolExecutor构造方法,进行了默认值的限定。

构造方法如下: 参数含义如下: 

Executor框架实例

实例一

public class ThreadPoolDemo {

    public static void main(String[] args) {

        ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0L, 
                TimeUnit.MILLISECONDS, 
                new LinkedBlockingQueue<>(10), 
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
            int index = i;
            executorService.submit(() -> System.out.println("i:" + index + 
                    " executorService"));
        }
        executorService.shutdown();
    }
}

public class ThreadPoolDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 2, 0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
            int index = i;
            pool.submit(() -> System.out.println("i:" + index +
                    " executorService"));
        }
        pool.shutdown();
    }
}

实例二

public static void main(String[] args) {

        ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10),
                new ThreadFactory() { //自定义ThreadFactory
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r);
                        thread.setName(r.getClass().getName());
                        return thread;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy()); //自定义线程拒绝策略

        for (int i = 0; i < 10; i++) {
            int index = i;
            executorService.submit(() -> System.out.println("i:" + index));
        }

        executorService.shutdown();
    }
}

1.使用submit()的坑

public class ThreadPoolDemo3 {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 5; i++) {
            int index = i;
            executorService.submit(() -> divTask(100, index));


        }
        executorService.shutdown();
    }

    private static void divTask(int a, int b) {
        double result = a / b;
        System.out.println(result);
    }
}

使用submit(Runnable task)的时候,错误的堆栈信息抛出的时候会被内部捕获到,所以打印不出来具体的错误信息,解决方法有两种:

2.1使用execute()代替submit()

public class ThreadPoolDemo3 {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 5; i++) {
            int index = i;
            executorService.execute(() -> divTask(100, index));
        }
        executorService.shutdown();
    }

    private static void divTask(int a, int b) {
        double result = a / b;
        System.out.println(result);
    }
}

2.2使用Future

public class ThreadPoolDemo3 {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 5; i++) {
            int index = i;
            Future future = executorService.submit(() -> divTask(200, index));
            try {
                future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        executorService.shutdown();
    }

    private static void divTask(int a, int b) {
        double result = a / b;
        System.out.println(result);
    }
}

3.execute和submit的区别

  • (1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。

  • (2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

(6) 深入分析ThreadLocal原理

1.Thread和ThreadLocalMap的关系

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

  • 可以看出每个thread实例都有一个ThreadLocalMap,默认的一个ThreadLocalMap初始化了16个Entry,每个Entry对象存放的是一个ThreadLocal变量对象。

  • 即一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry,即一个Thread可以依附有多个ThreadLocal对象

2.ThreadLocalMap与WeakReference

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:

  • (1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象

  • (2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>

public class ThreadLocalMap{
    
    static class Entry extends WeakReference<ThreadLocal<?>>{
        Object value;
        
        Entry(ThreadLocal<?> k,Object v){
            super(k);
            value = v;
        }
    }
}
  • 类Entry很显然是一个保存map键值对的实体,ThreadLocal<?>为key,要保存的线程局部变量的值为value。super(k)调用的WeakReference的构造函数,表示将ThreadLocal<?>对象转换成弱引用对象,用做key。

3.ThreadLocalMap的构造函数

ThreadLocalMap(ThreadLocal<?> firstKey,Object firstValue){
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 );
    table[i] = new Entry(firstKey,firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
} 
private void setThreshold(int len){
    threshold = len * 2 / 3;
}

可以看出,ThreadLocalMap这个map的实现是使用一个数组private Entry[] table 来保存键值对的实体,初始大小为16,ThreadLocalMap自己实现了如何从 key 到 value 的映射:

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  • ThreadLocalMap是一个类似HashMap的集合,只不过自己实现了寻址,也没有HashMap的put方法,而是set方法

ThreadLocal的set方法

public void set(T value){
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map != null){
     map.set(this,value);
     }else{
         createMap(t,value);
     }
}
void  createMap(Thread t,T firstValue){
    t.threadLocals = new ThreadLocalMap(this,firstValue);
}

ThreadLocal的get方法

public T get(){
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map != null){
        ThreadLocalMap.Entry e = map.getEntry(this);
        if(e != null){
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key){
    int i = key.threadLocalHashCode & (table.length()-1);
    Entry e = table[i];
    if(e != null && e.get()==key)
      return e;
    else 
      return getEntryAfterMiss(key,i,e);
}
  • 首先获取ThreadLocalMap对象,由于ThreadLocalMap使用的当前的ThreadLocal作为key,所以传入的参数为this,然后调用getEntry()方法,通过这个key构造索引,根据索引去table(Entry数组)中去查找线程本地变量,根据下边找到Entry对象,然后判断Entry对象e不为空并且e的引用与传入的key一样则直接返回,如果找不到则调用getEntryAfterMiss()方法。调用getEntryAfterMiss表示直接散列到的位置没找到,那么顺着hash表递增(循环)地往下找,从i开始,一直往下找,直到出现空的槽为止。

总结

(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;

(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;

(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;

(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;

(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);

(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;

(7)线程死亡时,线程局部变量会自动回收内存;

(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1)来完成的

(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

对于多线程共享资源的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者只提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

THreadLocal导致内存泄漏的根源

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

  • 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值