高并发课程笔记+Java并发编程的艺术读书笔记

深入解析Java并发编程,涵盖synchronized、ReentrantLock、线程池、并发容器、原子类等核心概念,详解多线程安全、锁机制、线程间通信及高并发场景下的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

高并发课程笔记+Java并发编程的艺术读书笔记

高并发编程(马士兵)
高并发几大块知识点 synchronizer(同步器) 、同步容器、ThreadPool, executor

(一) 第一课

synchronized

如果Java程序中的synchronized明确指定了对象参数, 那就是这个对象的reference;
如果没有明确指定, 那就根据synchronized修饰的是实例方法还是类方法, 去取对应的对象实例或Class对象来作为锁对象。

第一种,这个锁的对象是new出来的,不干别的对象,就来当锁的对象,比较麻烦。
在这里插入图片描述
锁的信息记录在堆内存的对象里的,而不是他的引用。

第二种 this
在这里插入图片描述
当使用类的方法时,就自动new一个T出来,指向自身。把自身的对象锁定。

第三种 synchronized 修饰静态方法
在这里插入图片描述
相当于锁定T.class 这个对象。 T.class 是class的一个对象(反射)。由于静态的方法不需要new出一个对象就可以访问的,因此这里没有this,所以不能写synchronized(this) :由于没有对象!!!。

线程重入

在这里插入图片描述
在栈内存里 有一个t存在,指向堆内存空间,new出一个T对象。在T对象里面有一个count值。

(一)第二课

是否可以在同步方法运行期间,插入一个非同步方法?

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以
例如一个同步方法1,有锁,运行期间睡眠10秒,放弃cup,非同步方法2不需要锁,占用cpu去执行。执行完,方法1占用CPU继续执行。。

脏读问题

例子是在一个线程和一个主线程中,线程写,主线程读,出现脏读问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
脏读:对写进行了加锁,但是对读没有进行加锁。可能会读到在写的过程中没有完成的数据。首先写加锁,但是写的过程中睡眠1秒钟,cpu资源给了没有加锁的读方法,会读到没有可能会读到在写的过程中没有完成的数据。

结果可能是:
0
100
如何解决:在读的过程中也进行加锁。加锁后,读和写的方法的锁都是new出来的Account对象的锁,所以执行写方法时,锁被固定,只有写方法完成后,锁才释放,读的方法才能拿到锁,这时候才能读到已经写入完成的数据。

一个同步方法可以调用另一个同步方法吗?

第一种情况
在这里插入图片描述
new出来一个T对象,当调用m1方法时,对T对象进行加锁,在M1方法里继续调用了另一个同步方法m2,注意到属于同一线程,这时锁是可以重入的,也就是说锁又重新获取了一次,计数由1加到2. 同一个线程,同一把锁,所以可以

(一)第三课

一个同步方法可以调用另一个同步方法吗?

第二种方法
在这里插入图片描述
在这里插入图片描述
synchronized 锁定的是同一个对象:TT对象,子类的同步方法调用父类的同步方法。

synchronized 方法异常

当在synchronized 方法执行期间如果抛出异常,锁会被释放,这时候其他线程可能会进入同步代码区去读没有修改完成的数据。比如第一个线程的同步方法去修改name和balance,修改name 后捕获了一个异常,balance还没有被修改好时,锁已经被释放了,这时候其他线程会进入同步代码区,访问这个异常的数据。
在这里插入图片描述
在这里插入图片描述
m方法启动进入一个死循环,每一秒count加1,当count=5时,会抛出一个数学的异常。锁被释放,线程2才会启动。

在这里插入图片描述
如果不想释放锁,要加try catch 去catch这个异常进行正确的处理

volatile 关键字

