Java并发小结02
主要参考自《实战Java高并发程序设计》。
线程与进程
进程是计算机系统进行资源分配和调度的基本单位,是线程的容器。
线程是处理器任务调度和执行的基本单位。
这里可以复习一下进程和线程的区别:
-
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
-
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
-
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
-
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
-
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
-
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
线程的生命周期
-新建 NEW
-就绪 RUNNABLE
-运行 RUNNING
-阻塞 BLOCKED
-死亡 DEAD
线程的所有状态都在Thread中State枚举中定义:
cpublic enum State{ NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;}
、、NEW状态表示刚刚创建线程还没开始执行。等到线程的start()方法调用时,才表示线程开始执行。当线程开始执行时处于RUNNABLE状态,标识线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,知道获得请求的锁。WAITING和TIMED——WAITING都表示等待状态,区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING会进行一个有限的等待。
、、等待在等什么呢?一般来说WAITING的现场正是在等待一些特殊的时间。比如通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的时间,线程就会继续执行,进入RUNNABLE状态。当前线程执行完毕后,则进入TERMINATED状态,表示结束。
线程的基本操作
新建线程
新建线程有三种方式:
-new Thread
-implements Runnable
-implements Callable
new Thread
Thread t1 =new Thread(){
@override
public void run(){
...
}
};
t1.start();
new一个Thread需要两个方法:run(),start()。
- start()方法用来创建一个线程并且让这个线程执行run()方法。
- run()方法是这个线程的任务,提供给使用者重写。
implements Runnable
源代码:
public interface Runnable {
void run();
}
运用示例:
自定义类实现Runnable并重写run()方法,用Thread实现。
public class CreateThread implements Runnable{
public static void main(String[] args){
Thread t1=new Thread(new CreateThread()){
t1.start();
}
@override
public void run(){
...
}
}
上述代码实现了Runnable接口并将该实例传入线程Thread中,这样避免重写Thread.run()方法,单纯使用接口来定义线程Thread,也是最常用的做法。
- Thread有一个特殊的构造方法:
public Thread(Runnable target)
默认的Thread.run()方法就是直接调用内部的Runnable接口。因此使用Runnable接口告诉线程该做什么更为合理。
implements Callable
源代码:
public interface Callable<V> {
V call() throws Exception;
}
可以发现Callable内部有一个call()方法并且带有返回值。
运用示例:
public class CallableTest implements Callable<String>{
private String str;
public CallableTest(String str){
this.str=str;
}
@override
public String call() throws Exception{
···
return this.str;
}
}
自定义类实现Callable并重写call()方法。
那么怎么实现这个线程呢?Thread是没有直接实现Callable的构造函数的。
在设计模式中建立两者之间的关系经常使用适配器。什么是适配器呢?
适配器就是接口转换器。把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
**找到Thread与Callable之间的适配类:FutureTask **
Thread有构造函数 public Thread(Runnable target)
Runnable的子类FutureTask可用于包装Callable或Runnable对象。
使用方式:
重写call()方法,用FutureTask封装Callable类,用Thread去实现线程。
public class CallableTest implements Callable<String>{
private String str;
public a01CreateCallableThread(String str) {
this.str = str;
}
@Override
public String call() throws Exception {
System.out.println("callable is called ");
return this.str;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> c1 = new a01CreateCallableThread("xxx");
FutureTask<String> task = new FutureTask<String>(c1);
long beginTime = System.currentTimeMillis();
//创建线程
new Thread(task).start();
//调用get()方法阻塞主线程
String str = task.get();
long endTime = System.currentTimeMillis();
System.out.println("hello :" + str);
System.out.println("time :" + (endTime - beginTime) / 1000);
}
}
所以Callable与Runnable最大的区别就是Callable的执行方法call()有返回值,换个说法,Callable可以生成有返回值的线程。Future是一种异步任务监视器,以后的章节应该会说明,此处可忽略。
终止线程
一般来说,线程执行完毕就会结束,无需手动关闭。但是一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。
如何正常地关闭一个线程呢?
Thread提供了一个stop()方法,但是已经被弃用了,因为stop()会强行把执行到一半的线程终止,可能会引起数据不一致的问题。
- 使用退出标志退出线程
用一个boolean值来控制while循环是否退出
@Override
public void run(){
while(!exit){
//do something
}
}
中断线程
Thread.interrupt()
Thread提供interrupt()方法来中断线程(只是给一个中断的标记,并不会真正中断线程,真正中断是因为退出while循环)
提供isInterrupted()方法来判断是否被中断
提供interrupted()方法既可以判断是否中断又可以清除中断状态
示例:
public class a02InterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("~~~t1在执行~~~");
if (Thread.interrupted()) {
System.out.println("Interrupted!");
break;
}
}
}
};
t1.start();
/**
* 主线程等待两秒
*/
Thread.sleep(2000);
/**
* 中断t1
*/
t1.interrupt();
}
}
结果是t1运行两秒后会中断.
Thread.sleep(long millis)
Thread.sleep()方法会让当前线程休眠一段时间,之后继续执行,期间如果被中断会抛出InterruptedException中断异常.
等待和通知
JDK提供了两个非常重要的方法:Object.wait()和Object.notify(),这两个方法在Object类中.
-
当一个对象实例调用wait()方法后,当前线程就会在这个对象上等待.比如在线程A中,调用了obj.wait()方法,那么线程A就会停止执行转为等待状态.一直等待到其他线程调用该对象obj.notify()方法为止.
-
如果一个线程调用了obj.wait()方法,那么它会进入object对象的等待队列(可能有多个线程在等待).当obj.notify()方法被调用时,随机从等待队列中随机选择一个线程唤醒.
-
Object提供notifyAll()方法唤醒等待队列中的所有线程.
需要注意的一点:
wait()和notify()方法都需要先获得目标对象的一个监视器,也就是目标对象需要有锁并且运行wait()或notify()方法的线程还需要获得这个锁.
代码示例:
public class a03WaitXNotify {
final static Object object = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("t1 start");
try {
System.out.println("t1 is waiting");
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 is end");
});
Thread t2 = new Thread(() -> {
synchronized (object) {
System.out.println("t2 start");
System.out.println("object notify");
object.notify();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
/**
* wait 和 notify 方法,都必须在synchronized代码块中执行,目的是为了获得监视器
* t1 获得监视器,执行object.wait进行等待,并释放监视器
* t2 获取监视器,执行object.notify释放t1,并且释放监视器
* t1 被唤醒,继续执行
*/
t1.start();
t2.start();
}
}
结果是:
t1与t2的执行流程如下:
挂起和继续执行
Thread类提供suspend()方法和resume()方法来将线程挂起和继续执行.
已经被废弃.原因是suspend()方法在导致线程暂停的同时并不会释放任何的锁资源.其他任何线程想要访问被它占用的锁都会被牵连.
而且,如果suspend()方法意外的在suspend()方法前面执行就会导致死锁.线程即使suspend()了也在RUNNABLE状态
可以用wait()和notify()来替代,wait()会在等待的时期让出锁资源避免了资源浪费.
代码示例:
public class a03WaitXNotifyKillSuspend {
/**
* wait notify 替代 suspend 的线程挂起方案
*/
public static Object u = new Object();
public static class ChangeObjectThread extends Thread {
volatile boolean suspendme = false;
/**
* 让线程挂起的方法
*/
public void suspendMe() {
suspendme = true;
}
/**
* 让线程继续执行的方法
*/
public void resumeMe() {
suspendme = false;
synchronized (this) {
notify();
}
}
@Override
public void run() {
/**
* 看这里,之前是对while的中断标志进行改变来控制中断
*/
while (true) {
synchronized (this) {
/**
* 现在获得锁的情况下控制线程是否等待
* 判断自己是否被挂起(挂起标志),如果是则等待
* 否则正常执行
*/
while (suspendme) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 让出cpu
*/
synchronized (u) {
System.out.println(" in ChangeObjectThread");
}
Thread.yield();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ChangeObjectThread t1 = new ChangeObjectThread();
t1.start();
Thread.sleep(1000);
System.out.println("-----------------------------");
t1.suspendMe();
System.out.println("suspend t1 30s");
Thread.sleep(30000);
System.out.println("-----------------------------");
System.out.println("resume t1");
t1.resumeMe();
}
}
结果是: t1 输出1s后被挂起30s,之后继续执行.
如果在t1挂起期间此时有另外的线程是可以正常执行的,因为wait()会释放锁资源.
等待线程和谦让
Thread类提供join()来等待线程
提供静态方法yeild()来礼让线程
join():
第一个join()会无线等待,一直阻塞当前线程,知道目标线程执行完毕.
第二个join等待一段时间,即使目标线程没有执行完毕也会向下执行.(不等了)
public class Test {
private static volatile long _longVal = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new LoopVolatile1());
t1.start();
try {
//让主线程等待t1执行完毕
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("final _longVal is: " + _longVal);
}
private static class LoopVolatile1 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
结果是100000
如果没有join(),大家可以试一下,结果是0,或者其他数字,因为主线程执行太快了t1都没执行完就结束了.
这里插一个t2线程,大家觉得结果是多少呢? 按理来说主线程和t2等待t1执行完,然后主线程等待t2执行完,结果应该是200000.
但是结果几乎不可能为200000
public class Test {
private static volatile long _longVal = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new LoopVolatile1());
t1.start();
Thread t2 = new Thread(new LoopVolatile2());
t2.start();
try {
t1.join();
t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("final _longVal is: " + _longVal);
}
private static class LoopVolatile1 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
private static class LoopVolatile2 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
}
这就涉及到了volatile对复合操作没有原子性,而i++是一个复合操作哦!
下次看volatile
yield():
yield()会让线程让出当前cpu,重新加入争夺cpu的队列.