原子性 可见性和有序性
- 原子性(Atomicity):由JMM直接保证原子性变量操作在上节的
read\load
,store\write
,use\assign
操作中介绍过了,对于long和double两个64位的非原子性协定知道即可。我们可以认为对基本数据类型的访问读写都是具有原子性的。而要保证一个更大的范围的原子性,JMM提供了lock\unlock
来满足需求,语言层面就是使用synchronized关键字。 - 可见性(Visibility):可见性是指一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。volatile关键字中我们已经讲了这一点。volatile和synchronized都保证了可见性,另一种保证可见性的关键字是final,这是显然的,一个被final修饰的变量一旦被赋值就不能再更改了,这与线程也没有什么关系。
- 有序性(Ordering):程序代码按照指令顺序执行。
- 如果在本线程内观察,所有的操作都是有序的,指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics);如果在一个线程中观察另一个线程,所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
- 提供两个关键字保证有序性:volatile 本身就包含了禁止指令重排序的语义;synchronized保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入。
关于volatile禁止指令重排序保证有序性的介绍,可以参考这篇文章——volatile关键字,举了个单例模式的例子,解释了volatile禁止指令重排序的效果。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
if(uniqueInstance == null){
// B线程检测到uniqueInstance不为空
synchronized(Singleton.class){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
// A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
}
}
}
return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
}
}
如果加上volatile之后,就没有这样的问题了。volatile保证被其修饰的变量不会被编译器重排序,但是其他代码还是可能会被重排序的。
线程控制的几个方法
我们之前只是用start来启动线程。下面再介绍几个API,用来控制线程达到其他的状态
暂停线程
首先是静态sleep()
方法:
Thread.sleep(long millis) // 静态方法,让调用这个方法的线程让出 CPU,休眠参数指定的毫秒数
调用这个方法的线程会进入阻塞状态。
其次是实例join()
方法:
Thread.join() // 实例方法,分为有参数版本和无参数版本,
// 调用这个方法的线程会让出 CPU 资源进行等待参数指定的时间(毫秒),如果没有指定参数,
// 那么会直到这个方法所属的线程对象执行完成后,陷入等待的线程会恢复就绪态,等待 CPU 资源
调用这个方法的线程会进入阻塞状态,CPU会让给这个方法所属的线程对象。
终止线程
run()方法执行完毕是正常的终止线程,但也可以人为调用方法来终止线程的执行。
Thread类有一个stop()方法,这个方法已经被废弃了,是不安全的,具体废弃原因去查文档。可以利用一个boolean变量,这样安全的终止一个线程:
public void run() {
boolean isFinish = false; // 记录线程任务是否完成
while (!isFinish) {
if(/*任务完成*/) {
break; // 或者 isFinish = true;
} else {
// do something ...
}
}
}
Thread类中还提供了个interrup()
以及相关的一些方法,这是实例方法,可以这样做:
public void run() {
while (Thread.currentThread().isInterrupted() == false) {
if (/*任务完成*/) {
Thread.currentThread().interrupt();
} else {
// do something ...
}
}
}
详细了解上面的几个方法之前,先知道一个中断标志的概念,中断标志可以理解成线程内部的一个 boolean 类型的字段,其本身不会影响线程的执行,但是和其他方法(下面介绍的会有sleep方法以及wait方法)混用时,就有可能影响线程的执行。
Thread.currentThread() // 静态方法,返回执行当前代码的线程对象引用
Thread.isInterrupted() // 实例方法,返回调用这个方法的线程对象的中断标志(true / false)
Thread.interrupt() // 实例方法,将调用这个方法的线程对象的中断标志设置为 true,
// 请注意:线程的中断标志本身不会影响线程的执行
直接用interrup自然比自己去定义boolean更方便,但是我们介绍这个中断标志就是有伏笔的,interrup会把中断标志设置为true,而这和sleep方法一起使用时候,会有异常抛出,我们打开sleep的源码:
从注释中看到,sleep方法调用时候,如果当前线程被中断(它的中断标志是true),那么在抛出异常时候这个中断标志会被清除(将中断标志设置为 false),由此就导致了isInterrupted方法的返回值,可能并不是我们想要的结果。
其他方法
静态yield()
方法,提示scheduler,让出调用线程的资源,一定概率。
Thread.yield() // 静态方法,提示线程调度器当前调用这个方法的线程(当前持有 CPU 资源的线程)已经完成任务,
// 可以让出 CPU 资源了,当然,这只是一种提示,线程调度器可以忽略这种提示,
// 所以 CPU 资源是否让出并不是一定的,是有一定概率的。
上面介绍的所有方法都是Thread类
中的方法,有的是静态的,有的是实例方法,静态的这些方法一般都直接对调用它的线程起到作用,而实例方法,则还对这个方法所属的对象线程有影响。下面我们再来看Object类
中一些与线程控制有关的方法。
- Object.wait()
- Object.notify()
- Object.notifyAll()
这些方法全部是实例方法。都是必须要在sychronized方法或者sychronized代码块中才能使用,而且还必须是某个线程已经获取到了这个Object的锁时候,才能调用它的wait,notify,notifyAll。
Object.wait() // 使得调用这个方法的线程释放这个 Object 对象的锁并且陷入无限等待,
// 直到某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常
Object.wait(long timeout)
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,单位为毫秒
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常
Object.wait(long timeout, int nanos)
// 使得调用这个方法的线程释放这个 Object 对象的锁并且等待参数指定的时间,
// 第二个参数是纳秒,提供更加精确的控制
// 直到这个等待的时间段过去、某个线程调用了这个 Object 对象的 notify 或者 notifyAll 方法
// 线程被唤醒之后进入就绪状态,等待 CPU 资源
// 如果当前线程的中断标志为 true,那么会抛出一个 InterruptedException 异常
Object.notify() // 唤醒一个因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程,具体哪个线程未知。
Object.notifyAll() // 唤醒所有因调用这个 Object 对象的 wait() 方法而陷入等待状态的线程。
使用实例还是可以参考这个老哥的文章——wait使用的一个实例——转账余额不足
注意wait和sleep的区别,他们好像都多少有些等待、休眠的意思。但是前者是Object类的实例方法,调用后会释放当前对象锁,并且需要其他线程调用这个对象的notify()或者notifyAll()来唤醒。而后者是Thread类的静态方法,调用后只是让出CPU,并不会释放锁,监控状态一直保持,过了指定时间后它会自动恢复运行状态。
网友评论