在这里插入图片描述在这里插入图片描述
当不加volatile时,线程不可见,第一个线程t将主内存中的running=true 读到自己的工作内存中,开始死循环,虽然主线程也将主内存中的running=true读到自己工作内存中,然后修改为false,然后主线程结束,工作内存中的false被写到主内存中,但是线程t的工作内存不会从主内存重新读值。导致其一直用自己工作内存中的true的running,导致一直死循环下去。当加volatile时,当主线程的工作内存的false写入主内存时,主内存只要发现值变化,会通知其他线程,他们工作内存中的该值已经失效,需要重新从主内存中读取新的值。这样线程t才会被停止。这就是volatile的可见性。
在这里插入图片描述

volatile 的问题

在这里插入图片描述
在这里插入图片描述
10个线程去访问修改一个变量,这个变量保证了可见性,一个线程都加了10000,10个线程应该加100000才对,
在这里插入图片描述
为啥?
volatile只能保证可见性。不能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
 当需要使用被volatile修饰的变量时,线程会从主内存中重新获取该变量的值,但当该线程修改完该变量的值写入主内存的时候,并没有判断主内存内该变量是否已经变化,故可能出现非预期的结果。如主内存内有被volatile修饰变量 a,值为3,某线程使用该变量时,重新从主存内读取该变量的值,为3,然后对其进行+1操作,此时该线程内a变量的副本值为4。但此时该线程的时间片时间到了,等该线程再次获得时间片的时候,主存内a的值已经是另外的值,如5,但是该线程并不知道,该线程继续完成其未完成的工作,将线程内的a副本的值4写入主存,这时,主存内a的值就是4了。这样,之前修改a的值为5的操作就相当于没有发生了,a的值出现了意料之外的结果。

根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
如何解决 加锁!

(一)第四课

原子类

在这里插入图片描述
在这里插入图片描述
AtomicInteger :原子性操作的interger类型。m()方法不需要加锁, count.incrementAndGet 为原子方法。该方法是系统相当底层的实现,所以效率比synchronized高好多,类似于count++, 只要是Atomic开头的类的,其中的方法都是原子性的。
注意 如果Atomic类的两个原子方法之间没有加锁,就不能保证两个方法的原子性。

synchronized 优化

在这里插入图片描述
在这里插入图片描述

避免将锁定对象的引用变成另外的对象

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
再次强调锁是锁在堆内存new出来的对象上的,而不是锁在栈内存的引用变量上的!!!
线程1 启动后一直进行死循环,虽然cup会被让出主线程来启动线程2,但是线程2一直不会被执行,由于线程1的加锁方法,它不会释放锁。但是通过t.o=new object0;来创建一个新的对象。这时候m()方法中的synchronized(o)锁定的是另外一个新的对象,这时候由于这个对象的锁是可用的,线程2 拿到o的锁开始执行。为啥打印出t2后还会打印t1 ??难道是由于两个m()方法锁定的对象不一样导致的?线程1的m锁定的对象1,线程2的m锁定的对象2. 难道线程1的m锁定的对象1不会通过t.o=new object0 改变成对象2吗???

不要用字符串常量作为锁定对象

在这里插入图片描述
虽然s1 和s2 是不同的引用变量,但是字符串“hello”值相同,指向同一堆空间,也就是说,s1和s2指向的对象相同。这就引发了m1(s1)锁住“hello”对象不放,而m2(s2)拿不到锁 不能执行方法。

(一)第五六课

第一个版本,错误
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述原因:没有加volatile 不可见!
第二版本
在这里插入图片描述
缺点:浪费cpu,能做到当数为5时,通知线程2开始执行就行了。并且不精确,通过加锁解决。
第三版本 (wait notify 大公司经常问到)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
只有先加锁,才能调用wait和notify的方法。wait和notify是调用被锁定对象的wait方法和notify方法。调用方法wait()时,该方法进入等待状态,同时释放锁。让别的线程进入。然后通过其他线程中调用notify(),叫醒等待状态的一个线程。 notify不会释放锁

