前言
这篇博客重点在于讲解API功能细节,对于同步异步还没有清晰概念,或从未使用过线程同步相关API的同学,请先自行补全入门基础,这里不再累述
由于Java为线程API都设置了强制异常检查,所以编程时需要编写大量的try-catch代码,为了节省这些无意义代码,让博客更简洁清晰,我们封装了一个Threads工具类,来屏蔽这些异常检查代码。比如:
- Threads.post(runnable)相当于new Thread(runnable).start()
- Threads.sleep()相当于Thread.sleep()
- Threads.yield()相当于Thread.yield()
- Threads.wait(lock)相当于lock.wait()
- Threads.notify(lock)相当于lock.notify()
- Threads.join(thread)相当于thread.join()
- Threads.interrupt(thread)相当于thread.interrupt()
博客中提到的lock对象,均是指作为同步锁的对象,任意对象都可以作为同步锁
线程状态
出于线程管理和描述的需要,我们必须清楚地定义出线程的所有状态或生命周期
- 新建状态(NEW):thread刚被new出来,还没有执行start方法
- 就绪状态(READY):thread已经在运行,但是尚未获得CPU或同步锁资源
- 运行状态(RUNNABLE):thread获得了CPU和同步锁资源,正在执行代码
- 阻塞状态(BLOCKED):thread因为sychronized,sleep,wait,join等原因,暂时停止执行(等待条件成立时,就会复原到就绪状态)
- 中止状态(TERMINATED):thread的run方法执行完毕,任务完成,线程结束
Thread.sleep(long ms)
让当前线程休眠一段时间,达到指定时间后,再继续执行后续代码
synchronized(lock) {…}
同步块,表示在{…}块作用域内,当前线程获得lock对象的访问和修改权限,也可以形象地说成是,当前线程获得lock对象锁 在{…}块结束之前,其它线程无法访问或修改lock对象,需要一直等待
这样说其实并不严谨,这只是最简单的情况,通过Thread.wait等方法可以让同步块暂时让出对线锁,马上就会提到
synchronized function() {…}
同步方法,同一时间只有一个线程可以进入被synchronized修饰的方法,只有当前线程退出方法后,其它线程才能进入,相当于是一个方法锁
lock.wait()
只有在同步块中,lock被作为同步锁时,才能使用此方法 这个方法让当前线程临时让出lock的所有权,当前线程进入阻塞状态 直到其它线程调用了lock.notify()或lock.notifyAll()通知资源已经释放,才会恢复到就绪状态,重新竞争资源
代码测试
//同步锁
final Object lock = new Object();
//启动线程A
Threads.post(() -> {
synchronized (lock) { //1.由于线程B休眠,所以线程A先进入同步块,获得lock所有权
Threads.sleep(5000); //2.线程A休眠,保持对lock的所有权
System.out.println("A1");
Threads.waits(lock); //4.线程A让出lock所有权,等待其它线程notify,再重新竞争lock所有权
System.out.println("A2"); //由于两个线程都没有调用notify,所以A2和B2永远不会打印,一直阻塞在wait处
}
});
//启动线程B
Threads.post(() -> {
Threads.sleep(2000);
synchronized (lock) { //3.由于lock被线程A占有,进入阻塞状态,等待lock资源
System.out.println("B1"); //5.线程B获得lock所有权
Threads.waits(lock); //6.线程B让出lock所有权,等待其它线程notify,再重新竞争lock所有权
System.out.println("B2");
}
});
lock.wait(long ms)
同wait()功能一样,但是增加了超时处理 当超出timeout指定的时间时,即使其它线程没有调用lock.notify(),线程也会自动进入就绪状态,重新竞争lock资源
代码测试
//同步锁
final Object lock = new Object();
//启动线程A
Threads.post(() -> {
synchronized (lock) { //1.由于线程B休眠,所以线程A先进入同步块,获得lock所有权
Threads.sleep(5000); //2.线程A休眠,保持对lock的所有权
System.out.println("A1");
Threads.waits(lock, 5000); //4.线程A让出lock所有权,等待其它线程notify或超时,再重新竞争lock所有权
System.out.println("A2"); //5.wait超时,线程自动切换到就绪状态,获得lock所有权
}
});
//启动线程B
Threads.post(() -> {
Threads.sleep(2000);
synchronized (lock) { //3.由于lock被线程A占有,进入阻塞状态,等待lock资源
System.out.println("B1"); //5.线程B获得lock所有权
Threads.waits(lock); //6.线程B让出lock所有权,等待其它线程notify,再重新竞争lock所有权
System.out.println("B2"); //由于线程A没有调用notify,所以B2永远不会打印
}
});
sleep和wait方法的区别
- sleep是Thread类的方法,wait是Object类的方法
- sleep(休眠)是当前线程什么都不做,和其它线程互不影响,没有如何互动
- wait(等待)则是交出同步锁所有权,并且等待其它线程的通知,wait是围绕着同步锁进行的线程间互动
lock.notify()
通知另一个正处于wait状态的线程退出wait状态,进入就绪状态,开始竞争lock资源 如果有多个线程都处于wait状态,具体哪个线程被通知是不确定的,由系统调度决定 还有一点非常重要的就是,线程调用notify,并不意味着就立刻释放lock锁,仅仅是通知其它线程而已,其它线程也只是开始竞争资源,并不代表可以立刻得到lock资源,只有notify的线程退出同步块之后,lock资源才会被释放,其它线程才有可能抢到资源 一般建议将notify放在同步块的最后一行代码执行,因为就算提前执行也没用,反而容易引起误会或重复调用
lock.notifyAll()
和nofify方法功能一致,只不过nofify方法只通知一个线程,而notifyAll通知所有处于wait状态的线程
代码测试
//同步锁
final Object lock = new Object();
//由于多个线程是并发运行的,没法控制哪个先执行,这样就不方便测试
//但是我们可以通过休眠,来控制有效代码的执行顺序
//我们让三个线程分别休眠1s,2s,3s,这样代码执行顺序就是A-B-C
//启动线程A
Threads.post(() -> {
Threads.sleep(100);
synchronized (lock) {
System.out.println("A1"); //1.线程A获得同步锁,开始执行代码
Threads.wait(lock); //2.线程A交出同步锁,进入wait状态
System.out.println("A2");
}
});
//启动线程B
Threads.post(() -> {
Threads.sleep(200);
synchronized (lock) {
Threads.sleep(500); //3.线程B获得同步锁,开始执行代码
System.out.println("B1");
Threads.wait(lock); //5.线程B交出同步锁,进入wait状态
System.out.println("B2");
}
});
//启动线程C
Threads.post(() -> {
Threads.sleep(300);
synchronized (lock) { //4.由于同步锁被B占有,无法进入同步块
System.out.println("C1"); //6.A和B都交出了同步锁,线程C进入同步块,获得同步锁
Threads.notify(lock); //7.通知其中一个wait线程退出阻塞状态,进入就绪状态
Threads.sleep(500); //8.由于线程C仍持有同步锁,wait线程只能继续等待,虽然它已经是就绪状态
System.out.println("C2");
} //9.线程C释放同步锁,wait线程获得同步锁,继续执行代码,但由于只通知了一个线程,A2和B2只有一个会打印
});
Thread.yield()
它告诉系统,可以让当前线程先放弃lock资源,从运行状态转入就绪状态,从而让其它线程有获得lock资源的机会 它是一个建议性的API,并不能保证其它线程一定能得到lock资源 因为就绪状态的线程仍然会竞争资源,可能刚刚让出lock资源,马上又给自己抢到了,但是这样至少保证了其它线程有得到lock资源的可能性 一般在我们不想垄断资源,也不想永远在其它线程之后执行,想让不同线程随机自由竞争的时候,就可以使用这个API 由于它是建议性的,运行效果也是随机的,一般我们并没有必要调用这个方法,往往我们都是出于"完美主义"的想法,想"公平对待"不同线程,才会去使用它
值得注意的是,Thread.sleep()和Thread.yield()都是静态方法,属于Thread类的方法,而不是属于某个对象的方法,它们的操作对象都是当前线程
thread.setPriority(int priority)
提到了Thread.yield方法,就顺便提下thread.setPriority方法,它也是一个建议性的方法 它为线程设置不同的优先级,取值范围为1-10,数值越大优先级越高 高优先级线程有更高概率获得CPU资源和锁资源,但这不是一定生效的,只是给CPU的一个建议
thread.join()
让另一个线程先执行,执行完再回到当前线程继续执行,这个thread一般是其它线程 这个方法不涉及同步锁,仅仅是控制多个线程的执行顺序,但也会让线程进入阻塞状态
thread.join(long ms)
和thread.join()功能一致,但是增加了超时限制,达到指定时间,即使其它线程未执行完毕,也会继续执行
代码测试
//同步锁
final Object lock = new Object();
//创建线程B
Thread t2 = new Thread(() -> {
Threads.sleep(3000); //3.线程B优先执行,线程A等待
System.out.println("B1");
System.out.println("B2");
}); //4.线程B执行完毕
//创建线程A
Thread t1 = new Thread(() -> {
System.out.println("A1"); //1.由于线程B在休眠,所以A先执行
Threads.join(t2); //2.等待B执行完毕,再继续执行
System.out.println("A2"); //5.线程A继续执行
});
//启动线程
t1.start();
t2.start();
线程阻塞的几种情景
- 由于sychronized无法获得对线锁,进入阻塞状态,其它线程让出同步锁时,即可打破阻塞状态
- 由于sleep方法,主动进入阻塞状态,达到超时时间,即可退出阻塞状态
- 由于wait方法,主动进入阻塞状态,收到其它线程的notify,或达到超时时间,即可打破阻塞状态
- 由于join方法,主动进入阻塞状态,其它线程执行完毕,或达到超时时间,即可打破阻塞状态
thread.interrupt()
中断一个处于阻塞状态的线程,并抛出一个InterruptedException 注意两点,一是只能中断处于阻塞状态的线程,不能中断处于正常运行状态的线程,二是中断后只是抛出一个异常,并不是直接让线程停止 如果我们正确处理了这个异常,线程还是会继续往下执行,当然,我们也可以在捕获到异常时,通过代码让线程return或跳到最后一行,从而达到停止线程的目的
注意,interrupt()方法只对sleep,wait,join方法引起的阻塞状态有效,对sychronized同步锁造成的阻塞无效
代码测试
//同步锁
final Object lock = new Object();
//创建线程C
Thread t3 = new Thread(() -> {
synchronized (lock) {
Threads.sleep(100000);
}
});
//创建线程A
Thread t1 = new Thread(() -> {
System.out.println("A1"); //1.线程A和C先运行,因为线程B在休眠
try {
Threads.join(t3); //2.等待线程C运行完毕,由于C睡眠时间很长,A会长时间阻塞
} catch (Exception e) { //6.线程A被打断,抛出InterruptedException异常
System.out.println("InterruptedException");
return; //7.捕获异常,return结束线程,也可以不结束,取决于代码
}
System.out.println("A2"); //8.由于线程结束,A2不会被打印
});
//创建线程B
Thread t2 = new Thread(() -> {
Threads.sleep(1000);
System.out.println("B1"); //3.线程B开始运行
t1.interrupt(); //4.打断线程A的阻塞状态
System.out.println("B2"); //5.线程B继续执行
});
//启动线程
t1.start();
t2.start();
t3.start();
thread.stop()
强制无条件立刻终止一个线程 这个方法比interrupt更加暴力,它可以在任何情景下立刻终止一个线程,不管线程run方法是否执行完,不管线程是否处于阻塞状态,或线程是否拥有同步锁,它都会立刻生效 这是个已经被废弃的方法,因为它的结束方式就决定了,这个接口与生俱来的危险性 比如这个线程使用了同时向一个List和一个Map对象写数据,并给读写操作加锁了,结果这个线程给List写入了数据,刚准备给Map写数据的时候,线程就被强制结束了,当其它线程再使用List和Map的时候,使用的已经是不同步的数据了 但是如果确定线程操作不涉及线程同步或内存泄露等问题,使用stop方法还是不错的选择的,毕竟它是唯一一个让线程立即结束的方法,非常简单
代码测试
//创建线程A
Thread t1 = new Thread(() -> {
while (true) {
long millis = System.currentTimeMillis();
System.out.println(millis);
}
});
//创建线程B
Thread t2 = new Thread(() -> {
Thread.sleeps(2000);
t1.stop();
});
//启动线程
t1.start();
t2.start();
Java源码中对Thread.State的定义
我们在文章的刚开始,就已经讲解过线程状态的定义,但是我们是从线程工作原理的角度来讲的,它适用于所有语言
Java在Thread也定义了一个名为State的内部类,可以通过thread.getState()来获取线程的State,Thread源码中定义的线程状态则和我们上面定义的有所差异
我们先来看下Java源码
package java.lang;
public class Thread implements Runnable {
public enum State {
NEW, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
}
private volatile int threadStatus = 0;
public State getState() {
return sun.misc.VM.toThreadState(threadStatus);
}
}
通过源码我们可以看到以下区别
- Java源码中没有定义就绪状态,因为就绪状态只存在一瞬间,如果竞争资源成功,马上就会进入运行状态,如果失败,则会立刻转入阻塞状态,从代码实现的角度来说,一瞬间的状态是没实际意义的,因为它的值马上就会发生变化
- Java源码中将阻塞状态分为了三种:BLOCKED, WAITING, TIMED_WAITING
- sychronized引起的同步锁阻塞,用BLOCKED表示
- wait(),join()引起的无限等待阻塞,用WAITING表示
- wait(ms),join(ms),sleep(ms)引起的限时等待阻塞,用TIMED_WAITING表示
- 可以看到,和我们在文章的划分其实本质上是一样的,只是表述上的区别。因为源码是为了实现Thread接口功能所设计的,它必须区分每种具体的状态,才能实现wait,notify这些功能
Java内存模型和volatile关键字
Java内存模型我们在这里不详解,只是为了介绍volatile而简单提下 在Java的内存模型中,为了保证速度,每个CPU都有一个高速缓存,线程使用变量时,首先访问的并不是内存中的值,而是CPU缓存中的值。这样就会出现一个问题,当多个线程使用不同CPU的时候,是从不同的CPU缓存中取值,这样就有可能会出现变量值不一致的情况 volatile关键字能够保证被其修饰的变量,在数值被改变时,能及时地反映到内存和各个CPU缓存中,这样就能每个线程获取到的值是最新的
volatile只能保证取值赋值语句在多线程情况下可以正确执行,并不能保证其它情景下变量值也是同步的,甚至是非常简单的语句都不行
volatile的作用非常有限,我们看下这个例子就知道了
private volatile int sum = 0;
//这就是我们要举例的语句,非常之简单
//但是volatile关键字不能保证这条语句能够正确地执行
sum++;
//volatile只能保证下面这种形式的语句能够按照预期的结果执行
sum = 100;
int result = sum;
//sum++其实相当于以下语句
sum = sum + 1;
//再进一步,它其实相当于这样的语句
int temp = sum;
sum = temp + 1;
//这就是问题的关键所在
//volatile可以保证int temp = sum的正确性
//volatile也可以保证sum = temp + 1的正确性
//但是这是对CPU来说,是两次运算,在两次运算之间,其它线程可能已经修改了sum的值
//比如我们有10000个线程都在这些sum++这个简单的代码
//我们的线程第一个拿到了sum的值,它的初始值为0,于是
//int temp = 0
//sum = temp + 1 = 1;
//但是在这两句之间,其它的线程可能已经让sum自增了100次,sum的最新值已经变成了100
//而我们却还在用旧的sum值在做自增运算,得到1,然后赋值给sum,覆盖了其它线程的运算结果
//这显然不是我们所预期的结果
//问题的关键就在于,sum++是复合操作,它其实相当于多个运算语句
//而多个运算语句之间,其它线程是有可能插入进来先执行的,让我们的操作变得无意义
//所以正如前面所说的,volatile只能保证基本取值赋值语句的正确性
//从这个例子我们可以看出,volatile的功能其实极其有限
//不知道大家有没有悟出来一个结论:
//volatile其实并不是用来解决线程间的语句同步问题的,而是用来解决CPU之间的变量值同步问题
有了sychronized还需要volatile吗
看来上节的说明,不知道大家有没有自己悟出来这样一点: volatile其实并不是用来解决线程间的语句同步问题的,而是用来解决CPU之间的变量值同步问题
sychronized关键字用于解决线程间的语句同步问题,它将同步块作为一个整体,其它线程只有在整个同步块都退出时,才有可能修改或访问同步变量 除此之外,其实sychronized关键字也具有保证CPU之间变量值同步的功能,sychronized在进入同步块时,会清空所有的CPU缓存,从主内存中重新获得变量值,sychronized在退出同步块时,会将最新的变量值同步到主内存当中
虽然sychronized的功能要强大于volatile,但是由于sychronized是阻塞式的,它会影响到代码的执行速度,一个线程在执行,其它线程就要等待。而且sychronized本身在实现上,就比volatile更加复杂,运行效率更低
所以在一些简单的情况下,比如一个线程只写值,另一个线程只读值,也不用关心多线程下语句的执行顺序时,使用volatile就足够了
代码测试
我们通过代码来测试下,没有volatile关键字,会不会出现变量值不同步的问题
@SuppressWarnings("all")
public class Hello {
public static Integer value = 0;
public static void main(String[] args) {
//线程A先执行,value=0
Threads.post(() -> {
while (true)
if (value != 0)
System.out.println("Value Change");
//永远不会打印,说明线程B的修改没有反映到线程A中
//如果我们在value前加上volatile修饰,则马上打印,说明volatile确实具有同步数值的作用
//另外,即使我们不使用volatile关键字
//如果我们在while (true)里面添加sychronized或sleep语句,发现也会打印语句
//这说明,进入sychronized同步块或执行sleep语句后,会自动同步数值
//注意:这个测试代码其实是有讲究的,我们不能直接通过打印value的值去测试
//因为System.out.println方法本身内部就包含了sychronized代码在里面
//如果直接打印,必然会造成变量同步,这样是测不出真实结果的
});
//线程B后执行,修改value的值
Threads.post(() -> {
Threads.sleep(200);
while (true)
value = 999;
});
}
}
工具类代码
补上工具类代码,方便大家做伸手党
@SuppressWarnings("all")
public class Threads {
public static void post(Runnable runnable) {
new Thread(runnable).start();
}
public static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void wait(Object lock) {
try {
lock.wait();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void wait(Object lock, long ms) {
try {
lock.wait(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void notify(Object lock) {
try {
lock.notify();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void notifyAll(Object lock) {
try {
lock.notifyAll();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void yield() {
try {
Thread.yield();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void join(Thread thread) {
try {
thread.join();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void join(Thread thread, long ms) {
try {
thread.join(ms);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void interrupt(Thread thread) {
try {
thread.interrupt();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}