实战Java高并发程序设计(葛一鸣,郭超)读书笔记
获取方式:
http://www.java1234.com/a/javabook/javabase/2018/1224/12627.html
一. Java线程概念
1. 同步与异步的区别
同步就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制
2. 并行和并发的区别
并发就是串行分割任务执行,并行是完全同时执行
如果只有一个cpu频率很高,把两件事情分割n,m份,穿插去执行,一下A,一下B,人看来就是感觉是A,B在同时执行,实际是并发
但如果有两个cpu,一个cpu跑A,一个CPU跑B,人看来也是A,B在同时进行,但这个是并行
3. 临界区:
指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。
4. 阻塞,非阻塞 :
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
5. 死锁 :
多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。
死锁产生的4个必要条件:
1、互斥:某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。
2、占有且等待:一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。
3、不可抢占:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
4、循环等待:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。
https://blog.csdn.net/wljliujuan/article/details/79614019
6 .饥饿 :
低优先级的线程长时间获取不到临界区运行而挂起的现象
7. 活锁 :
多个线程主动都互相谦让临界区给对方,导致都拿不到临界区运行的现象
8. 无锁 :
即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。
二. Java线程状态
1.线程的状态:
java/lang/Thread.java中有枚举出线程的6种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
2.线程状态切换:
三. Java线程的操作
1. 新建线程 :
new 一个thread并通过start起来
调用Thread的start方法和run方法的区别:
调用run方法是在当前调用线程中串行执行,相当于简单的调用了Thread的一个方法,未新开一个线程跑;
看start方法调用过程,的确是重新创建了一个新的线程在跑:
java/lang/Thread.java
public synchronized void start() {
boolean started = false;
try {
start0();
started = true;
} finally {
}
}
private native void start0();
libcore/ojluni/src/main/native/Thread.c
static JNINativeMethod methods[] = {
{"start0", "(JZ)V", (void *)&JVM_StartThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(Ljava/lang/Object;J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};
art/runtime/openjdkjvm/OpenjdkJvm.cc
JNIEXPORT void JVM_StartThread(JNIEnv* env, jobject jthread, jlong stack_size, jboolean daemon) {
art::Thread::CreateNativeThread(env, jthread, stack_size, daemon == JNI_TRUE);
}
runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->self;
Thread* child_thread = new Thread(is_daemon);
...
// 创建JNIEnvExt Try to allocate a JNIEnvExt for the thread.
std::unique_ptr<JNIEnvExt> child_jni_env_ext(
JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM(), &error_msg));
...
int pthread_create_result = 0;
if (child_jni_env_ext.get() != nullptr) {
...
// 创建线程
pthread_create_result = pthread_create(&new_pthread,
&attr,
Thread::CreateCallback,
child_thread);
CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");
...
}
}
2. 终止线程
Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程锁持有的锁。如果线程写数据写了一半被终止,其他线程就有可能获取到锁而可以读操作,结果读出来一个写了一半的错误值。
该方法已经Deprecated,如果不是很清楚确定stop操作不会带来负面影响,就不要使用了。
3. 线程中断
Thread.interrupt()方法调用后线程并不会立即退出,而是会给线程发一个通知,而目标线程收到通知后如何操作取决与目标线程自己的操作
如果收到中断后需要退出需要目标线程自己实现退出操作,如果中断收到通知后线程无条件stop退出的话就会遇到上面的风险。
在Thread.sleep()方法使用时需要catch一个InterruptedException,即中断异常处理,Thread.sleep() 抛出中断异常之后会清除中断标记,如果不加处理,在下一次循环时则无法处理这个中断,故在异常处理位置再次设置中断标记位。
另外,在捕获中断异常之后做一些数据正确性的回退或设置标志等操作来避免操作一半退出其他线程获得锁来读出错误数据的情况。
4. 等待wait()和通知notify()
wait()和notify()这两个方法不是Thread中的,而是object类中的,意味着任何对象都可以调用这两个方法。
如果一个线程调用来object.wait()方法,那么它就会进入该object的等待队列中,这个等待队列中可能有多个等待该object的线程;
当object.notify()调用之后,会从这个等待队列中随机选择一个线程并将其唤醒,这个选择是不公平的,不是谁先来谁先被调度,而是完全是随机的。object.notifyAll()则是唤醒这个队列中的所有线程。
object.wait()方法不能随意调用,需要在锁内,无论wait()还是notify()都需要先获得对应的锁才能操作。
object.wait() 和Thread.sleep()都可以让线程等待若干时间,但是不同是objetc.wait()会释放目标对象锁,而Thread.sleep()不会释放任何资源。
5. 挂起suspend()和继续执行resume()
废弃方法,由于suspend()在导致线程暂停的同时不会释放任何锁资源,会牵连其他线程获得锁。另外suspend()之后的线程状态依然是Runnable。
6. 等待线程结束(join)和谦让(yield)
在调用线程中调用T1的join()方法,调用线程会等待T1运行完毕退出后在继续执行下面的操作。join(long millis)可以指定等到超时。
join()的本质上让调用线程wait()在目标线程的对象实例上,当目标线程执行完,目标线程在退出志强调用notifyAll()方法通知等待线程继续执行。
yield()方法调用可以会使目标线程让出CPU:
yield是一个静态的原生(native)方法
yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
四. Java线程的同步手段
4.1 线程安全控制方法
一. volatile:
正确使用 Volatile 变量:https://www.ibm.com/developerworks/cn/java/j-jtp06197.html
- volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
- 对其修饰的变量操作不会进行指令重排
二. synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
让你彻底理解Synchronized:https://www.jianshu.com/p/d53bf830fa09
线程安全和锁Synchronized概念:https://blog.csdn.net/xlgen157387/article/details/77920497
三. ReentrantLock
重入锁:ReentrantLock 详解:https://blog.csdn.net/Somhu/article/details/78874634
- 有加锁就必须有释放锁,而且加锁与释放锁的分数要相同
- 中断响应
lock.lockInterruptibly(); // 以可以响应中断的方式加锁
Thread.interrupt() - 可以使用 tryLock()或者tryLock(long timeout, TimeUtil unit) 方法进行一次限时的锁等待
- 使用重入锁(默认是非公平锁)创建公平锁,维护了一个有序队列,实现成本高,性能较低。
四. Condition条件
和ReentrantLock配合使用,ArrayBlockingQueue即是两者配合实现
基本使用,具体可以看
java并发编程之Condition:https://www.jianshu.com/p/be2dc7c878dc
Java并发——使用Condition线程间通信:https://www.cnblogs.com/shijiaqi1066/p/3412346.html
- 基本接口
void await() throws InterruptedException //当前线程进入等待状态,直到被通知(signal)或者被中断时,当前线程进入运行状态,从await()返回;
boolean await(long time, TimeUnit unit) throws InterruptedException//
同样是在接口1的返回条件基础上增加了超时响应,与接口3不同的是:
可以自定义超时时间单位;
void signal()
唤醒一个等待在Condition上的线程;
void signalAll()
唤醒等待在Condition上所有的线程。
- 和object.wait()和notify()方法一样,当线程使用Contition.wait()和signal()时要先获得锁,在Contition.wait()调用之后会释放这把锁,在signal()调用之后系统会从当前contition对象的等待队列中唤醒一个线程,如过该线程获取到锁就开始执行了,所以一般signal()之后会主动去释放相关锁,让给唤醒的线程。
五. 信号量 semaphore
Java之 Semaphore信号量的原理和示例:https://blog.csdn.net/Emmanuel__/article/details/78586945
允许多个线程同时访问,Synchronized和ReentrantLock 一次只允许一个线程访问一个资源,而信号量可以指定多个线程,同时访问某一个资源。
- 构造信号量时指定准入数量
// 创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)
- 基本接口:
// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void acquire(int permits)
// 从此信号量中获取许可,在有可用的许可前将其阻塞。
void acquireUninterruptibly()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信号量中当前可用的许可数。
- 注意申请信号量使用acquire()操作,离开时务必要i使用release()释放该信号量,如果发生量信号量泄漏,acquire了但没有release便会导致可入线程越来越少。
六. ReadWriteLock
【Java并发】ReadWriteLock读写锁的使用:https://www.jianshu.com/p/9cd5212c8841
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
- 读写分离锁可以有效的帮助锁竞争,以提升系统性能
- 在读多写少的情况下性能提升很高
七. 倒计时器
两种常用的线程计数器CountDownLatch和循环屏障CyclicBarrier:https://blog.csdn.net/xlgen157387/article/details/78218736
CountDownLatch是一个非常实用的多线程控制工具类,称之为“倒计时器”,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
八. 循环栅栏
CyclicBarrier是另一种多线程并发控制使用工具,和CountDownLatch非常类似,他也可以实现线程间的计数等待,但他的功能要比CountDownLatch更加强大一些。
循环屏障CyclicBarrier以及和CountDownLatch的区别:
https://www.cnblogs.com/twoheads/p/9555867.html
九 . LockSupport
LockSupport里面的park和unpark:https://blog.csdn.net/anLA_/article/details/78635300
LockSupport:https://www.cnblogs.com/skywang12345/p/3505784.html
LockSupport原理剖析:https://segmentfault.com/a/1190000008420938
- 和object .wait()相比它不需要先获得某个对象锁,也不抛InterruptedException异常
- LockSupport使用了类似信号量机制,为每个使用它的线程都与一个许可,如果许可可用就立即返回并消费掉这个许可,如果许可不可用就阻塞;和信号量不同的是许可不可累加,许可永远只有一个。
- 处于park()挂起的线程状态是明确的WAITING状态。
十. ThreadLocal
线程私有,线程安全
十一.原子数 无锁线程安全整数
AtomicInteger,CAS指令进行操作,线程安全
AtomicReference
AtomicIntegerFieldUpdater
4.2 线程安全的类型
Java 中一些常用的线程安全类型:
类型 | 非线程安全 | 线程安全 | 备注 |
---|---|---|---|
数组 | Arraylist | CopyOnWriteArrayList | |
数组 | vector/stack | stack继承自vector,vector是线程安全的,stack也是 | |
map类型 | Hashmap | ConcurrentHashmap | |
map类型 | ConcurrentSkipListMap | ||
队列 | LinkedList | ConcurrentLinkedQueue | |
队列 | BlockingQueue |
五. 多线程管理:
5.1 守护线程的使用
当一个应用进程仅存在守护进程时,Java虚拟机就会自然退出。通过Thread.setDaemon()进行设置。
需注意设置需要在线程start()调用之前设置,否则只是一个普通的用户线程。
5.2 线程优先级
通过Thread.setPriority()方法进行设置,值从1到10,数字越大优先级越高。
5.3 线程组的使用
新建线程组对象,在之后创建线程可作为参数将一些线程指定为一组
- 线程组也有stop方法,会停止线程组中的所有线程,也会有Thread.stop同样的问题。
5.4 线程池的使用
JDK提供一套线程池框架 Executor
- 可以创建固定线程数量的线程池
- 可以实现计划任务 newScheduleThreadPool()
- 自定义线程创建:ThreadFactory
- 扩展线程池,ThreadPoolExecutor是一个可扩展的线程池,提供如下接口对线程池进行控制:
beforeExecutor()
afterExecutor()
terminated() - 线程池数量优化,需要考虑CPU数量内存情况等
- 线程池可能会吃掉线程中的异常,导致没有堆栈信息,解决方法:放弃submit方法采用execute方法
六. 锁优化及注意事项
1.减少持锁时间
2.减少锁粒度
3.锁分离
4.锁粗化
七. 并行模式
1. 线程安全的单例模式
2.不变模式
3. 消费者生产者模式
4. Future模式
5. 并行搜索&并行排序&并行算法
6. 并行流水线
7. 比较交换CAS
8. 乐观锁&悲观锁
----未完待续---