先启动线程2,只要数不为5,就让线程2进行等待。当数为5时,调用notify()进行唤醒线程2.
思考为啥是t1结束时,才输出t2 语句?。由于线程2虽然被唤醒。但是由于notify不释放锁,线程2得不到锁,所以无法执行,只有当T1运行完释放锁后,T2才会拿到锁去执行。

第四版

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
首先t2启动,等待,然后t1启动, t1 线程等到5时 进行notify去唤醒t2,同时t1进行wait来释放锁。t2拿到锁后执行完后一定要加notify去唤醒t1,这时候t2不需要再次wait来释放锁,由于t2执行完可以自动释放锁!! 这样t1才可以继续执行!!!这样通信比较复杂。

第五版
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
利用门栓,CountDownLatch latch=new CountDownLatch(1),栓一个数,当1变为0,门栓就开了。利用CountDown方法,减一。
首先当不是5时,线程2利用latch.await() 方法将门栓栓上进行等待,这时候没有锁,当线程1加到5时,将门栓拿走,这时候线程2会继续执行。这时候线程1也是可以继续执行的,
问题:门栓打开后,到底线程1执行还是线程2执行??

(二) 第一二三课

ReenTrantLock

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意首先创建一个锁对象,Lock。然后当想要执行同步代码时,利用lock.lock()方法手动加锁(一般写在try()外面),相当于 synchronized(this),注意 ,一定要手动释放锁!!!!,一般在finally语句中通过unlock()方法手动释放锁。注意m1和m2锁定的是同一个对象。

ReenTrantLock与synchronized的区别

在这里插入图片描述
在这里插入图片描述

trylock()

trylock()第一种方式:括号里没有参数,对锁对象进行尝试锁定,也就是说,可能拿到锁,也可能拿不到锁,如果拿到锁返回true,如果拿不到锁返回false。实际应用中,可以根据自己的逻辑,如果拿到锁怎么样,拿不到锁怎么办。
在这里插入图片描述

trylock()第二种方式:更常用。括号里有参数,意思是尝试锁定5秒钟,如果没有锁定就怎么样,如果锁定了就怎么样。
在这里插入图片描述
在这里插入图片描述

lockInterruptibly

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
线程1输出一个t1.start hou,开始长时间睡眠,由于睡眠不会释放锁。这时候线程2也想申请该锁,如果利用之前的lock.lock()申请的话,只要申请不到就会一直等(等到死)。但是如果使用lock.lockInterruptibly()方法,可以打断线程2对该锁的等待。也就是说,线程1一直拿着锁占据着某个资源,线程2也想申请该锁拿资源,如果它等了一段时间后,主线程可以打断线程2的等待。抛出一个异常。

公平锁

默认的synchronized都是不公平锁。公平锁是谁等的时间长,谁优先得到这把锁,一般性能低。不公平锁是不管你等的时间长短,谁得到这把锁由线程调度器 决定。因为不用计算各线程等待的时间,因此效率性能较高。
在这里插入图片描述
在这里插入图片描述
new ReentrantLock(true) 表示公平锁,如果没有参数表示不公平锁。

在这里插入图片描述
结果为线程1和2交替得到锁。非常公平。

生产者消费者模式

第一种
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
阻塞式的同步容器。如果容器满了,存的方法要等待,如果容器空了,取的方法要等待。再次是多线程访问容器。要求共享的数据不能出错。

两种方式,一个是wait notify 一个是condition。
很多公司问这个经典的问题,要熟练背诵。
其中有几个陷阱需要 注意:
第一个:为啥用while 而不用if 在《effective Java》中写道绝大部分的wait都是与while一起使用的。
解释:如果使用if 假设容器是满的,一个生产者1进行等待,当一个消费者拿走一个,然后生产者2拿到锁,进行添加一个,这时容器又变满了,这时候生产者1又拿到锁,此时接着刚才wait()后面的代码执行加1,这时会出现错误 。
如果使用while,当刚才的情况,生产者1拿到锁时,由于在循环内,这时它还会再次进行一次循环去判断容器是否满!!!.
第二个:使用notifyall而不是notify。如果使用notify,如果叫醒的还是生产者就还是满的。就开始wait(),这时候线程就死了。

