目录
进程和线程
并发模块的类型:进程和线程
进程:私有空间嘛,彼此隔离。
- 拥有整台计算机的资源
- 多进程之间不共享内存,进程之间通过消息传递进行协作
- 一般来说,进程==程序=应用(但一个应用中可能包含多个进程)
- 进程=虚拟机
线程:程序内部的控制机制(程序的一部分)
- 线程=虚拟CPU
- 程序共享、资源共享,都隶属于进程
- 共享内存,通过创建消息队列在线程之间进行消息传递。
每个线程都有自己的栈。栈中包含所有方法的局部变量。线程只能访问自己的线程栈。线程创建的局部变量其他线程不可见。即使两个线程的代码一样,创建的同名变量仍然在各自得栈中。每个线程有自己版本的局部变量。
线程 | 进程 |
---|---|
轻量级 | 重量级 |
共享内存 | 私有 |
同步机制(存在一定的冲突) | 无 |
结束(kill threads)不安全 | 结束(kill processes)安全 |
一个进程可以有多个线程。
线程的创建和启动、runnable
每个应用至少有一个线程
主线程——可以创建其他的线程。
创建现成的两种方法:
- 从Thread类派生子类
public class HelloThread extends Thread{
@Override
public void run(){
System.out.println("Hello from a thread!");
}
//启动该线程的两个方式
public static void main(String[] args){
HelloThread p=new HelloThread();
p.start();//若重复使用采用这种方式
(new HelloThread()).start();//第二种方法
}
}
- 从Runnable接口构造Thread对象
public class HelloRunnable implements Runnable{
public void run(){
System.out.println("Hello from a thread!");
}
public static void main(String args[]){
(new Thread(new HelloRunnable())).start();//跟Thread解耦
}
}
内存共享模式、消息传递模式
The Internal Java Memory Model
Stack:每个线程有自己的栈,栈中包含所有方法的局部变量。基本类型的局部变量保存在线程栈中
基本类型的局部变量保存在线程栈中 - 线程只能访问自己的线程栈
- 线程创建的局部变量其他线程不可见
- 即使两个线程的代码一样,创建的同名变量仍然在各自的栈中。一个线程可能会将基本类型变量的副本传递给另一个线程,但它本身不能共享基本类型局部变量。
- 每个线程有自己版本的局部变量
Heap:对象类型数据保存在堆中,如果对象被指派到某个局部变量,或者作为其他对象的成员变量,创建的对象仍然在堆中。
Key:
- 基本数据类型的局部变量保存在线程栈中
- 局部变量引用了对象,引用保存在栈中,对象本身存储在堆中
- 对象包含的方法和方法包含的局部变量存储在栈中
- 对象的成员变量同对象一起存储在堆中,不论成员变量的类型是基本类型还是对象类型(对其他对象的引用)
- 静态的类变量同类的定义一起保存在堆中。
- 堆中的对象可以被所有拥有引用的线程访问。
- 两个线程同时调用同一个对象上的一个方法,它们都可以访问该对象的成员变量,但是每个线程都有自己的局部变量副本。
消息传递机制无法解决竞争条件问题
时间分片、交错执行、竞争条件
时间分片:虽然有多线程,但只有一个核,每个时刻只能执行一个线程。通过时间分片,在多个进程/线程之间共享处理器。即使是多个核CPU,进程/线程的数目也往往大于核的数目。
时间分片是由OS自动调度的
竞争条件:不能通过查看一个表达式来判断它是否存在竞争条件下是安全的。作用于同一个mutable数据上的多个线程,彼此之间存在对该数据的访问竞争。
ptint可以解决!
线程的休眠、中断
线程的休眠:Thread.sleep(time);将某个线程休眠,意味着其他线程得到更多的机会。
进入休眠的线程不会失去对现有monitor或锁的所有权,从休眠中苏醒后可以继续执行。
线程中断:向线程发出中断信号(Thread.interrupt())。
t.interrupt():在其他线程里向t发出中断信号(是请求不是操作)
中断是一种协作机制。当一个线程中断另一个线程是,被中断的线程不一定立即停止正在做的事情。中断是礼貌地请求另一个线程在它愿意并且方便地时候停止正在做的事情。
有些方法,如Thread.sleep()、Thread.join()、Thread.wait()等,很认真地对待这样的请求(立即响应),但其他方法不一定要对中断做出响应,可以不予理会。
当另一个线程通过调用t.interrupt()中断一个线程时,会主线以下两种情况之一:
- 如果被中断线程在执行一个低级可中断阻塞方法,例如Thread.sleep()、 Thread.join() 或Object.wait(),那么它将响应终端,抛出InterruptedException异常,程序捕获该异常后,可以做中断后的处理
- 否则,interrupt() 只是设置线程的中断状态,通知该线程有其他线程想终止它,让它自己决定是否终止。
每个线程都有一个与之相关联的Boolean属性,用于表示线程地中断状态,中断状态初始时为false
Ps.interrupt()和isInterrupted()是实例方法,interrupted()是类方法。
线程安全/threadsafe的四种策略
线程之间的“竞争条件”:作用于同一个mutable数据上的多个线程,彼此之间存在对该数据的访问竞争并导致interleaving,导致post-condition可能被违反,这是不安全的。
线程安全:ADT或方法在多线程中要执行正确。
线程安全的四个层次:
- Confinement限制数据共享
- Immutability共享不可变
- ThreadSafe data type共享线程安全地可变数据
- Synchronization同步机制:通过锁(Lock)的机制共享线程不安全的可变数据,变并行为串行。(同步机制为实现自己定义的线程安全的数据类型提供了支持)
Confinement
将可变数据限制在单一线程内部,避免竞争
不允许任何线程直接读写该数据。
核心思想:线程之间不共享mutable数据类型。避免全局变量。
如果一个ADT的rep中包含mutable的属性且多线程之间对其进行mutator操作,那么就很难使用confinement策略来确保该ADT是线程安全的
Immutability
使用不可变数据类型和不可变引用,避免多线程之间的race condition。(final)
不可变数据通常是安全的。
beneficent mutation(有益的可变)对于并发编程有时是 不安全的。
如果ADT中使用了beneficent mutation,必须通过“加锁”机制来保证线程安全
ThreadSafe类型
如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。(在JDK中的类,文档中明确指明了是否threadsafe)。
一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响。
例如:
Synchronization(同步)/Lock
容器类(List、Map、Set)是线程不安全的。Java提供了进一步的decorator。
private static Map<Integer,Boolean> cache=
Collections.synchronizedMap(new HashMap<>());
在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,不要保留别名,一定要彻底销毁
Threadsafe wrappers:
即使在线程安全的集合类上,使用iterator也是不安全的,除非使用lock机制。
即使是线程安全的collection类,仍可能产生竞争.(执行其上某个操作时threadsafe的,但如果多个操作放在一起,仍旧不安全)。
List <Type> c=Collections.synchroizedList(new ArrayList<Type>());
synchronized(c){
for(Type e:c)
foo(e);
}
线程安全不应依赖于偶然。
通过“同步”策略,避免多线程访问数据。
使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问。
Lock:Lock用来保护共享数据。拥有lock的线程可独占式的执行该部分代码。
synchronized(lock){//thread blocks here until lock is free
//now this thread has the lock
balance=balance+1;
//exiting the lock releases the lock/退出块释放锁
}
Note。要互斥,必须使用同一个lock进行保护。
锁只能保证与其他请求获取相同对象锁的线程互斥访问,如果其他线程没有使用synchronized(obj)或者利用了不同的锁,则同步会失效,需要仔细检查和设计同步块和同步方法。
两个基本同步idioms:
- synchronized statements /block:同步语句/同步代码块
- synchronized methods:同步方法
Monitor pattern:ADT的一个实例(对象)种所有方法都是互斥的。
当线程调用同步方法时,它会自动获取该方法所在对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会释放锁。
同一对象上的同步方法的两次调用不会有交叉现象。
当一个线程在执行一个对象的同步方法时,所有其他线程如果调用同一对象的同步块,则会挂起执行,直到第一个线程针对此对象的操作完成。
当一个同步方法退出时,它会自动建立一个与之后调用同一个对象的同步方法的happens-before关系,这保证对象状态的更改对所有线程都是可见的。
Static Synchronized Methods: 由于静态方法与类关联,而不是对象,此时线程获取与该类关联的Class对象的内部锁;对类的静态字段的访问由
与该类的任何实例的锁截然不同的锁来控制。
避免在方法spec中加synchronized,而是在方法代码内部更加精细的区分哪些代码行可能有threadsafe风险,为其加锁
任何共享的mutable变量/对象必须被lock所保护。涉及到多个mutable变量的时候,它们必须被同一个lock所保护。
在monitor pattern中,ADT所有方法都被同一个synchronized(this)所保护
死锁
同步带来的副作用
死锁:多个线程竞争lock,相互等待对方释放lock。
solution:
- lock ordering:需要直到子系统/系统中所有的锁,无法模块化,紧耦合。优势需要经过计算后才能直到需要用到哪些锁。
- coarse-grained locking:粗粒度的锁,用单个锁来同步多个对象实例/子系统。性能损失大。如果用一个锁保护大量的可变数据,那么就放弃了同时访问这些数据的能力。最糟糕的情况下,程序可能基本上是顺序执行的,丧失了并发性。
以注释的形式撰写线程安全策略(ThreadSafe Argument)
在代码中以注释的形式增加说明:该ADT采取了什么设计决策来保证线程安全。
- 阐述清楚采取了四种方法中的哪一种?
- 如果是后两种,还需考虑对数据的访问都是原子的,不存在interleaving(交叉情况)
我们要避免rep exposure。