第1章 并发编程的挑战
1.1 上下文切换
并发:多线程对相同资源进行竞争,对同一个变量同时进行读写操作
并行:对不同变量进行读写操作,没有竞争
并发有资源竞争,并行没有资源竞争
有些线程执行结束后即消失,有些线程会一直存在。
linux时间片等长,一般为20ms,windows时间片不等长。
在多线程的CPU下,线程切换达到1ms/1次。
进程进入就绪态是我们自己决定。
Q:多线程一定比单线程快吗?
A: 不一定,在没有CPU浪费的情况下,多线程比单线程慢。
Q:如何查看进程信息?
**A:**用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么。
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
// 切换管理员权限,在/opt/ifeve/java/bin/ 目录下执行jstack命令,传31177参数,创建/home/tengfei.fangtf/dump17 文件并且将线程信息传进去
grep指令(查询):
grep ' + 要查询的内容 + ' + 查询的文件 //查询内容
grep -C 1 ' + 要查询的内容 + ' + 查询的文件 //查询内容和上下一行
grep -B 1 ' + 要查询的内容 + ' + 查询的文件 //查询内容和下一行
grep -A 1 ' + 要查询的内容 + ' + 查询的文件 //查询内容和上一行
grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
//查询dump17文件以 awk '{print $2$3$4$5}' 格式输出,按文件名排序,
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
1.2 死锁
synchronized()方法【加锁关键字】锁的只能是引用类型,不能是基本类型。
synchronized(){...}方法执行完毕开锁。
线程加锁后,只能由该线程对该资源进行读写,其他线程均没有权限对其进行读写。
即使CPU分配给该线程的时间片运行结束,CPU去运行其他线程,该锁也不会消失。
进入阻塞队列锁不释放。
Q:请举一个死锁的例子。
**A:**不同线程锁不同资源就可能出现死锁。
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
Q:如何避免死锁?
A:
避免一个线程同时获取多个锁。
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
第2章 Java并发机制的底层实现原理
锁的位置是在对象头部。
2.1 volatile的应用
防止指令重排序,保证读的时候是准确的。
线程可见性:在多线程的情况下,一个线程修改了volatile修饰的变量,其他线程可以立刻知晓到当前的数据。
每个线程在进行变量操作时都会拷贝一个副本数据进行操作。
内存屏障:一个约定(标记),见到该标记后就不再访问。
缓冲行:缓存中可以分配的最小存储单位(64B)。
原子操作:要么都成功,要么都失败。
緩存:把数据存到离调用更近的地方。
缓存命中:在高速缓存中找到需要的数据。
写命中:在高速缓存中对数据进行写操作(增删改)。
写缺失:一个进程对高速缓存中的数据进行写操作时,另一个进程将内存中的数据删除,导致高速缓存中的数据写回内存中时找不到内存中的数据。
事务必须具有原子性。
Q:volatile的作用是什么?volatile是如何保证线程可见性的?
**A:**当有多个线程调用内存中同一个数据时,该数据会加上地址;当其中一个线程对其进行写操作后,该数据会从总线写回到内存中,并且同时通知高速缓存中所有该地址的数据无效,其他线程想要调用时必须从内存中从新读取。
理论上,CPU有几个核就可以有几个线程同时运行。
线程操作内存中的数据时需要排队。
volatile的实现方法:
- 锁总线(浪费资源较大);
- 锁缓存(缓存锁定)。
2.2 synchronized的实现原理与应用
静态方法加锁叫类锁,会把所有静态方法锁住;非静态方法加锁叫对象锁,会把所有的对象锁住。对象锁只限制在自己的对象之内,对其他对象不影响。
类锁和对象锁互不影响。
//创建线程池
class Solution {
public static void main(String[] args) {
Test x = new Test(); // x叫做多个线程的共享变量,相当于并发资源
// 在线程内使用x时相当于被final修饰过了,不能看到任何修改操作。
Thread t1 = new Thread() {
@Override
public void run() {
x.m1();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
x.m2();
}
};
t1.start();
t2.start();
System.out.println("main");
}
}
//测试锁
public class Test {
public synchronized void m1() {
System.out.println("m1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m1 end");
}
public synchronized void m2() {
System.out.println("m2 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2 end");
}
public synchronized void m3() {
System.out.println("m3 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m3 end");
}
public static synchronized void m4() {
System.out.println("m4 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m4 end");
}
}
x.join(); //等待x线程执行结束后再执行本行之后的代码
数据往回更新后还会重新读一次。
写后读思想:A线程修改后,B线程基于A修改后的值进行叠加操作。写后读是并发编程能够进行正确操作的核心思想。只有写后读生效的场景才是并发安全的。
自旋锁加锁快,一旦资源解锁可以立刻感知到并且加锁。
当线程竞争激烈时,轻量级锁的整体性能大幅下降。自旋锁太多时,CPU利用率降低,整体进度变慢。
重量级锁几乎不会导致CPU浪费。
没有并发的使用偏向锁,无加锁流程,速度快;有轻微竞争时使用轻量级锁,自旋锁感知速度快,能立刻感知到解锁,能以最快的速度加锁;高并发时使用重量级锁,CPU浪费较低,整体执行速度快。
Q:偏向锁在哪配置?
A:
Q:自旋锁自旋多少次竞争不到会升级成重量级锁?
A:
方法执行时入栈并加锁,方法执行结束后锁释放。
2.3 原子操作的实现原理
Q:什么是原子操作?
**A:**不可被中断的一个或一系列操作。
CAS(比较并交换):CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS实现需要调用操作系统底层的C接口。
现实中的流水线:一个人干同一件事,由许多人完成整个工作。
CPU流水线:任务执行顺序会改变,但会提升速度。
Java中Atomic开头的类都是并发安全的类,内部自动加锁,我们不需要给他加锁。concurrent包中的类全部都是线程安全的类。
ABA问题:因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
第3章 Java内存模型
3.1 Java内存模型的基础
Q:线程之间如何进行通讯?
**A:**在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
线程之间如何同步?
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
Q:进程之间如何进行通讯?
**A:**进程间通信的8种方法:
1、无名管道通信
无名管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2、高级管道通信
高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
3、有名管道通信
有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4、消息队列通信
消息队列(message queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5、信号量通信
信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6、信号
信号(sinal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
7、共享内存通信
共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
8、套接字通信
套接字(socket):套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
线程有的通讯方式,进程不一定有;进程有的通讯方式,线程一定有。
进程之间内存不共享,他们独占自己的内存空间。
等号右侧是变量的是读操作(Lead),等号右侧是数字的是写操作(Store)。
a = b; d = f; //Load-Load
a = b; e = 9; //Load-Store
w = 8; q = 5; //Store-Store
r = 8; h = g; //Store-Load
有依赖的不允许指令重排序。
happens-before的传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
3.4 volatile的内存语义
volatile只保证读到正确的数据,却不保证线程安全。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写。
保证安全,有且只能有写后读操作。
3.8 双重检查锁定与延迟初始化
Servlet,Service等运行时只有一个对象。
懒加载:
饿汉式:对象使用前就加载;
懒汉式:对象使用后才加载。
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1:A线程执行
instance = new Instance(); // 2:B线程执行
return instance;
}
}
Q:为什么getInstance() 方法需要设为静态?
**A:**getInstance() 方法不是静态无法访问到静态变量instance。
Q:为什么instance变量是私有的?
**A:**如果不设为私有,其他线程也可对他进行写操作,创建新的对象。
加锁力度越大,其他线程被挡的概率越大。
双检锁/双重校验锁(DCL,即 double-checked locking)
**JDK 版本:**JDK1.5 起
**是否 Lazy 初始化:**是
**是否多线程安全:**是
**实现难度:**较复杂
**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
// 只锁这一块的原因:只有写操作才会有冲突,而读操作不会有冲突,不需要加锁
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Q:为什么singleton是private类型?
**A:**如果不设为私有,其他线程也可对他进行写操作,创建新的对象。
Q:为什么singleton是volatile类型?
**A:**防止指令重排序+可见性(保证其他线程读取到正确的数据)【可能导致对象没有创建成功(只分配了地址,没有数据就返回句柄,空指针异常)】。
保证写操作的原子性。(情况:1.申请空间后存入地址,还没有初始化对象;2.预定空间,但还未申请空间【地址不为空,数据为空】)。
Q:为什么singleton是static类型?
A:调用它的方法是静态的。如果属性是非静态的,那么静态方法调用不到它。
Q:为什么构造方法是私有的?
**A:**防止外部创建对象。
Q:getSingleton()方法为什么是静态的?
**A:**getSingleton()方法不是静态无法访问到静态变量singleton。
Q:为什么会有两个if判空?
**A:**第二个if是给第二个及以后的线程使用的,防止多次创建对象(有可能在第一个线程加锁之前其他线程第一个if已经判断为空,如果没有第二个if判空就可能会导致多次创建对象)。
ReentrantLock 可以用于解决死锁问题。
加锁和解锁过程
state表示加锁的次数,解锁时可以按加锁次数来解锁。加锁次数 == 解锁次数。
线程安全的类有哪些?
- 通过synchronized 关键字给方法加上内置锁来实现线程安全
Timer,TimerTask,Vector,Stack,HashTable,StringBuffer
- 原子类Atomicxxx---包装类的线程安全类
- BlockingQueue 和BlockingDeque
- CopyOnWriteArrayList和 CopyOnWriteArraySet
- Concurrentxxx,最常用的就是ConcurrentHashMap,当然还有ConcurrentSkipListSet和ConcurrentSkipListMap等等。
- ThreadPoolExecutor
- ollections中的synchronizedCollection(Collection c)方法可将一个集合变为线程安全,其内部通过synchronized关键字加锁同步
Final
被final修饰的对象不能修改。防止一定程度上的指令重排序。
完成等价写后读保证线程安全。
CAS 比较并交换
读取后先记录原始值,操作后返回,如果记录的值和内存中的值相等则操作有效,反之无效。
ABA问题
如果内存的值 A -> B -> A,CAS感知不到。
解决ABA问题:增加版本号。
指令重排序
相邻两行代码,如果没有依赖关系,那么执行顺序不固定。
happens-before原则
1)A happens-before B
2)B happens-before C
3)A happens-before C
这里的第3个happens-before关系,是根据happens-before的传递性推导出来的。
对象锁
同一个对象里面的多个非静态加锁方法,只能同时被一个线程调用。
线程竞争对象锁需要CAS。
在竞争对象锁时,如果一个线程竞争失败会进入阻塞队列,直到锁被释放才会重新回到就绪态。
第4章 Java并发编程基础
线程本质为栈。
多线程运用场景
Tomcat,网络爬虫,数据库线程池。
线程简介
多线程20~40个线程性能最快。
线程优先级没有用,程序正确性不能依赖线程的优先级高低。
sleep(0),sleep(1):快速让出CPU。
Thread.yield():让出CPU并不是立即让出,会有一定的延迟。如果未执行完,切换回来会继续执行。
线程优先级由操作系统决定。
线程状态
新建,就绪,运行,等待,阻塞,死亡。
必须事先持有锁才能进入等待状态。
sleep()和wait()区别
sleep() 不会释放锁,wait()会释放锁。
如何进行Java优化?
能够看到Java底层,了解底层运作方式。
有中断机制才会有响应机制。