diff --git a/README.md b/README.md index e7665ed9..c166b8ee 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ 整理自《深入理解 Java 虚拟机》,主要整理了内存模型、垃圾回收以及类加载机制。 +> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) + +只整理了一些比较基础的概念,之后会继续添加更多内容。 + > [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) 容器的一些总结,包含容器源码的分析。 diff --git a/notes/Java 并发.md b/notes/Java 并发.md new file mode 100644 index 00000000..dfe524b4 --- /dev/null +++ b/notes/Java 并发.md @@ -0,0 +1,450 @@ + +* [使用线程](#使用线程) + * [1. 实现 Runnable 接口](#1-实现-runnable-接口) + * [2. 实现 Callable 接口](#2-实现-callable-接口) + * [3. 继承 Tread 类](#3-继承-tread-类) + * [4. 实现接口 vs 继承 Thread](#4-实现接口-vs-继承-thread) +* [Executor](#executor) +* [基础线程机制](#基础线程机制) + * [1. sleep()](#1-sleep) + * [2. yield()](#2-yield) + * [3. join()](#3-join) + * [4. deamon](#4-deamon) +* [线程之间的协作](#线程之间的协作) + * [1. 线程通信](#1-线程通信) + * [2. 线程同步](#2-线程同步) + * [2.1 synchronized](#21-synchronized) + * [2.2 Lock](#22-lock) + * [2.3 BlockingQueue](#23-blockingqueue) +* [线程状态](#线程状态) +* [结束线程](#结束线程) + * [1. 阻塞](#1-阻塞) + * [2. 中断](#2-中断) +* [原子性](#原子性) +* [volatile](#volatile) + * [1. 内存可见性](#1-内存可见性) + * [2. 禁止指令重排](#2-禁止指令重排) +* [多线程开发良好的实践](#多线程开发良好的实践) +* [未完待续](#未完待续) +* [参考资料](#参考资料) + + + +# 使用线程 + +有三种使用线程的方法: + +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 { + public Integer call() { + // ... + } + public static void main(String[] args) { + MyCallable mc = new MyCallable(); + FutureTask 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 queue; + + public Producer(BlockingQueue 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 queue; + + public Consumer(BlockingQueue 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 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**(死亡) + +

+ +# 结束线程 + +## 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 来保证有序性,它们保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 + +# 多线程开发良好的实践 + +- 给线程命名; +- 最小化同步范围; +- 优先使用 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) diff --git a/notes/MySQL.md b/notes/MySQL.md index e3c5c88e..6cccf3f3 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -12,7 +12,7 @@ * [1. 索引分类](#1-索引分类) * [1.1 B-Tree 索引](#11-b-tree-索引) * [1.2 哈希索引](#12-哈希索引) - * [1.3. 空间索引数据(R-Tree)](#13-空间索引数据r-tree) + * [1.3. 空间索引(R-Tree)](#13-空间索引r-tree) * [1.4 全文索引](#14-全文索引) * [2. 索引的优点](#2-索引的优点) * [3. 索引优化](#3-索引优化) @@ -92,7 +92,7 @@ MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 **其它特性** -MyISAM 支持全文索引,地理空间索引; +MyISAM 支持全文索引,地理空间索引。 # 数据类型 @@ -172,7 +172,7 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 限制:哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;无法用于分组与排序;只支持精确查找,无法用于部分查找和范围查找;如果哈希冲突很多,查找速度会变得很慢。 -### 1.3. 空间索引数据(R-Tree) +### 1.3. 空间索引(R-Tree) MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 diff --git a/pics/19f2c9ef-6739-4a95-8e9d-aa3f7654e028.jpg b/pics/19f2c9ef-6739-4a95-8e9d-aa3f7654e028.jpg new file mode 100644 index 00000000..905b16ba Binary files /dev/null and b/pics/19f2c9ef-6739-4a95-8e9d-aa3f7654e028.jpg differ