第二种使用lock 和condition来写,可以更精确地指定哪些线程被唤醒。
在这里插入图片描述
在这里插入图片描述

(二)第四课

线程局部变量

在这里插入图片描述在这里插入图片描述
线程2启动会把名字改掉,因为这个名字是volatile,这时候线程1就会知道名字被改变。因此会输出“lisi”
如果线程1的变量不想被别的线程修改,需要利用线程局部变量。
在这里插入图片描述
在这里插入图片描述
线程局部变量就是我的线程的变量只能我自己用,其他的线程不能用。线程2创建了一个局部变量person,线程1得不到线程2person的值。

(三)Java的并发容器类

(三)第一二课

线程的单例模式

面试经常被问到 单例模式介绍

创建线程安全的单例模式,

对于之前的博客介绍的四种方法,下面是第五种方法,使用内部类,不用加锁,而且可以实现懒加载。

在这里插入图片描述
在这里插入图片描述
无论有多少个线程调用静态方法getSingle() ,只能拿到同一个对象。通过类Singleton里面的一个私有的静态内部类,构建出一个静态对象。然后通过getSingle方法返回内部类的静态对象。

高并发容器开头

火车站卖票第一版

在这里插入图片描述
在这里插入图片描述
上个程序有问题,有可能卖重复,有可能卖超量。使用的容器是ArrayList 它的各种方法比如remove都不是同步的。

火车站卖票第二版(Vector容器)

使用同步容器Vector 它的所有方法都是加锁的。
在这里插入图片描述
还是有问题,虽然size和remove的方法都是原子性的,但是size方法和remove方法之间不是原子性的,可能被打断。比如还有最后一张票,线程A判断size>0,然后被打断。随后线程B判断size>0 然后取票,现在票没有了,然后线程A继续执行,由于之前已经判断了size>0了,所以直接remove,出问题。

火车站卖票第三版(LinkedList +synchronized)

在这里插入图片描述
利用synchronized,将判断和销售都加锁,这样不会出现问题。

火车站卖票第四版(并发容器 ConcurrentLinkedQueue)

支持多线程。
在这里插入图片描述
利用poll的原子性方法,从头head拿走一个票,如果拿走成功再打印,拿走不成功就返回null。
为啥操作和打印还是分开不加锁的,不会出问题?
之前的程序是先判断,再操作。会出问题。现在是直接拿,然后打印,不会出问题。
例如还有最后一张票,线程A拿走后票后,被打断。线程B发现票没了,被break。然后A继续执行打印“销售了”,不会出问题。
这时候没有加锁,效率较高。
如果并发容器搞得清楚,进大公司比较容易

(三)第三四课

并发容器

Concurrent 类的Map

在这里插入图片描述
在这里插入图片描述
启动100个线程,每个线程向Map里添加10000个元素。所以一共添加100万个随机数。主线程等着,等 添加完元素后,门闩打开,主线程继续执行计算结束时间,看哪个容器添加的最快。
通过实验发现,ConcurrentHashMap比Hashmap效率高,因为HashMap 每个方法都是加锁的,锁住的是整个容器的对象。ConcurrentHashMap也是加锁,但是它把容器分成几段,每个线程都是向容器的一小段里添加元素,所以上锁的对象是容器的一小段,而不是整个容器。。所以多线程可以同时向容器里添加元素。而HashMap只能有一个线程向容器添加元素。

如果高并发并且需要排序的话,用ConcurrentSkipListMap() 跳表数据结构。

写时复制容器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ArrayList 最快,没有锁,所以会出现并发问题。
Vector 加锁了,不会出现并发问题。但是效率比ArrayList低。
CopyOnWriteArrayList 写最慢,不会有并发问题。

ConcurrentLinkedQueue

