1、容器
1.1、同步容器与并发容器
- 同步容器
Vector、HashTable – JDK提供的同步容器类
==VectorSingleThreadDemo ==
/**
* 使用单线程进行迭代,在迭代的过程中把里面的元素进行移除
*/
public class VectorSingleThreadDemo {
public static void main(String[] args) {
Vector<String> stringVector = new Vector<>();
for (int i = 0; i < 1000; i++) {
stringVector.add("demo"+i);
}
//错误的迭代
// stringVector.forEach(e->{
// if("demo3".equals(e)){
// stringVector.remove(e);
// }
// System.out.println(e);
// });
//正确迭代
Iterator<String> iterator = stringVector.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if("demo998".equals(next)){
iterator.remove();
}
}
}
}
Collections.synchronizedXXX 本质是对相应的容器进行包装
public class SynchronizedListDemo {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
List<String> strings1 = Collections.synchronizedList(strings);
}
}
==VectorMoreThreadDemo ==
/**
* 使用多线程进行迭代,在迭代的过程中把里面的元素进行移除
*/
public class VectorMoreThreadDemo {
public static void main(String[] args) {
Vector<String> stringVector = new Vector<>();
for (int i = 0; i < 1000; i++) {
stringVector.add("more"+i);
}
Iterator<String> iterator = stringVector.iterator();
for (int i = 0; i < 4; i++) {
new Thread(()->{
//使用多线程进行迭代的时候,使用synchronized对iterator(迭代器)进行加锁
synchronized (iterator){
while (iterator.hasNext()){
String next = iterator.next();
if("more998".equals(next)){
//两个线程同时进入这一行,都未执行。一个线程已经执行remove(),
// 另一个在执行remove时,发现不存在,就会报java.util.NoSuchElementException
iterator.remove();
}
}
}
}).start();
}
}
}
同步容器类的缺点
在单独使用里面的每一个方法的时候,可以保证线程安全,但是,复合操作需要额外加锁来保证线程安全,使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。想要避免出现ConcurrentModificationException,就必须在迭代过程持有容器的锁。但是若容器较大,则迭代的时间也会较长。那么需要访问该容器的其他线程将会长时间等待。从而会极大降低性能。
若不希望在迭代期间对容器加锁,可以使用"克隆"容器的方式。使用线程封闭,由于其他线程不会对容器进行修改,可以避免ConcurrentModificationException。但是在创建副本的时候,存在较大性能开销。
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate进行迭代操作,也即可能抛出ConcurrentModificationException。
- 并发容器
同步容器Vector不能使用for-each遍历容器移除元素,可以使用迭代器移除元素,但是要对迭代器加锁。在上面的同步容器多线程使用迭代器移除元素时需要加锁,否则报错
CopyOnWrite(不支持在迭代器移除相应的元素,使用for-each遍历容器移除元素可以)、Concurrent、BlockingQueue
根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
ConcurrentBlockingQueue:基于queue实现的FIFO的队列。队列为空,取操作会被阻塞
ConcurrentLinkedQueue:队列为空,取得时候就直接返回空
CopyOnWrite
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> strings = new CopyOnWriteArrayList<>();
for (int i = 0; i < 1000; i++) {
strings.add("copyon"+i);
}
//可以正常操作元素
// strings.forEach(e->{
// strings.remove(e);
// });
//不能正常操作元素,不支持。报java.lang.UnsupportedOperationException
// Iterator<String> iterator = strings.iterator();
// while (iterator.hasNext()){
// String next = iterator.next();
// if("copyon998".equals(next)){
// iterator.remove();
// }
// }
//多线程
for (int i = 0; i < 4; i++) {
new Thread(()->{
strings.forEach(e->{
if("copyon998".equals(e)){
strings.remove(e);
}
});
}).start();
}
}
}
1.2、LinkedBlockingQueue的使用
在并发编程中,LinkedBlockingQueue使用的非常频繁,因为它可以支持读写两个线程的并发操作。因其可以作为生产者消费者的中间商
add 实际上调用的是offer,区别是在队列满的时候,add会报异常
offer 对列如果满了,直接入队失败
put(“111”); 在队列满的时候,会进入阻塞的状态
remove(); 直接调用poll,唯一的区是remove会抛出异常,而poll在队列为空的时候直接返回null
poll(); 在队列为空的时候直接返回null
take(); 在队列为空的时候,会进入等待的状态
2、并发工具类
2.1、CountDownLatch
await(),进入等待的状态
countDown(),计数器减一
应用场景:启动三个线程计算,需要对结果进行累加。
2.2、CyclicBarrier–栅栏
允许一组线程相互等待达到一个公共的障碍点,之后再继续执行
跟countDownLatch的区别
CountDownLatch一般用于某个线程等待若干个其他线程执行完任务之后,它才执行;不可重复使用
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;可重用的
2.3、Semaphore–信号量
控制并发数量
使用场景:接口限流
2.4、Exchanger
用于交换数据
它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是【成对】的。
3、线程池及Executor框架
3.1、 为什么要使用线程池?
诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP、FTP )、通过 JMS队列或者可能通过轮询数据库。 不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。每当一个请求到达就创建一个新线程,然后在新线程中为请求服务,但是频繁的创建线程,销毁线程所带来的系统开销其实是非常大的。
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
风险与机遇
用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,
诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。
3.2、 创建线程池及其使用
3.3、 Future与Callable、FutureTask
Callable与Runable功能相似,Callable的call有返回值,可以返回给客户端,而Runable没有返回值,一般情况下,Callable与FutureTask一起使用,或者通过线程池的submit方法返回相应的Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。get方法会阻塞,直到任务返回结果
FutureTask则是一个RunnableFuture,而RunnableFuture实现了Runnbale又实现了Futrue这两个接口
3.4、 线程池的核心组成部分及其运行机制
corePoolSize:核心线程池大小 cSize
maximumPoolSize:线程池最大容量 mSize
keepAliveTime:当线程数量大于核心时,多余的空闲线程在终止之前等待新任务的最大时间。
unit:时间单位
workQueue:工作队列 nWorks
ThreadFactory:线程工厂
handler:拒绝策略
运行机制
通过new创建线程池时,除非调用prestartAllCoreThreads方法初始化核心线程,否则此时线程池中有0个线程,即使工作队列中存在多个任务,同样不会执行
任务数X
x <= cSize 只启动x个线程
x >= cSize && x < nWorks + cSize 会启动 <= cSize 个线程 其他的任务就放到工作队列里
x > cSize && x > nWorks + cSize
x-(nWorks) <= mSize 会启动x-(nWorks)个线程
x-(nWorks) > mSize 会启动mSize个线程来执行任务,其余的执行相应的拒绝策略
3.5、 线程池拒绝策略
AbortPolicy:该策略直接抛出异常,阻止系统正常工作
CallerRunsPolicy:只要线程池没有关闭,该策略直接在调用者线程中,执行当前被丢弃的任务(叫老板帮你干活)
DiscardPolicy:直接啥事都不干,直接把任务丢弃
DiscardOldestPolicy:丢弃最老的一个请求(任务队列里面的第一个),再尝试提交任务
3.6、 Executor框架
通过相应的方法,能创建出6种线程池
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService1 = Executors.newFixedThreadPool(2);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
ExecutorService executorService2 = Executors.newWorkStealingPool();
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();
上面的方法最终都创建了ThreadPoolExecutor
newCachedThreadPool:创建一个可以根据需要创建新线程的线程池,如果有空闲线程,优先使用空闲的线程
newFixedThreadPool:创建一个固定大小的线程池,在任何时候,最多只有N个线程在处理任务
newScheduledThreadPool:能延迟执行、定时执行的线程池
newWorkStealingPool:工作窃取,使用多个队列来减少竞争
newSingleThreadExecutor:单一线程的线程次,只会使用唯一一个线程来执行任务,即使提交再多的任务,也都是会放到等待队列里进行等待
newSingleThreadScheduledExecutor:单线程能延迟执行、定时执行的线程池
3.7、 线程池的使用建议
尽量避免使用Executor框架创建线程池
newFixedThreadPool newSingleThreadExecutor
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
newCachedThreadPool newScheduledThreadPool
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
为什么第二个例子,在限定了堆的内存之后,还会把整个电脑的内存撑爆
创建线程时用的内存并不是我们制定jvm堆内存,而是系统的剩余内存。(电脑内存-系统其它程序占用的内存-已预留的jvm内存)
创建线程池时,核心线程数不要过大
相应的逻辑,发生异常时要处理
submit 如果发生异常,不会立即抛出,而是在get的时候,再抛出异常
execute 直接抛出异常
4、jvm与并发
4.1、jvm内存模型
硬件内存模型
处理器--》高速缓存--》缓存一致性协议--》主存
java内存模型
线程《--》工作内存《--》save和load 《---》主存
java内存间的交互操作
(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(8)write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
上面8中操作必须满足以下规则
1、不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存。
4、一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6、如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7、如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
8、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
4.2、先行发生原则 happens-before
判断数据是有有竞争、线程是否安全的主要依据
1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
2. 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
3. volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
4. 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
5. 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
6. 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断
7. 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
8. 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。
为什么要有该原则?
无论jvm或者cpu,都希望程序运行的更快。如果两个操作不在上面罗列出来的规则里面,那么久可以对他们进行任意的重排序。
时间先后顺序与先行发生的顺序之间基本没有太大的关系。
4.3、指令重排序
什么是指令重排序?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
数据依赖性
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。)
两操作访问同一个变量,其两个操作中有至少一个写操作,此时就存在依赖性
写后读 a=0 b=a
读后写 a=b b=1
写后写 a=1 a=2
a=1,b=1
写后读 a=0 b=a 正确b=0 错误b=1
as-if-serial原则
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
x=0,y=1
x=1, y=0
x=1, y=1
x=0, y=0
5、实战
5.1、数据同步接口–需求分析
业务场景:
一般系统,多数会与第三方系统的数据打交道,而第三方的生产库,并不允许我们直接操作。在企业里面,一般都是通过中间表进行同步,即第三方系统将生产数据放入一张与其生产环境隔离的另一个独立的库中的独立的表,再根据接口协议,增加相应的字段。而我方需要读取该中间表中的数据,并对数据进行同步操作。此时就需要编写相应的程序进行数据同步。
数据同步一般分两种情况
全量同步:每天定时将当天的生产数据全部同步过来(优点:实现简单 缺点:数据同步不及时)
增量同步:每新增一条,便将该数据同步过来(优点:数据近实时同步 缺点:实现相对困难)
我方需要做的事情:
读取中间表的数据,并同步到业务系统中(可能需要调用我方相应的业务逻辑)
模型抽离
生产者消费者模型
生产者:读取中间表的数据
消费者:消费生产者生产的数据
接口协议的制定
1.取我方业务所需要的字段
2.需要有字段记录数据什么时候进入中间表
3.增加相应的数据标志位,用于标志数据的同步状态
4.记录数据的同步时间
技术选型:
mybatis、单一生产者多消费者、多线程并发操作
5.2、中间表设计
5.3、基础环境搭建
5.4、生产者代码实现
1:分批读取中间表(10I),并将读取到的数据状态修改为10D(处理中)
2:将相应的数据交付给消费者进行消费
1:把生产完的数据,直接放到队列里,由消费者去进行消费
2:把消费者放到队列里面,生产完数据,直接从队列里拿出消费者进行消费
6、总结
工作线程数是不是设置的越大越好?
调用sleep()函数的时候,线程是否一直占用CPU?
synchronized关键字可用于哪些地方
java中wait和sleep方法的不同
如果CPU是单核,设置多线程有意义么,能提高并发性能么?
手写单例 懒汉式 双重检查 为什么要加volatile关键字
分析问题时常用的命令 jps jstack jconsole
看过跟JUC下的那些源码,简单说说
线程池的核心组成部分及其运行机制