多线程进阶(二)Callable接口,JUC下常见类使用及线程安全集合类

目录

前言:

Callable接口

代码实现

JUC常见类使用

ReentrantLock类

代码实现

信号量

代码实现

CountDownLatch类

代码实现

线程安全的集合类

多线程环境下使用ArrayList

多线程环境下使用队列

多线程环境下使用哈希表

小结:


前言:

    这篇文章主要介绍Callable接口,JUC包下一些常见类的使用,还有我们之前使用集合类在多线程环境下的使用。

Callable接口

    可以使用Callable接口创建带有返回值的线程任务(和Runable类似)。这样的线程具有返回值,由于线程调度的随机性,我们不确定线程什么时候被调度,具体线程任务什么时候执行完毕。基于这样的问题采取FutureTask类对Callable进行包装。FutureTask就可以等待Callable的执行结果。

    FutureTask提供的get方法就可以获取Callable的返回结果。它会阻塞,直到Callable里的任务执行完毕。

代码实现

public class ThreadDemo30 {
    public static void main(String[] args) throws Exception {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for(int i = 0; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        //get方法获取到结果,这里会进行阻塞,直到callable执行完毕,才能获取到结果
        Integer tmp = futureTask.get();
        System.out.println(tmp);
    }
}

JUC下常见类使用

ReentrantLock类

    ReentrantLock和Synchronized类似,都是加锁的。那么为什么存在Synchronized还要有ReentrantLock呢?下面介绍它的优点:

     1)ReentrantLock的加锁和解锁是分开的,可以更加灵活的使用。

     2)提供了公平锁的实现,只需要在构造方法中参数写为true即可。不写或者写false都是非公平锁

     3)Synchronized产生的等待是死等,而它提供了tryLock()方法,返回值为boolean(是否加锁成功)。无参数版本能加锁就加不能则放弃。有参数版本可指定阻塞最大时间,如果时间到了不能获取到锁也就放弃了。 

     4)Synchronized加锁使用notify随机唤醒一个线程。它搭配Condition类指定唤醒某个线程。

    ReentrantLock提供lock()方法加锁,unlock方法解锁。由于两者是分开的,如果加锁成功了,为了能保证unlock方法一定可以执行我们将其写在finally代码块中。

代码实现

public class ThreadDemo31 {
    volatile private static int sum = 0;
    private static ReentrantLock reentrantLock = new ReentrantLock(true);
    public static void func() {
        try {
            reentrantLock.lock();
            for(int i = 0; i <= 100; i++) {
                sum += i;
            }
        }finally {
            reentrantLock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                func();
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                func();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(sum);
    }
}

信号量

    信号量为可用资源个数。如果信号量为0,继续申请可用资源个数,就会阻塞(信号量不能为负数)。锁就可以视为计数器为1的信号量(二元信号量)。

    可用资源个数就是具体的一个数字。描述了这个信号量可以提供的最大可用资源的数量,每申请一个可用资源,其计数器就减1。释放一个可用资源计数器就加1。

    Java中提供Semaphore类来实现信号量。acquire()方法申请信号量,release()方法释放信号量,构造方法传递一个参数就是初始化信号量个数。

代码实现

public class ThreadDemo32 {
    public static void main(String[] args) throws InterruptedException {
        //初始化信号量为3
        Semaphore semaphore = new Semaphore(3);
        //申请信号量(个数 -1),可以指定参数一次就申请多个
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        semaphore.release();
        semaphore.acquire();
        semaphore.acquire();
        //释放信号量(个数 +1),可以指定参数一次就释放多个
        //semaphore.release();
    }
}

     注意:可以清楚看见代码在阻塞当中。整个进程都没有结束。

CountDownLatch类

    CountDownLatch类可以实现如果有10个线程,可以使另一个线程阻塞到这10个线程全部执行完毕。类似的场景比如跑步比赛,只有当最后一个人到达终点才算比赛结束。