并发容器里用的最多的容器是Queue,高并发下经常使用两个,一个是 ConcurrentLinkedQueue(并发的,加锁的),另一个是LinkedBlockingQueue (阻塞式队列)。
在这里插入图片描述
第一个用法offer方法,类似于add,之前ArrayQueue的add方法有最大容量,如果超了会报错。使用Offer方法会返回一个布尔值,true就是加成功了,false相反。
concurrentLinkedQueue()是无界队列,类似于链表的结构,是单向的
使用offer进行加,
使用poll进行拿,从head拿并删除。
使用peek进行拿,但是不删除。
还有双向队列 ConcurrentLinkedDeque 看API文档。

LinkedBlockingQueue

无界队列
在这里插入图片描述
多了一个put()方法,加不进去会阻塞。

ArrayBlockingQueue

有界队列
在这里插入图片描述
几个方法之间的比较:
add() 加不进去会抛异常
offer 加不进去不会抛异常,会返回一个boolean类型的值。加不进去返回false
offer(,时间) 意思是加这个元素进去,加1秒钟,如果1秒钟加不进去就不加了,返回一个布尔值。
put() 加不进去不会抛异常,一直加不进去就一直阻塞。

DelayQueue

在这里插入图片描述
在这里插入图片描述
用于执行定时任务,MyTask必须实现Delayed 接口 复写comepareTo() 和getDelay() 方法。

TransferQueue

在这里插入图片描述
其中有一个特殊的方法 transfer()
它用于更高并发的情况,生产者加进去的消息不给队列,直接给消费者,这样效率更高。
这时候应该消费者先启动,如果生产者先制作,找不到消费者 这时候线程会阻塞。也就是说,我制造的东西你要给我消费掉,不然我不走了。(阻塞)

synchronusQueue(同步队列)

特殊的transferQueue。transferQueue是有一定容量的,如果没有消费者,还是可以通过add put offer 等方法将元素加进队列里。但是synchronusQueue 是容量为0的队列。
在这里插入图片描述
如果使用add,会抛出异常,由于容量为0;
使用put不会抛出异常,由于他直接交给了消费者去消费,其实内部方法调用的还是transfer。如果没有消费者,或者消费者在忙,那么put会阻塞。

在这里插入图片描述
map或者set
并发性不高,使用hashmap,treemap,linkedhashmap.
并发性很高,使用concurrenthashmap(无序) 或者concurrentskiplistmap(有序)。
队列
无并发 使用ArrayList或者LinkedList
高并发使用ConcurrentLinkedQueue(并发加锁) 或者BlockingQueue(阻塞)
特殊的:写少读多使用 CopyOnWriteList
DelayQueue执行定时任务。

(四)Java的线程池框架

Executor()

先从Executor 接口说起。
在这里插入图片描述Executor接口里只有一个execute方法,实现该接口的都要重写这个方法,
在这里插入图片描述
第一种 直接调用runnable的run方法 属于方法调用。
第二种,可以new一个 Thread 进行start。

callable()接口

在这里插入图片描述
这个和Runnable接口很像,前者只有一个call方法 后者只有一个run方法。很像。
Runnable 没有返回值
Callable 有返回值,返回值是泛型可以自己指定。

Executors 工具类

是操作Executor 的一个工具类。里面有大量的静态方法。

线程池

在这里插入图片描述
在这里插入图片描述
所有的线程池都实现了ExecutorService 接口,之前说了Executors工具类里有很多方法,这里的newFixedThreadPool 返回创建了容量为固定为5的线程池,也就是说里面有5个线程。

实现了ExecutorService 接口是可以向里面扔任务的,可以扔不返回值的任务Runnable 用execute方法.
也可以扔有返回值的callable 用submit 方法。

代码是扔了6个任务,每个任务先睡眠 然后打印线程的名字。
也就是说有一个线程池里有5个线程,扔了6个任务进去,所以有5个任务是active task 一个任务是queued task (任务队列,扔到线程池的消息队列里,当有线程空闲时,会执行消息队列里排队的任务)。当一个线程执行完任务时,它是不会消亡的,还会可以执行新派给他的任务。线程重用。新建线程和消灭线程都是消耗系统资源的。
shutdown() 关闭线程池,前提是执行完任务关闭
isTerminated() 任务是否执行完?
isshutdown () 线程池是否关闭,意思是是否正在关闭?
在这里插入图片描述从上图知,1,2,5,4,3 线程分别执行前五个任务,然后线程1执行挖第一个任务后,又执行了第六个任务。
一个线程池维护两个队列,一个是未完成的任务队列,一个是已经完成的任务队列。

Future

FutureTask

在这里插入图片描述
在这里插入图片描述
FutureTask 是包装了callable的一个类,泛型里的类型是返回值的类型(相当于new出一个callable类型,然后这个类型返回值为Integer,然后把它包装成一个FutureTask)。这个FutureTask就是让线程睡500毫秒,然后返回一个Integer型的1000.
随后启动线程,通过task.get()这个阻塞式的方法打印出1000(没有执行完,主线程就在这里阻塞,一直等到任务执行完才打印)。
为什么要将callable包装成FutureTask?,由于在线程中不能扔callable任务,只能扔FutureTask。而且callable 也没有get方法(FutureTask的方法)阻塞得到返回值。

通过service.submit 扔进去带返回值的callable的任务,任务的返回值存在Future里。因此可以不使用futuretask。
代码是向一个容量为5的线程池中扔进去一个任务。睡500毫秒,返回1.
如果调用f.get() 会打印出返回值。然后调用isDone 会返回true
如果不调用f.get() ,直接调用isDone 可能会返回false。

利用线程池进行并行计算

在这里插入图片描述在这里插入图片描述
代码是求1到20万之间的质数,线程池的好处就是并行计算,提高效率。

CashedPool

在这里插入图片描述
之前的 newFixedThreadPool 是固定容量的线程池,现在的CashedPool的线程池开始线程数为0,来一个任务启动一个线程,再来一个任务再启动一个线程,再来一个任务,判断当前线程池里是否有空闲的线程,如果有就让空闲的线程去执行第三个任务,不用新启动新的线程;如果没有就启动第三个线程。。。。一共可以启动多少个线程?直到系统无法承担的线程数为止。如果一个线程空闲时间超过他的生存周期60秒,那么他会自动销毁。

newSingleThreadExecutor

在这里插入图片描述
线程池只有一个线程,无论向里面扔几个任务,只有一个线程执行任务。这种可以保证任务的前后顺序执行。前面的多线程不能保证任务的执行顺序。

newScheduledThreadPool(定时器线程池)

在这里插入图片描述
在这里插入图片描述
这个方法是一个线程池的线程执行每隔多少时间执行一个任务。有四个参数,第一个参数代表任务,第二个代表第一个任务执行的时候开始延迟多长时间,第三个参数代表每隔多久执行下一个任务,第四个代表时间单位。
(这里的线程可以复用)
所以代码的意思是有一个任务,用线程池中的4个线程去执行,马上开始执行(0),每隔500毫秒执行一次(500)

在这里插入图片描述

WorkStealingPool

任务窃取
跟之前的线程池维护一个任务队列不同,WorkStealingPool线程池中的每一个线程都维护自己的一个任务队列,假设线程池里有三个线程123,线程1有ABC三个任务,线程2有D一个任务,线程3有EF两个任务,当线程2执行完D任务后,他会从线程1或者3的任务队列里去偷任务执行。之前的线程都是分配任务,干完了就空闲,现在的线程干完活 自己找活干。他根据电脑CPU是几核去启动合适数量的线程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由于是精灵线程,所以主线程不阻塞的话,看不到输出,也就是说,如果不注释system.in.read() ,结果只会打印4,其他的不会被打印。精灵线程一直在后台运行,只要虚拟机不退出,精灵线程就不会退出。

