并发编程-线程的认识、基本操作
并发编程——多线程的认识、基本操作
多线程
线程的定义
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中, 是进程中的实际运作单位
Java线程从创建到销毁,一共经历6个状态
NEW: 初始状态,线程被构建,但是还没有调用start方法
RUNNABLED: 运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
BLOCKED: 阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
WAITING: 等待状态
TIME_WAITING: 超时等待状态,超时以后自动返回
TERMINATED: 终止状态,表示当前线程执行完毕
多线程的本质是:合理的利用多核心CPU资源来实现线程的并行处理,来实现同一个进程内的多个任务的并行执行,同时基于线程本身的异步执行特性,提升任务处理的效率
多线程优势
1.在多核CPU中,利用多线程可以实现真正意义上的并行执行
2.在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性
3.线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快
多线程应用场景
因此多线程一般应用于以下及类似场景
1.使用多线程实现文件下载
2.后台任务:如定时向大量(100W以上)的用户发送邮件
3.异步处理:记录日志
4.多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来 协作处理,多任务的分割,由一个主线程分割给多个线程完成
线程的基本操作
创建线程
线程的创建有三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
简单总结三者之间的区别:如果需要获得返回值,推荐使用Callable。由于Java中只能实现单继承(接口除外),因此某个线程类如果需要继承别的类的时候实现Runnable接口,反之直接继承Thread类即可。
Thread.join()
Thread.join()方法本质上是wait/notifyall,他可以用来保证线程执行结果的可见性。我们可以看一下join()的源码
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
这里写一段示例代码,在这段代码中,在t2启动之前,主线程先执行t1.join(),导致主线程执行到这一行代码时进入阻塞一直到t1线程执行完成,因此对于t2来讲,t1的执行结果是可见的,所以会正确返回结果i=4。
public class ThreadJoinDemo {
private static int x=0;
private static int i=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
//阻塞操作
i=1;
x=2;
});
Thread t2=new Thread(()->{
i=x+2;
});
//两个线程的执行顺序,
t1.start();
t1.join(); //t1线程的执行结果对于t2可见(t1线程一定要比t2线程有限执行) --- 阻塞
t2.start();
Thread.sleep(1000);
System.out.println("result:"+i);
}
Thread.sleep()
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断
sleep()首先挂起线程并修改运行状态,利用提供的参数来设置一个定时器,时间结束后定时器触发,内核收到中断后修改线程运行状态。
特别强调sleep(0),sleep(0)虽然只睡0秒,但可以利用它修改线程运行状态,使它重新进行CPU资源抢占。
wait和notify
一个线程修改了一个对象的值,而另个线程感 知到了变化,然后进行响应的操作
首先,wait和notify是基于对互斥资源的争夺。也就是说wait和notify方法一定是互斥存在的,因此synchronized是一种很好的解决方案。两个方法分别处于不同的线程之中,它们依靠synchronized进行线程间的通信。最出名的例子就是生产者消费者模型了。
public class WaitNotifyDemo {
public static void main(String[] args) {
Queue<String> queue=new LinkedList<>();
int size=10000; //数据量足够大容易观察到线程运行的过程
Producer producer=new Producer(queue,size);
Consumer consumer=new Consumer(queue,size);
Thread t1=new Thread(producer);
Thread t2=new Thread(consumer);
t1.start();
t2.start();
}
}
中断线程
众所周知,Thread.stop()是一个已经被废弃的方法,因为调用stop()方法会使得线程变得不安全。
停止一个线程会导致其解锁其上被锁定的所有监视器(监视器以在栈顶产生ThreadDeath异常的方式被解锁)
这句话的意思就是,假设一个线程指向一个对象并正在修改它,调用stop()方法会使得该对象的状态变得不确定的,当线程在受损的对象上进行操作时,会导致任意行为。这种行为可能微妙且难以检测,也可能会比较明显。不像其他未受检的(unchecked)异常, ThreadDeath 悄无声息的杀死及其他线程。因此,用户得不到程序可能会崩溃的警告。崩溃会在真正破坏发生后的任意时刻显现,甚至在数小时或数天之后。
因此我们采用Thread.interrupt()和Thread.interrupted()方法来中断线程。调用interrupt()进行友好的中断,可以对处于阻塞状态的线程进行中断,例如while()中写入Thread.current().interrupted()作为是否中断的标志,或者线程处于sleep()阻塞。
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
//Thread.currentThread().isInterrupted() 默认是false
//正常的任务处理..
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
//抛出异常来相应客户端的中断请求
e.printStackTrace();
}
});
Thread thread2=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
i++;
}
});
thread.start();
thread2.start();
Thread.sleep(5000);
//interrupt 这个属性有false-true
thread.interrupt(); //中断(友好)
thread2.interrupt();
System.out.println("i:"+i);
}
}
用interrupt()中断sleep()时会抛出InterruptedException: sleep interrupted异常
i为负数是因为i超过int的范围。
interrupt这个属性是在JVM中进行维护。Thread.interrupted()对设置中断标识的线程复位,并且返回当前的中断状态。