进程和线程
进程
- 定义:正在运行程序的一个实例,拥有自己私有专用的内存空间
- 可抽象为虚拟计算机,拥有独立的执行环境和完整的资源
- 进程间通常不共享内存,不能访问其他进程的内存和对象,需要特殊机制
- 进程通信采用消息传递方式,即标准I/O流,为了实现进程间通信,大多数操作系统都支持“进程间通信(IPC)资源”,如管道和socket
线程
- 定义:正在运行程序的一个执行路径(一个进程可对应多个线程)
- 线程有自己的堆栈和局部变量,但是多个线程共享内存空间
- 可抽象为一个虚拟处理器,有时也称为轻量级进程
- 线程存在于进程内,与进程中的其他线程共享相同的资源
- 采用内存共享机制通信,需要特殊处理才能实现消息传递和私有内存
- 线程需要同步:在改变对象时要保持lock状态
- 清理线程是不安全的
1. 进程是负责整个程序的运行,而线程是程序中具体的某个独立功能的运行。
2. 一个进程中至少应该有一个线程。
3. 主线程可以创建其他的线程。
线程的创建和启动,runable
- 继承Thread类型
- 方法:用Thread类实现了Runnable接口,但它其中的run方法什么都没做,所以用一个类做Thread的子类,提供它自己实现的run方法。用Thread.start()来开始一个新的线程。
- 创建:MethodThread m = new MethodThread()
- 启动: m.start()
- 步骤:
- 定义一个类MethodThread 继承于java.lang.Thread类
- 在类MethodThread 中覆盖Thread类中的run()方法
- 在run()方法中编写需要执行的操作:run()方法里的代码,线程执行体
- 在main方法(线程)中,创建线程对象,并启动线程
- 例子:
public class MethodThread extends Thread{ public void run() { System.out.println("Hello from a thread!"); } public static void main(String[] args) { MethodThread m = new MethodThread(); m.start(); } }
- 实现Runable接口
- 创建:Thread t = new Thread();
- 调用:t.strat()
- 步骤:
- 定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类】
- 在A类中覆盖Runnable接口中的run方法
- 在run方法中编写需要执行的操作:run方法里的,线程执行体
- 在main方法(线程)中,创建线程对象,并启动线程
public class MethodRunable implements Runnable{ public void run() { System.out.println("Hello from a thread!"); } public static void main(String[] args) { MethodThread m = new MethodThread(); m.start(); } }
- 惯用法:用一个匿名的Runnable启动一个线程,避免创建命名类
new Thread(new Runable() { public void run(){ System.out.println("Hello from a thread!"); } }).start();
- 实现Runnable接口相比继承Thread类有如下好处:
- 避免点继承的局限,一个类可以继承多个接口;
- 适合于资源的共享
- 创建并运行一个线程所犯的常见错误是调用线程的 run()方法而非 start()方法,如下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run(); //should be start();
起初并不会感觉到有什么不妥,因为 run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行 run()方法,必须调用新线程的 start 方法。
时间分片、交错执行、竞争条件
时间分片
- 在某时刻,一个运行核心上只有一个线程可以运行
- 通过时间分片,再多个线程/进程之间共享处理器
- 当线程数多于处理器数量时,并发性通过时间片来模拟,处理器切换处理不同的线程
- 通过时间分片,在多个进程/线程之间共享处理器。(时间分片是由OS自动调度的)
交错执行
时间片的使用不可预知且非确定,线程可能随时暂停或恢复
竞争条件
竞争是发生在线程交错的基础上的。当多个线程对同一对象进行读写访问时,就可能会导致竞争的问题。程序中可能出现的一种问题就是,读写数据发生了不同步。
程序的正确性(后置条件和不变量的满足)取决于并发计算A和B中事件的相对时间,例如A和B都需要从银行中取走全部存款,A取走时B可能不知道,导致了没有足够的存款,产生竞争。
程序运行时有一种情况,就是程序如果要正确运行,必须保证A线程在B线程之前完成(正确性意味着程序运行满足其规约)。当发生这种情况时,就可以说A与B发生竞争关系。
线程的休眠、中断
休眠(Thread.sleep)-让当前线程暂停指定时间的执行
- 在线程中允许一个线程进行暂时的休眠,直接使用Thread.sleep()方法即可。
- 将某个线程休眠,意味着其他线程得到更多的执行机会
- 进入休眠的线程不会失去对现有monitor或锁的所有权
public static void sleep(long milis,int nanos) throws InterruptedException
示例:
for (int i = 0; i < n; i++){
Thread.sleep(1000);
System.out.println("Hello!");
}
Thread.join-确保当前线程能够执行完毕
示例:
public class JoinExample2 {
public static void main(String[] args) {
Thread th1 = new Thread(new MyClass2(), "th1");
Thread th2 = new Thread(new MyClass2(), "th2");
Thread th3 = new Thread(new MyClass2(), "th3");
th1.start();
try {
th1.join();
} catch (InterruptedException ie) {}
th2.start();
try {
th2.join();
} catch (InterruptedException ie) {}
th3.start();
try {
th3.join();
} catch (InterruptedException ie) {}
}
}
Thread.wait-释放锁,让线程进入等待,直到调用notify()
Thread.interrupt
- 一个线程可以被另一个线程中断其操作的状态,使用 interrupt()方法完成
- 通过线程的实例来调用interrupt()函数,向线程发出中断信号
- t.interrupt():在其他线程里向t发出中断信号
- t.isInterrupted():检查t是否已在中断状态中
- 每个线程都有中断状态,初始为false
- 若该线程在执行低级可中断阻塞方法,取消阻塞,抛出中断异常,否则设置中断状态为true
- 当某个线程被中断后,一般来说应停止 其run()中的执行,取决于程序员在run()中处理
- 一般来说,线程在收到中断信号时应该中断,直接终止
- 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情
- 当一个线程中断另一个线程时,被中断的线程不一定要立即停止,只需在愿意并且方便的时候停止,为安全地构造可取消活动提供了更大的灵活性
- 如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态,中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止
示例:
线程安全的四个策略
定义:无论如何执行,不许调度者做额外的协作,都能满足正确性
Confinement:限制可变变量的共享
- 核心思想:线程之间不共享mutable数据类型
- 将数据限制在单个线程中,避免线程在可变数据上进行竞争
- 不允许任何线程直接读写该数据
- 在多线程环境中,取消全局变量,尽量避免使用不安全的静态变量。
- 局部变量保存在线程栈中,每个调用都有自己的变量副本,如果是对象的引用,则要确保不能引用任何其他线程可访问的对象。
- 限制数据共享主要是在线程内部使用局部变量,因为局部变量在每个函数的栈内,每个函数都有自己的栈结构,互不影响,这样局部变量之间也互不影响。
- 如果局部变量是一个指向对象的引用,那么就需要检查该对象是否被限制住,如果没有被限制住(即可以被其他线程所访问),那么就没有限制住数据,因此也就不能用这种方法来保证线程安全。
Immutability:用不可变的共享变量
不可变数据类型,指那些在整个程序运行过程中,指向内存的引用是一直不变的,通常使用final来修饰。不可变数据类型通常来讲是线程安全的,但也可能发生意外。
- 解决了因为共享可变数据造成的竞争,可以安全地从多个线程访问final。
- 这种安全性只适用于变量本身,仍需确保变量指向的对象是不可变的。
- 如果类型的对象在整个生命周期中表示相同的抽象值,则类型不可变。
- 若改变对用户不可见,且对应抽象值不变,允许对rep进行更改。
- 不变性的强定义
- 没有改变数据的操作
- 所有字段均为private和final
- 没有表示暴露
- 表示中的任何可变对象都不能变化
- 不存储传递给构造函数的外部可变对象的引用
- 避免在方法返回值中包含对可变对象的引用
Threadsafe data type:将共享数据封装在线程安全的数据类型中
如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。一般来说,JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响。
- 如List、Set、Map等数据结构是不安全的,线程安全提供了Connections的类型,可确保方法是原子的(动作的内部操作不会同其他操作交叉,不会产生部分完成的情况),例:private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
- 统一采用包装类的形式,确保抛弃对底层非线程安全容器类的引用
- 即使在线程安全的集合类上,使用iterator也 是不安全的,在迭代collection的时候需获取集合的锁
- 原子操作仍然不足以完全避免竞争,如检查集合是否为空的时候,另一个线程可能提前取走了元素
- 包装的实现是将所有的实际工作委托给指定的容器,但在容器的基础上添加额外的功能
前三种策略的核心思想:避免共享 --> 即使共享,也只能读/不可写(immutable) -->即使可写(mutable),共享的可写数据应自己具备在多线程之间协调的能力,即“使用线程安全的mutable ADT”
Synchronization:使用同步来防止线程同时访问变量
- 锁是一种实现同步的抽象,某一时刻最多允许一个线程拥有锁
- 锁的两种操作
- 获取锁的所有权:如过锁被其他线程拥有,将进入阻塞状态,等待锁释放后再同其他线程竞争
- 释放锁的所有权
- 使用锁可以确保锁的所有者始终查看最新的数据
- 锁只能确保与其他请求获取相同对象锁的线程互斥访问,若其他线程采用不同的锁,则同步失效
- 当线程调用同步方法时,它会自动获取该方法对象的内部锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,也会释放锁
- 同一对象上的同步方法的两次调用不会有交错现象
死锁
- 由于使用锁需要线程等待,可能会陷入两个线程正在等待对方,陷入永远阻塞的情况
- 死锁可能涉及两个以上的模块,线程间的依赖关系环是出现死锁的信号
- 防止死锁方法
- 对需要同时获取的锁进行排序,并确保所有代码按照该顺序获取锁定,例:
- 用单个粗粒度的锁监控多个对象实例(但性能损失大)
- 对需要同时获取的锁进行排序,并确保所有代码按照该顺序获取锁定,例:
以注释的形式撰写线程安全策略
- 需要对安全性进行这种仔细的论证,阐述使用了哪种技术,使用threadsafe data types, or synchronization时,需要论证所有对数据的访问都是具有原子性的
示例:
反例: