CS-Notes/notes/笔记/Java 并发.md.txt
2018-02-22 14:47:22 +08:00

536 lines
22 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[TOC]
# 使用线程
有三种使用线程的方法:
1. 实现 Runnable 接口
2. 实现 Callable 接口
3. 继承 Tread 
实现 Runnable  Callable 接口的类只能当做一个可以在线程中运行的任务不是真正意义上的线程因此最后还需要 Thread 来调用。可以说任务是通过线程驱动从而执行的。
## 1. 实现 Runnable 接口
有一个 run() 方法需要实现
需要 Thread 调用 start() 来启动线程
```java
public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
    public static void main(String[] args) {
        MyRunnable instance = new MyRunnable();
        Tread thread = new Thread(instance);
        thread.start();
    }
}
```
## 2. 实现 Callable 接口
 Runnable 相比Callable 可以有返回值返回值通过 FutureTask 进行封装。
```java
public  class  MyCallable  implements  Callable<Integer> {
    public Integer call() {
        // ...
    }
    public  static  void  main(String[]  args) {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}
```
## 3. 继承 Tread 
同样也是需要实现 run() 方法并且最后也是调用 start() 方法来启动线程。
```java
class MyThread extends Thread {
    public void run() {
        // ...
    }
    public  static  void  main(String[]  args) {
        MyThread mt = new MyThread();
        mt.start();
    }
}
```
## 4. 实现接口 vs 继承 Thread
实现接口会更好一些,因为:
1. Java 不支持多重继承因此继承了 Thread 类就无法继承其它类但是可以实现多个接口。
2. 类可能只要求可执行即可继承整个 Thread 类开销会过大。
# Executor
Executor 管理多个异步任务的执行而无需程序员显示地管理线程的生命周期。
主要有三种 Excutor
1. CachedTreadPool一个任务创建一个线程
2. FixedThreadPool所有任务只能使用固定大小的线程
3. SingleThreadExecutor相当于大小为 1  FixedThreadPool。
```java
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++) {
    exec.execute(new MyRunnable());
}
```
# 基础线程机制
## 1. sleep()
**Thread.sleep(millisec)** 方法会休眠当前正在执行的线程millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。
sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
```java
public void run() {
    try {
        // ...
        Thread.sleep(1000);
        // ...
    } catch(InterruptedException e) {
        System.err.println(e);
    }
}
```
## 2. yield()
对静态方法 **Thread.yield()** 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。
```java
public void run() {
    // ...
    Thread.yield();
}
```
## 3. join()
在线程中调用另一个线程的 **join()** 方法,会将当前线程挂起,直到目标线程结束。
可以加一个超时参数。
## 4. deamon
后台线程(**deamon**)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。
当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。
main() 属于非后台线程。
使用 setDaemon() 方法将一个线程设置为后台线程。
# 线程之间的协作
- **线程通信**:保证线程以一定的顺序执行;
- **线程同步**:保证线程对临界资源的互斥访问。
线程通信往往是基于线程同步的基础上完成的,因此很多线程通信问题也是线程同步问题。
## 1. 线程通信
**wait()、notify()  notifyAll()** 三者实现了线程之间的通信。
wait() 会在等待时将线程挂起而不是忙等待并且只有在 notify() 或者 notifyAll() 到达时才唤醒。
sleep()  yield() 并没有释放锁但是 wait() 会释放锁。实际上只有在同步控制方法或同步控制块里才能调用 wait() 、notify()  notifyAll()。
这几个方法属于基类的一部分而不属于 Thread。
```java
private boolean flag = false;
public synchronized void after() {
    while(flag == false) {
        wait();
        // ...
    }
}
public synchronized void before() {
    flag = true;
    notifyAll();
}
```
**wait()  sleep() 的区别**
1. wait()  Object 类的方法 sleep()  Thread 的静态方法
2. wait() 会放弃锁 sleep() 不会。
## 2. 线程同步
给定一个进程内的所有线程都共享同一存储空间这样有好处又有坏处。这些线程就可以共享数据非常有用。不过在两个线程同时修改某一资源时这也会造成一些问题。Java 提供了同步机制以控制对共享资源的互斥访问。
### 2.1 synchronized
**同步一个方法**
使多个线程不能同时访问该方法。
```java
public synchronized void func(String name) {
    // ...
}
```
**同步一个代码块**
```java
public void func(String name) {
    synchronized(this) {
        // ...
    }
}
```
### 2.2 Lock
若要实现更细粒度的控制我们可以使用锁lock
```java
private Lock lock;
public int func(int value) {
    lock.lock();
    // ...
    lock.unlock();
}
```
### 2.3 BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现
- **FIFO 队列**LinkedBlockingQueue、ArrayListBlockingQueue固定长度
- **优先级队列**PriorityBlockingQueue
提供了阻塞的 take()  put() 方法如果队列为空 take() 将一直阻塞到队列中有内容如果队列为满  put() 将阻塞到队列有空闲位置。它们响应中断当收到中断请求的时候会抛出 InterruptedException从而提前结束阻塞状态。
**使用 BlockingQueue 实现生产者消费者问题**
```java
// 生产者
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
    private BlockingQueue<String> queue;
    public Producer(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is making product...");
        String product = "made by " + Thread.currentThread().getName();
        try {
            queue.put(product);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
```
```java
// 消费者
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable{
    private BlockingQueue<String> queue;
    public Consumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        try {
            String  product = queue.take();
            System.out.println(Thread.currentThread().getName() + " is consuming product " + product + "...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
```
```java
// 客户端
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Client {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>(5);
        for (int i = 0; i < 2; i++) {
            new Thread(new Consumer(queue), "Producer" + i).start();
        }
        for (int i = 0; i < 5; i++) {
            // 只有两个 Product因此只能消费两个其它三个消费者被阻塞
            new Thread(new Producer(queue), "Consumer" + i).start();
        }
        for (int i = 2; i < 5; i++) {
            new Thread(new Consumer(queue), "Producer" + i).start();
        }
    }
}
```
```html
// 运行结果
Consumer0 is making product...
Producer0 is consuming product made by Consumer0...
Consumer1 is making product...
Producer1 is consuming product made by Consumer1...
Consumer2 is making product...
Consumer3 is making product...
Consumer4 is making product...
Producer2 is consuming product made by Consumer2...
Producer3 is consuming product made by Consumer3...
Producer4 is consuming product made by Consumer4...
```
# 线程状态
JDK  1.5 开始在 Thread 类中增添了 State 枚举包含以下六种状态
1. **NEW**(新建)
2. **RUNNABLE**当线程正在运行或者已经就绪正等待 CPU 时间片
3. **BLOCKED**(阻塞,线程在等待获取对象同步锁)
4. **Waiting**调用不带超时的 wait()  join()
5. **TIMED_WAITING**调用 sleep()、带超时的 wait() 或者 join()
6. **TERMINATED**(死亡)
![](index_files/19f2c9ef-6739-4a95-8e9d-aa3f7654e028.jpg)
# 结束线程
## 1. 阻塞
一个线程进入阻塞状态可能有以下原因:
1. 调用 Thread.sleep() 方法进入休眠状态;
2. 通过 wait() 使线程挂起直到线程得到 notify()  notifyAll() 消息或者 java.util.concurrent 类库中等价的 signal()  signalAll() 消息;
3. 等待某个 I/O 的完成
4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。
## 2. 中断
使用中断机制即可终止阻塞的线程。
使用 **interrupt()** 方法来中断某个线程它会设置线程的中断状态。Object.wait(), Thread.join()  Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态并抛出 InterruptedException。
应当捕获这个 InterruptedException 异常从而做一些清理资源的操作。
**不可中断的阻塞**
不能中断 I/O 阻塞和 synchronized 锁阻塞。
**Executor 的中断操作**
Executor 避免对 Thread 对象的直接操作但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断所有它里面的所有线程shutdownNow() 方法会发送 interrupt() 调用给所有线程。
如果只想中断一个线程那么使用 Executor  submit() 而不是 executor() 来启动线程就可以持有线程的上下文。submit() 将返回一个泛型 Futrue可以在它之上调用 cancel()如果将 true 传递给 cancel()那么它将会发送 interrupt() 调用给特定的线程。
**检查中断**
通过中断的方法来终止线程需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true但是该线程不发生阻塞那么线程就永远无法终止。
interrupt() 方法会设置中断状态可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。
interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。
# 原子性
对于除 long  double 之外的基本类型变量的读写可以看成是具有原子性的以不可分割的步骤操作内存。
JVM  64 位变量long  double的读写当做两个分离的 32 位操作来执行在两个操作之间可能会发生上下文切换因此不具有原子性。可以使用 **volatile** 关键字来定义 long  double 变量从而获得原子性。
**AtomicInteger、AtomicLong、AtomicReference** 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。
```java
boolean compareAndSet(expectedValue, updateValue);
```
AtomicInteger 使用举例
```java
private AtomicInteger ai = new AtomicInteger(0);
public int next() {
    return ai.addAndGet(2)
}
```
原子性具有很多复杂问题,应当尽量使用同步而不是原子性。
# volatile
保证了内存可见性和禁止指令重排,没法保证原子性。
## 1. 内存可见性
普通共享变量被修改之后,什么时候被写入主存是不确定的。
volatile 关键字会保证每次修改共享变量之后该值会立即更新到内存中并且在读取时会从内存中读取值。
synchronized  Lock 也能够保证内存可见性。它们能保证同一时刻只有一个线程获取锁然后执行同步代码并且在释放锁之前会将对变量的修改刷新到主存当中。不过只有对共享变量的 set()  get() 方法都加上 synchronized 才能保证可见性如果只有 set() 方法加了 synchronized那么 get() 方法并不能保证会从内存中读取最新的数据。
## 2. 禁止指令重排
 Java 内存模型中允许编译器和处理器对指令进行重排序重排序过程不会影响到单线程程序的执行却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来进制指令重排即重排序时不能把后面的指令放到内存屏障之前。
可以通过 synchronized  Lock 来保证有序性它们保证每个时刻只有一个线程执行同步代码相当于是让线程顺序执行同步代码自然就保证了有序性。
# 可重入内置锁
每个Java对象都可以用做一个实现同步的锁这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获取锁并且在退出同步代码块时会自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。
重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:
```java
public class Father
{
    public synchronized void doSomething(){
        ......
    }
}
public class Child extends Father
{
    public synchronized void doSomething(){
        ......
        super.doSomething();
    }
}
```
子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。
由于Fither和Child中的doSomething()方法都是synchronized方法因此每个doSomething()方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的那么在调用super.doSomething()时将无法获得该Child对象上的互斥锁因为这个锁已经被持有从而线程会永远阻塞下去一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
同一个线程在调用本类中其他synchronized方法/块或父类中的synchronized方法/块时,都不会阻碍该线程地执行,因为互斥锁时可重入的。
# Concurrent
**Concurrent包里的其他东西ArrayBlockingQueue、CountDownLatch等等。**
CyclicBarrier  CountDownLatch 区别
这两个类非常类似都在 java.util.concurrent 都可以用来表示代码运行到某个点上二者的区别在于
*   CyclicBarrier 的某个线程运行到某个点上之后该线程即停止运行直到所有的线程都到达了这个点所有线程才重新运行CountDownLatch 则不是某线程运行到某个点上之后只是给某个数值 -1 而已该线程继续运行
*   CyclicBarrier 只能唤起一个任务CountDownLatch 可以唤起多个任务
*   CyclicBarrier 可重用CountDownLatch 不可重用计数值为 0  CountDownLatch 就不可再用了
# 免锁容器
免锁容器通用策略是:对容器的修改可以与读取操作同时发生,只要读取这只能看到完成修改地结果即可。修改实在容器数据结构的某个部分的一个单独的副本(又是是整个数据结构的副本)上执行的,并且这个副本在修改过程是不可视的。只有当修改完成后,被修改地结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改版本了。
**CopyOnWriteArrayList** 和 **CopyOnWriteArraySet** 在写入时都会导致创建整个底层数组的副本,而 **ConcurrentHashMap** 和 **ConcurrentLinkedQueue** 只会创建部分副本。
1).Java.util 包中
HashMap、 HashSet、 ArrayList 都不是线程安全的 Vector、 HashTable 是线程
安全的。通过 Collections.synchronizedList/Set/Map/SortedSet/SortedMap 可以返回
一个同步的对应集合
通过对每一个公共方法进行同步实现线程安全。但是在多线程环境下对它们
进行诸如“缺少即加入”这类符合操作时, 就会出现问题。【解决】: 客户端加锁,
要注意锁定的对象是对 List 等集合类而不是客户端类
【ConcurrentModificationException】
使用 Vector 或者同步的 List返回的 Iterator 迭代器都是 fail-fast  这意味
着如果在迭代过程中 任何其他线程修改 List都会失败 抛出上异常若想避
免该异常则必须在迭代期间对容器加锁。
2).Java.util.concurrent 
以下类在迭代期间都无需对容器加锁
CopyOnWriteArrayList  CopyOnWriteArraySet  ConcurrentHashMap 
ConcurrentLinkedQueue  ConcurrentSkipListMap(   SortedMap) 、
ConcurrentSkipListSet( 替代 SortedSet)
CopyOnWriteArrayList底层维护一个 volatile 的基础数组 某线程在对容器
修改的时候先显示获取锁(只能有一个线程修改), 然后复制基础数组(保证了
其他读线程不会出错 最后更新原数组因为是 volatile 所以对其他读线程
立即可见)。 因为复制数组开销较大,所以适合元素少,修改操作少的情况。
## HashTable、HashMap、ConcurrentHashMap 的区别
1). 主要区别:
1). 直观使用上 HashTable 不接受 key  null, HashMap 接受 key  null
2). 哈希冲突的概率不同 根据 Hash 值计算数组下标的算法不同 HashTable
直接使用对象的 hashCode hashMap 做了重新计算 HashTable 的冲突几率比
HashMap 
hashTable 默认的数组大小为 11 hashMap 默认数组大小为 16他们的默认
负载因子都是 0.75 HashTable 扩展为 2*size+1, HashMap 扩展为 2*size
3). 线程安全 HashTable 是线程安全的 HashMap 则不是线程安全的 , 但是
仅仅是 Hashtable 仅仅是对每个方法进行了 synchronized 同步 首先这样的效率
会比较低;其次它本身的同步并不能保证程序在并发操作下的正确性(虽然每个
方法都是同步的,但客户端调用可能在一个操作中调用多个方法,就不能保证操
作原子性了),需要高层次的并发保护。
2).ConcurrentHashMap 改进
并发效率问题 Hashtable  Collections.synchronizedMap 通过同步每个方
法获得线程安全。 即当一个线程执行一个 Map 方法时无论其他线程要对 Map
进行什么样操作,都不能执行,直到第一个线程结束才可以。对比来说,
ConcurrentHashMap 所使用的锁分段技术首先将数据分成一段一段的存储
后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他
段的数据也能被其他线程访问。 从而可以有效提高并发效率。
迭代问题 ConcurrentHashMap 返回的 iterator 是弱一致性的并不会抛出
ConcurrentModifiedException。 弱一致性的迭代器可以容许并发修改迭代器可以
感应到迭代器在被创建后,对容器的修改。
增加了常见的原子操作 API 它的 API 中包含一些原子形式的“putIfAbsent()、
相等便移除、 相等便替换”这些在 HashTable 中非原子操作
## ConcurrentHashMap 源码分析
[探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
# ThreadLocal
重点ThreadLocal的设计理念与作用ThreadPool用法与优势
[ThreadLocal 源码深入分析 ](http://www.sczyh30.com/posts/Java/java-concurrent-threadlocal/)
# 线程池
**线程池的底层实现和工作原理(建议写一个雏形简版源码实现)**
[线程、多线程与线程池总结](https://www.jianshu.com/p/b8197dd2934c)
# 多线程开发良好的实践
- 给线程命名;
- 最小化同步范围;
- 优先使用 volatile
- 尽可能使用更高层次的并发工具而非 wait  notify() 来实现线程通信 BlockingQueue, Semeaphore
- 多用并发容器,少用同步容器,并发容器壁同步容器的可扩展性更好。
- 考虑使用线程池
- 最低限度的使用同步和锁,缩小临界区。因此相对于同步方法,同步块会更好。
# 参考资料
- Java 编程思想
- [Java 线程面试题 Top 50](http://www.importnew.com/12773.html)
- [Java 面试专题 - 多线程 & 并发编程 ](https://www.jianshu.com/p/e0c8d3dced8a)
- [可重入内置锁](https://github.com/francistao/LearningNotes/blob/master/Part2/JavaConcurrent/%E5%8F%AF%E9%87%8D%E5%85%A5%E5%86%85%E7%BD%AE%E9%94%81.md)