    构造方法提供一个参数,描述了具体任务的数量。countDown()方法来体现一个任务执行完毕,await()方法阻塞到初始任务数量全部执行完毕,那么意味着只有和任务数量一致那一次countDown()方法才起作用。

代码实现

public class ThreadDemo33 {
    public static void main(String[] args) throws InterruptedException {
        //具体有10个任务
        CountDownLatch latch = new CountDownLatch(10);
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    //任务执行完成,调用countDown方法,表示任务执行完成
                    latch.countDown();
                }
            });
            t.start();
        }

        //阻塞到10个任务全部执行完成,第10次调用countDown方法才起作用
        latch.await();
        System.out.println("Aaaaaa");
        //使用场景跑步比赛
        //等待所有人跑完才算结束
    }
}

线程安全的集合类

    Vector,Stack,HashTable是线程安全的,但是不建议使用。其他线程都是线程不安全的。

多线程环境下使用ArrayList

 1)可以自己手动加锁,来保证线程安全。

 2)使用Collections类下的synchronizedList静态方法,对ArrayList进行包裹。synchronizedList关键操作都带有synchronized。如下图源码可以清楚看见。

 3)CopyOnWriteArrayList

    CopyOnWriteArrayList即写时复制的容器。针对读数据不做任何工作。针对写操作,首先会拷贝一份新的ArrayList,在这份新的当中写(两个不同的对象不会存在线程安全问题)。如果在写的期间需要读,就读旧的ArrayList,当新的写完后在替换到旧的上面去(替换的本质就算引用之间的修改,原子的)。

优点:

    写时拷贝,不需要加锁,就可以实现线程安全。代码效率高。

缺点:

    它只适合数据量比较小的(拷贝需要时间),并且占用内存较多,新写的数据不能第一时间读取到。

多线程环境下使用队列

1)ArrayBlockingQueue

    基于数组实现的阻塞队列。

2)LinkedBlockingQueue

    基于链表实现的阻塞队列。

3)PriorityBlockingQueue

    基于堆实现带有优先级的阻塞队列。

4)TransferQueue

    最多只包含一个元素的阻塞队列。

多线程环境下使用哈希表

    HashMap多线程环境下是不安全的。HashTable多线程环境下线程安全(给主要方法上加了一把大锁)。ConcurrentHashMap更优化的线程安全哈希表。

优化之处:

1)将HashTable的一把大锁改为了小锁

     如果两个元素在同一个链表(树)上,多线程下是不安全的。但如果不在同一条链表(树)上多线程下是安全的(两个元素间没有联系)。HashTable不管在没在同一个链表上,锁是加在方法上的,只要调用了这样的方法就会阻塞,那么两个元素在不同的链表上也是会阻塞。ConcurrentHashMap锁是加在每条链表或者树的头节点上的。元素在不同链表上由于锁对象不同,则不会产生锁竞争。元素在同一条链表上,锁对象相同,则产生锁竞争。

2)针对读不加锁,针对写加锁

    读和读之间没有锁竞争。写和写之间存在锁竞争。读和写之间不存在锁竞争,这样就可能造成脏读问题(读了一条不全的数据),基于这样的问题,这里的写操作设计为:volatile + 原子写操作。那么就只有写完才能读数据,就不会存在脏读问题。

3)充分使用CAS

    充分使用CAS,进一步减少锁的数量。比如维护元素个数。

4)针对扩容采取“化整为零”的方式

    HashMap/HashTable扩容时首先开辟一块更大的数组,将旧数组上数据重新哈希到新数组上,再释放旧数组。如果数据量较大,可能某次put操作就比较耗时。

    ConcurruteHashMap如果需要扩容,首先开辟一块更大的数组,会每次搬运一小部分数据。保留新旧两个数组,put时就往新数组上哈希,并且搬运一部分旧数组数据到新数组上(后续只要操作ConcurruteHashMap的线程都会参与搬运的过程),删除旧数组上被搬运的元素。直到旧数组数据全部搬运完毕,释放旧数组。查找元素时两个数组都查找。删除元素时,两个数组也查找,找到就正常删除即可。

小结:

    与大家分享一句名言:真正的才智是刚毅的志向。 -----  拿破仑

猜你喜欢

转载自blog.csdn.net/weixin_62353436/article/details/128649342