Skip to content

Commit 5bc2d4f

Browse files
authored
Update Java基础-多线程.md
1 parent 0b339c9 commit 5bc2d4f

File tree

1 file changed

+61
-25
lines changed

1 file changed

+61
-25
lines changed

MD/Java基础-多线程.md

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
3. 实现:在Java里如何实现线程,Thread、Runnable、Callable。
1212
4. 问题:线程可以获得更大的吞吐量,但是开销很大,线程栈空间的大小、切换线程需要的时间,所以用到线程池进行重复利用,当线程使用完毕之后就放回线程池,避免创建与销毁的开销。
1313

14-
### 线程同步/线程间通信的方式
15-
https://fangjian0423.github.io/2016/04/18/java-synchronize-way/
16-
https://github.com/crossoverJie/Java-Interview/blob/master/MD/concurrent/thread-communication.md
14+
### 线程间通信的方式
15+
1. 等待通知机制 wait()、notify()、join()、interrupted()
16+
2. 并发工具synchronized、lock、CountDownLatch、CyclicBarrier、Semaphore
1717

1818
###
1919
#### 锁是什么
@@ -40,23 +40,6 @@ notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞
4040

4141
**我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。**
4242

43-
### 有几种锁
44-
#### 悲观锁
45-
假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。synchronized关键字的实现也是悲观锁。
46-
#### 乐观锁
47-
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。原子变量类就是使用了乐观锁的一种实现方式CAS实现的。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
48-
#### 可重入锁
49-
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁
50-
#### 可中断锁
51-
可中断锁:顾名思义,就是可以interrupt()中断的锁。 在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
52-
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
53-
#### 公平锁
54-
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
55-
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
56-
#### 读写锁
57-
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
58-
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。
59-
6043
### volatile
6144
功能:
6245

@@ -95,10 +78,63 @@ new Thread弊端:
9578
2.2.2 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略
9679

9780
#### [阻塞队列的策略](https://blog.csdn.net/hayre/article/details/53291712)
98-
* 直接提交。SynchronousQueue,它将任务直接提交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSizes 以避免拒绝新提交的任务
99-
* 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性
100-
* 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
81+
* 直接提交。SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take。将任务直接提交给线程而不保持它们
82+
* 无界队列。当使用无限的 maximumPoolSizes 时,将导致在所有corePoolSize线程都忙时新任务在队列中等待
83+
* 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。
10184

10285
### 并发包工具类
103-
[http://www.wangtianyi.top/blog/2018/05/01/javagao-bing-fa-xi-lie-si-:juc/](http://www.wangtianyi.top/blog/2018/05/01/javagao-bing-fa-xi-lie-si-:juc/?utm_source=github&utm_medium=github)
104-
[https://blog.csdn.net/mzh1992/article/details/60957351](https://blog.csdn.net/mzh1992/article/details/60957351)
86+
#### CountDownLatch
87+
计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。
88+
89+
![](http://www.wangtianyi.top/images/blog/2018-04-28_3.png)
90+
图中,A为主线程,A首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。
91+
92+
使用场景:
93+
并行计算:把任务分配给不同线程之后需要等待所有线程计算完成之后主线程才能汇总得到最终结果
94+
模拟并发:可以作为并发次数的统计变量,当任意多个线程执行完成并发任务之后统计一次即可
95+
96+
#### Semaphore
97+
信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。
98+
99+
```java
100+
public class CountDownLatchTest {
101+
102+
public static void main(String[] args) {
103+
ExecutorService executorService = Executors.newCachedThreadPool();
104+
Semaphore semaphore = new Semaphore(3); //配置只能发布3个运行许可证
105+
for (int i = 0; i < 100; i++) {
106+
int finalI = i;
107+
executorService.execute(() -> {
108+
try {
109+
semaphore.acquire(3); //获取3个运行许可,如果获取不到会一直等待,使用tryAcquire则不会等待
110+
Thread.sleep(1000);
111+
System.out.println(finalI);
112+
semaphore.release(3);
113+
} catch (InterruptedException e) {
114+
e.printStackTrace();
115+
}
116+
});
117+
}
118+
executorService.shutdown();
119+
}
120+
}
121+
```
122+
123+
由于同时获取3个许可,所以即使开启了100个线程,但是每秒只能执行一个任务
124+
125+
使用场景:
126+
数据库连接并发数,如果超过并发数,等待(acqiure)或者抛出异常(tryAcquire)
127+
128+
#### CyclicBarrier
129+
可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类
130+
131+
![](http://www.wangtianyi.top/images/blog/2018-05-01_2.png)
132+
133+
与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。
134+
135+
与CountDownLatch的区别:
136+
1. CountDownLatch是一个线程等其他线程,CyclicBarrier是多个线程相互等待
137+
2. CyclicBarrier的计数器能重复使用,调用多次
138+
139+
使用场景:
140+
有四个游戏玩家玩游戏,游戏有三个关卡,每个关卡必须要所有玩家都到达后才能允许通过。其实这个场景里的玩家中如果有玩家A先到了关卡1,他必须等到其他所有玩家都到达关卡1时才能通过,也就是说线程之间需要相互等待。

0 commit comments

Comments
 (0)