前言
最近有看到马士兵老师在B站上一个关于多线程的视频,在此把重要的知识点进行总结。
正文
多线程基础
1.synchronized 锁定的代码块中的代码越少,效率越高。
2.synchronized 锁定的是堆内存, 而不是对象的引用。 如果synchronized 放在方法上锁定的是 this 也就是当前的这个对象本身的堆地址, 如果synchronized 放在static的变量或者方法上,锁定的是Class对象。 所以当指向的堆内存发生了变化,之前用同样引用的将不再被锁。
3.volatile解决了数据可见性,但是并不能解决原子性问题,所以他是无法代替synchronized的。 另一个可以使用原子性的办法是使用AtomicXXX,例如说 AtomicInteger…,但是功能没有synchronzined好用。
举个简单的例子: 我们做 i++ 操作的时候,其实是线程不安全的,为了保证安全除了synchronized我们还可以使用AtomicInteger中的getAndIncrement 方法 或者是 incrementAndGet方法
4.不要以字符串做为锁定常量,如果a,b字符串值相同, a先锁定一块,b在锁定一块,有可能就会导致死锁。 根本原因是因为JVM有常量池,会缓存定义的字符串。
5.Thread 中wait 会释放锁, notify不会释放锁,这两个方法都是线程间通信的方法。但是这两个方法一定要慎用,使用不当会带来线程的逻辑混乱。
6.CountDownLatch 门闩机制,某一个线程一直等待latch.await,直到latch.countDown. 这个类主要应该于一个线程在等待其他所有的线程都变换了状态之后,等待的线程再往下执行。有点像typescript中的forkJoin方法。
7.如果写加锁,读不加锁,那么很有可能出现脏读的情况,这种情况我们通常使用copyOnWrite。
ReentrantLock 重入锁
1.ReentrantLock重入锁,这个类可以替代synchronized,但是是一把手工锁,一定要手工释放 。
2.ReentrantLock 与synchronized 不太一样的地方在于,可以通过tryLock进行尝试拿锁,如果拿到锁了应该做sth,如果没拿到应该做另外的sth,而不会像synchronized一样在那里死等。
3.ReentrantLock 可以指定为公平锁(想象一下这样的场景,5个线程同时等待另一个线程释放锁,如果锁释放之后,线程调度器不会去看这5个锁他们到底等了多久的差别,而是随机调度的,这是非公平锁),只需要在构造函数中传入ture即可
4.wait往往和while一起使用,而不是if,因为wait会释放锁,而在释放的时候,有可能有两个线程同时被叫醒,然后同时操作,就出了问题
5.想多线程并发的时候如果通知其他线程醒来,使用notifyAll。 如果使用reetrantLock的话,可以使用 Condition去规范具体让哪些线程醒来。 可以根据reentrantLock的newCondition()方法,得到一个Condition,如果有不同条件可以有不同个 Condition
6.ReentrantLock 与传统Thread 方法对比。
等待方法 | 通知方法 | 通知全部 |
---|---|---|
condition.await() | condition.signal() | condition.signalAl |
wait() | notify() | notifyAll() |
7.ThreadLocal 可以使每一个线程都有自己的一个空间区域,使用空间换时间。
并发容器
1.如果像保证list的size和remove共同的原子性(例如说一个购票系统),我们可以使用ConcurrentListQueue,poll 方法进行删除操作,如果 返回值为空则queue中已经取完了,这样先删后检测空的办法很好。
2.ConcurrentHashMap 代替原来的HashTable,原来是用一个范围较大的锁,锁定对象,现在替换他的对象使用的是范围更小的锁,每次锁定的是一个segment 参考文档 https://www.cnblogs.com/heyonggang/p/9112731.html
3.在高并发并且要求map拍好序的情况下,我们使用ConcurrentSkipListMap 代替TreeMap ConcurrentSkipListMap 插入效率低,查询速率高
4.CopyOnWriteList 写时复制,写的效率很低,但是读的效率很高,是在写的时候,复制一份list然后在新的上面加上元素,最后把引用指向新的list
5.Collections.synchronizedXXX() 可以包装一个不加锁的容器为加锁的容器。 XXX可以为任意容器 e.g. List,Set Map
6.高并发可使用两种Queue
1)ConcurrentLinkedQueue
2)BlockingQueue(LinkedBQ 它是无界队列,ArrayBQ它是有界队列,DelayQueue用于执行定时任务的)
Queue相关的三个方法
1)add 如果 queue满了,报错
2)offer queue满了,不报错,会返回boolean
3)put queue满了,阻塞等待queue中有被消费的
7.DelayQueue中有一个transform方法,如果没有消费者会一直被阻塞
8.SychronusQueue 容量为0, 没有办法进行add,只能通过put放进一个元素后被阻塞,等待被消费者消费
线程池
1.Executor: 线程池顶级接口,只有execute一个方法,传入Runnable作为参数
2.ExecutorService: 可以向其中扔任务 任务可以为runnable也可以为callable,有execute和submit方法
3.Callable: callable 中的call方法有返回值,而且可以抛出异常,这是他与runnable最大的区别
4.Executors:是一个工具类用于操纵Executor等
有五个方法可以初始化不同的线程池:
-
newFixedThreadPool
- 固定个数线程池
-
newCachedThreadPool
- 缓存线程池,默认情况下最大的值为int的长度,如果一个线程60s没有被调用,则会被自动销毁
-
newSingleThreadExecutor
- 单例线程池,此线程保证了线程的顺序性,像是一个队列一样,先进先出 FIFO
-
newScheduledThreadPool
- 定时任务的线程池
-
newWorkStealingPool
- 偷工作线程,一个开启多个线程,分别往其中各方有任务,如果早的线程执行完了,它会去拿取别人线程中的任务,就是这么勤劳能干。
5.ThreadPool:线程池的概念,相当于我们使用固定数量的工人,当它们的手头的活干完之后,我们不需要使用新的线程工人,而是继续使用刚刚干活的但是现在空闲的工人
6.Future 作为callable的返回值
7.下面的代码,是单线程和多个线程获取一个范围内所有的质数的例子:
package msb_013;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 算出一定范围内所有的质数
* @author luckyharry
*
*/
public class Test03_ParallelComputing {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//下面是通过一个线程进行工作,工作时间大概在12s左右
long start = System.currentTimeMillis();
List<Integer> list = getPrime(1,500000);
long end = System.currentTimeMillis();
System.out.println(end - start);
//使用了固定个数线程池
//下面是启动了6个线程进行工作,工作时间大概在6s左右,工作时间缩短了一般
//1.首先我们需要一个执行器服务,指定到底要开多少个线程
//2.然后我们将我们实现了callable类的具体类实例化出来,并把对象传递进执行器服务的submit方法中
//3.从submit的方法中,我们可以获取到Futrue对象,这个对象就是我们可以看见未来数据的入口。
//4.调用future.get(),我们可以获取到具体的每一个线程所得到的结果,也就是未来的数据,值得注意的是,这个方法是阻塞的
//好多个future.get方法有点像我们之前学过的countDownLatch的作用
ExecutorService service = Executors.newFixedThreadPool(6);
//每个区间的范围不同,是因为到后来,数越大,需要判断的时间越长。
MyTask taskA = new MyTask(1,200000);
MyTask taskB = new MyTask(200001,350000);
MyTask taskC = new MyTask(350001,450000);
MyTask taskD = new MyTask(450001,500000);
Future<List<Integer>> futureA = service.submit(taskA);
Future<List<Integer>> futureB = service.submit(taskB);
Future<List<Integer>> futureC = service.submit(taskC);
Future<List<Integer>> futureD = service.submit(taskD);
start = System.currentTimeMillis();
List<Integer> listA = futureA.get();
List<Integer> listB = futureB.get();
List<Integer> listC = futureC.get();
List<Integer> listD = futureD.get();
end = System.currentTimeMillis();
System.out.println(end - start);
service.shutdown();
}
/**
* 每一个任务的实现
* @author luckyharry
*
*/
static class MyTask implements Callable<List<Integer>>{
int start, end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public List<Integer> call() throws Exception {
List<Integer> list = getPrime(start, end);
return list;
}
}
/**
* 判断是否为质数
* @param number
* @return
*/
public static boolean isPrime(int number) {
for(int i =2; i<number/2; i++) {
if(number % i ==0) {
return false;
}
}
return true;
}
/**
* 获取质数
* @param start
* @param end
* @return
*/
public static List<Integer> getPrime(int start, int end){
List<Integer> list = new ArrayList<Integer>();
for(int i = start; i<= end; i++){
if(isPrime(i))list.add(i);
}
return list;
}
}
后记
我会在以后,不断对这篇文章进行更新,因为现在只是粗略的知识点整理。