在代码里,第一个任务给第一个线程,第二个任务给第二个线程,第三个任务给第三个线程,第4个任务一定是第一个任务。因为第一个任务一定先执行完。

ForkJoinPool(最难)

也是精灵线程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
有一个大任务,可以分解成小任务,小任务可以分解成更小的任务,可以指定,然后计算出来再合并,ForkJoinPool根据任务的分解规则,自己去维护线程池里有几个线程。
执行的任务一定是ForkJoinTask,它可以切分
在这里插入图片描述

一般从ForkJoinTask 去继承,但是一般都从递归任务 RecursiveAction 和 RecursiveTask 去继承,前者无返回值,后者有返回值。

先用ForkJoinAction
在这里插入图片描述
如果需要返回值,从ForkJoinTask 继承 是返回值类型 join() 方法是阻塞似的,只有子任务都计算出来 才会合并。为啥最后要用task.join() 方法 由于返回的值都是一个一个小于50000 长度子任务的计算值,所以最后要把这些子任务的值都加起来。

线程池背后的原理

之前的六种线程都是底层调用ThreadPoolExecutor 这个类,线程池通用类
在这里插入图片描述
第一个参数 核心线程数 第二个参数最大线程数,第三个参数存活周期,第四个参数 时间单位,第五个参数任务队列。 之前的六种线程都可以看API 看他们的这四个参数是怎么设定的。

在这里插入图片描述

《Java并发编程的艺术》读书笔记

第一章

么并发执行的速度会比串行慢呢? 这是因为线程有创建和上下文切换的开销。

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

无锁并发编程:将数据ID按照Hash算法取模分段,不同线程处理不同段数据
CAS算法:Java的Atomic包使用CAS算法来更新数据
使用最少线程:避免创建不必要的线程
协程:单线程里实现多任务调度

避免死锁的几个常见方法。
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
使用定时锁,lock.tryLock(timeout)来替换使用内部锁机制
对于数据库锁,加锁解锁必须在一个数据库连接里,否则会出现解锁失效的情况

第2章 Java并发机制的底层实现原理

周阳 JUC

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ArrayList 线程不安全集合
在这里插入图片描述
在这里插入图片描述
CopyOnWriteList 的源码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Hashset 线程不安全集合 两个解决方式:
在这里插入图片描述
底层:hashmap
在这里插入图片描述
HashSet的add方法就是调用的HashMap的put方法。key就是传入的e Value就是固定写死的常量
在这里插入图片描述
HashMap的底层 线程不安全集合 两个解决方式
在这里插入图片描述
在这里插入图片描述

第三种建线程:实现Callable 接口
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
new的线程中要传入一个Runnable 接口的引用。不能传Callable接口的引用,怎么办?借助一个叫FutureTask的接口,这个接口实现了 Runnable 接口,且这个接口的构造方法中传入了一个Callable的引用 这样就将callable和Thread联系起来了。
在这里插入图片描述

CountDownLatch
在这里插入图片描述
在这里插入图片描述
CyclicBarrier
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
WriteReadLock
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
写的时候保证了数据的完整性,每一次只有一个线程写,但是读的时候可以并发去读。

阻塞队列
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
红色的要掌握 黑色的了解
在这里插入图片描述
ThreadPool 线程池
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
单个线程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程池7大参数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
都不使用,自己创建线程池。也就是说 不使用ExectorService 类和 Exectors类,而使用ThreadPoolExector类自己创建。
在这里插入图片描述
自己创建实例;
在这里插入图片描述
必须要写 如果不写的话 就是Int的最大数

最大可以容纳5+3=8个任务。第九个就会报错,会报拒绝执行的异常

拒绝策略:
在这里插入图片描述
在这里插入图片描述
第二种:
在这里插入图片描述
一共10个 main做一个 线程池还是做最大的8个 然后有一个丢失
第三种
在这里插入图片描述
线程池做8个 丢两个任务

最大线程数:CPU密集型,最大线程数=CPU核数+1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值