今天来继续带大家学习多线程进阶部分啦,今天是最后一期啦,下期带大家做一些多线程的题,我们就可以开始下一个环节啦;
1,JUC(java.util.concurrent)的常见类
1)Callable 接口
我们之前学过Runnable接口,它是一个任务,我们可以在创建线程的时候把任务丢给线程使用匿名内部类等方法来完成创建对象,现在我们有了一个新的方法来创建任务,并且执行这个任务,就是我们的Callable接口,Runnable的run方法是没有返回值的,但是Callable提供了返回值,支持泛型,我们就能获取到我们想要的参数,
我们来看看是怎么用的;
java
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return null;
}
};
我们使用匿名内部类的方法创建一个Callable对象,并且重写call方法,就相当与重写Runnable的run方法, 我们是不能把这个对象直接放到线程的构造方法中的,因为Thread没有提供传入Callable的版本,我们要使用另一个类FutureTask来拿到结果,在把创建的futureTask对象放到线程创建时的构造方法中去;
java
FutureTask<Integer> task = new FutureTask<>(callable);
Thread t1 = new Thread(task);
t1.start();
System.out.println(task.get());
这里的task.get方法会阻塞main线程结束,直到t1线程正确计算出结果;
2)ReentrantLock
这个是上古时期的锁,现在有更智能,更好的替代synchronized,那我们还学它干嘛呢,它还活着就一定是有原因的,
1,synchronized是关键字,是由JVM内部通过C++实现的,而ReentrantLock是一个类;
2,synchronized是通过进出代码块来实现的,ReentrantLock需要Lock和UnLock方法来辅助;
java
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
a++;
reentrantLock.unlock();
3,ReentrantLock除了提供Lock和unLock之外还提供了一个不会造成阻塞的tryLock()
它会根据是否加锁成功返回true或者false;
4,synchronized是非公平锁,而ReentrantLock是默认是非公平锁,但是也提供了公平锁的实现;
5,ReentrantLock的等待通知机制是Condition类,比synchronized的wait和notify功能更强
3)线程池
博主博主,咱们之前不是讲过线程池了吗,怎么又来一遍呀,确实嗷,上次虽然给大家详细讲过了,但是我们还没有用呀,哈哈哈哈哈,我直接上代码;
我们先来简单的版的;
java
public class Demo2 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(1111);
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(1);//创建固定数目的线程池
//ExecutorService executorService1 = Executors.newSingleThreadExecutor(); 创建单线程池
//ExecutorService executorService2 = Executors.newCachedThreadPool(); 创建线程动态增长的线程池
//ScheduledExecutorService service = Executors.newScheduledThreadPool(1); 创建定时线程池
//executorService.submit(runnable);
executorService.shutdown();
}
}
我们还可以通过execute来提交任务
java
executorService.execute(runnable);
都是官方给提供的现成的,我们这会来自己创建;
java
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,
10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
这就是我们自己创建的线程池,我们要把所有的参数都填上;
1. 任务队列类型
队列类型 | 特点 |
---|---|
ArrayBlockingQueue |
有界队列,需指定容量 |
LinkedBlockingQueue |
无界队列(默认使用,可能 OOM) |
SynchronousQueue |
不存储任务,直接提交给线程 |
PriorityBlockingQueue |
支持优先级排序 |
2. 拒绝策略
策略类 | 行为 |
---|---|
AbortPolicy (默认) |
抛出 RejectedExecutionException |
CallerRunsPolicy |
由提交任务的线程直接执行任务 |
DiscardPolicy |
静默丢弃新任务 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,然后重试提交 |
工厂模式那个也是官方给提供的现成的哈哈哈哈,太懒了我;
4)信号量 Semaphore
一种计数器,可以表示可用资源的个数;
信号量的P操作,申请资源,计数器加一;
信号量的V操作,释放资源,计数器减一;
如果此时计数器为零,再尝试申请资源就会进入阻塞等待;
有一点点像锁;
我们使用acquire来申请资源,使用release来释放资源,
我们来试试写代码;
java
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);//3个可用资源
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("申请资源");
semaphore.acquire();
System.out.println("获取到了资源");
Thread.sleep(10000);
semaphore.release();
System.out.println("释放资源");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
Thread t1= new Thread(runnable);
Thread t2= new Thread(runnable);
Thread t3= new Thread(runnable);
t1.start();
t2.start();
t3.start();
Thread t4= new Thread(runnable);
t4.start();
t4.join();
}
}
我通过运行这个代码可以看到t1, t2,t3线程获取申请资源之后不释放,t4申请资源就要等着,直到10s之后,t4线程才开始工作;
5)CountDownLatch
也类似一个计数器,我们传入构造方法的参数就是需要完成的任务个数,完成一个任务就调用countDown()方法,主线程中使用await方法,等待所有任务完成主线程才结束;
java
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(111);
countDownLatch.countDown();
}
};
for (int i = 0; i < 10; i++) {
Thread t = new Thread(runnable);
t.start();
}
countDownLatch.await();
}
}
2,线程安全的集合类
我们之前学习的数据结构大部分是不安全的,我们还想使用之前的数据结构就要做相应的修改;
1)多线程环境使用ArrayList
1,使用ArrayList的第一种方式就是自己加锁,使用synchronized或者ReentrantLock,来对容易引发线程安全的地方来加以限制;
2,就是套壳
collections.synchronized(new ArrayList)
对于public的方法都加上synchronized;
3,使用CopyOnWriteArrayList
这个方法是不去加锁的,我们知道,读操作是不影响线程安全的,那么我们在使用ArrayList的时候,我们修改了,我们就再复制一个数组,我们读取的时候只能读到旧的数据或者是已经修改完成的数据,不存在读取修改一半的情况,但是,如果我们的数据很大很大呢,难道我们要一下复制所有的元素吗,是的,就是这么难受,并且多个线程修改数据的时候也可能会发生问题,那我们干嘛要用它,这个是存在特定的使用场景的,服务器如果修改配置了的话是需要重新启动的,我们玩游戏的时候,如果我们要修改设置,比如打开声音,或者设置按键等,难道我们还要关掉游戏吗,我们这时候就是我们给出指令,根据新的设置,服务器就会创建新的哈希数组,来代替旧的数组,完成配置文件的修改,而不是服务器的重启;
2)多线程环境下使用队列
-
ArrayBlockingQueue 基于数组实现的阻塞队列
-
LinkedBlockingQueue 基于链表实现的阻塞队列
-
PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
-
TransferQueue 最多只包含⼀个元素的阻塞队列
3)多线程环境下使用哈希表
哈希表,查找时间复杂度O(1)啊,这必选得拿到多线程中,我们之前讲过,Hashtable是线程安全的,但它只是对HashMap的所有方法加锁,效率肯定是不高的,我们有一个完美的替代品就是ConcurrentHashMap;
ConcurrentHashMap是对桶级别加锁,和HashTable不一样,更高效;

大家还记不记得的哈希表是咋样的了, 我们要解决哈希冲突,我们通常是在每个下标中构建链表或者是红黑树;如果链表太长了,我们还涉及到扩容操作;

ConcurrentHashMap是对每个下标都加锁的,锁对象就使用表头,当两个线程在不同的下标是,就不会发生锁竞争,当两个线程修改同一个下标时,就存在线程安全性问题了,因为有表头锁的存在就会发生竞争,成功避免了线程安全问题;另外,记录的元素个数size怎么办呢,两个线程同时增加数据,size也会有线程安全问题,还有加锁吗,忘了我们的AtomicIngter了吗,这个原子类也是很好用的呀,大家不要忘了;
还有最后一个哈希扩容问题,如果发生扩容就意味着和CopyOnWriteArrayLIst一样了,我们要把原来的数距全部复制过来,那肯定需要很多的时间,所以我们不会一次就把所有元素复制过去,我们会把每次put一些数据的过程中偷偷复制一些数据到新哈希表,就意味着我们把100%的任务分三开,每次执行别的操作都完成一点点的任务,直到扩容完全完毕;