1 线程
线程是大多数操作系统调度的基本单元。
一个程序作为一个进程运行,程序运行过程中可能会创建多个线程。一个线程在一个时刻只能运行在一个CPU核心上。
为什么使用多线程?
①异步执行
②利用多CPU资源实现真正意义上的并列执行
线程的应用场景:
①使用多线程实现文件下载
②后台任务:如定时向大量(100W以上)的用户发送邮件
③异步处理:记录日志
④多步骤的任务处理:可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成。
多线程的本质是:
合理利用多核心CPU资源来实现线程的并行处理,来实现同一个进程内的多个任务的并行执行,同时基于线程本身的异步执行特性,提升任务处理效率。
2 在Java中使用多线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现callable接口
2.1 继承Thread类
public class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
public static void main(String [] args) {
ThreadDemo demo = new ThreadDemo();
demo.start();
}
}
2.2 实现Runnable接口
public class RunableDemo implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("当前线程:"+Thread.currentThread().getName());
}
public static void main(String[] args) {
RunableDemo runnable = new RunableDemo();
Thread thread = new Thread(runnable);
thread.start();
}
}
2.3 实现callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getName());
return "hello";
}
public static void main(String [] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(1); //线程池
Future<String> future = executorService.submit(new CallableDemo());
System.out.println(future.get());
// future.get()是一个阻塞方法。阻塞到Callable 方法返回值之后输出
}
}
区别:
Callable实现了,线程返回一个返回值(带类型)
Runnable是接口。(多重实现——可以用于继承了父类的子类中实现线程)
Thread是继承。(单继承)
3 Java线程的生命周期
Java线程从创建到销毁,一共经历6个状态(不一定每一种状态都经历):
- NEW:初始状态,线程被构建,但是还没有调用start方法
- RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”
- BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权,阻塞也分为几种情况
- WAITING:等待状态
- TIME_WAITING:超时等待状态,超时以后自动返回
- TERMINATED:终止状态,表示当前线程执行完毕
4 线程的基本操作和原理
4.1 Thread.join()
1.Thread.join的作用是保证线程执行结果的可见性
4.1.1 示例
对于下述代码的执行结果,有可能是1,也有可能是4。主要原因在于t1和t2不一定是顺序执行的,假如t2先执行完了,再执行t1,就会出现i=1的情况。
package demoThread;
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();
t2.start();
Thread.sleep(1000);
System.out.println("result:"+i);
}
}
但是如果加入了Thread.join()方法,就能够保证使用了join的线程先执行完。如下述代码所示:
package demoThread;
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);
}
}
4.1.2 Join方法工作流程
底层使用 wait(0) 方法对主线程进行阻塞,当前线程结束之后,才在JVM中使用notify方法唤醒被阻塞的线程。(ensure_join方法中有个lock(notify_all(主线程))去唤醒主线程)。
为什么阻塞的是主线程?
Thread.join的本质其实是wait/notifyall
4.2 Thread.sleep()
4.2.1 示例
sleep的单位是ms,因此1秒=1000ms。
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断(InterruptedException)。
如下述代码所示:
package demoThread;
public class SleepDemo extends Thread {
public static void main(String[] args) {
new SleepDemo().start();
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("begin:"+System.currentTimeMillis());
try {
Thread.sleep(3000); //睡3秒
System.out.println("end:"+System.currentTimeMillis());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行结果:
begin:1617955794912
end:1617955797913
4.2.2 Thread.sleep的工作流程
- 挂起线程并修改其运行状态(为阻塞状态)
- 用sleep()提供的参数来设置一个定时器。
- 当时间结束,定时器会触发,内核收到中断后修改线程的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度
4.2.3 相关问题
- 假设现在是 2019-11-18 12:00:00.000,如果我调用一下Thread.Sleep(1000) ,在 2019-11-18 12:00:01.000 的时候,这个线程会不会被唤醒?
会睡眠1秒。但是1s之后,有可能会有其他进程使用cpu资源,此时需要等那个进程结束才可以使用。还可能被其他进程抢占。
- Thread.Sleep(0) 的意义
其作用类似于:Thread.yeild()。含义是出让CPU,触发操作系统重新进行CPU竞争。
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法(所有进程排成队列,操作系统会依次分配),而
Windows则属于抢占式(如果一个进程得到了CPU资源,那么除非自己放弃资源,否则会霸占一直到运行结束)的。
4.3 Thread.wait()及notify()
如何实现:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行响应的操作
4.3.1 生产者消费者示例
在下面的实例中,生产者和消费者共用一个队列,完成消费生产过程。
当bags数量为空时,消费者进入阻塞状态,生产者生产bags,并调用notifyAll方法唤醒阻塞的消费者。
当bags数量达到最大值(size)时,生产者进入阻塞状态,消费者消费bags之后,调用notifyAll方法唤醒阻塞的生产者。
Producer.java
import java.util.*;
public class Producer implements Runnable{
private Queue<String> bags;
private int size;
public Producer(Queue<String> bags, int size) {
this.bags = bags;
this.size = size;
}
@Override
public void run() {
int i=0;
while(true) {
i++;
synchronized (bags) {
while(bags.size()==size) {
System.out.println("bags已经满了");
try {
bags.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("生产者生产bag:"+i);
bags.add("bag"+i);
// 唤醒处于阻塞状态的消费者
bags.notifyAll();
}
}
}
}
Consumer.java
import java.util.Queue;
public class Consumer implements Runnable{
private Queue<String> bags;
private int size;
public Consumer(Queue<String> bags, int size) {
this.bags = bags;
this.size = size;
}
@Override
public void run() {
while(true) {
synchronized (bags) {
while(bags.isEmpty()) {
System.out.println("bags为空");
try {
bags.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String bag = bags.remove();
System.out.println("消费者消费bag"+bag);
// 唤醒处于阻塞状态的生产者
bags.notifyAll();
}
}
}
}
WaitNotifyDemo.java
import java.util.*;
public class WaitNotifyDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
int size = 10;
Producer pro = new Producer(queue, size);
Consumer con = new Consumer(queue, size);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
t1.start();
t2.start();
}
}
4.3.2 为什么wait/notify需要加synchronized
1.从刚刚的案例来看,其实wait/notify本质上其实是一种条件的竞争,至少来说,wait和notify方法一定是互斥存在的,既然要实现互斥,那么synchronized就是一个很好的解决方法
2. wait和notify是用于实现多个线程之间的通信,而通信必然会存在一个通信的载体,比如我们小时候玩的游戏,用两个纸杯,中间用一根线连接,然后可以实现比较远距离的对话。而这根线就是载体,那么在这里也是一样,wait/notify是基于synchronized来实现通信的。也就是两者必须要在同一个频道也就是同一个锁的范围内
4.4 Thread.interrupted 和 Thread.interrupt(如何正确终止一个线程)
可以尝试使用构建全局变量实现线程间的通信。从而实现终止线程。(不能使用stop()方法)
import java.util.concurrent.TimeUnit;
public class Stopdemo {
static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new StopThread());
t1.start();
TimeUnit.SECONDS.sleep(2);
stop = true;
}
static class StopThread implements Runnable{
@Override
public void run() {
while(!stop) { //while(true)
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
4.4.1 interrupt方法
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
在JVM中,设置了interrupt这样一个属性。
Thread.currentThread().isInterrupted()默认是flase。
但我们可以通过调用Thread.interrupt();方法来显式加上使interrupt值变为true。
public class StopDemo2 {
private static int i;
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()) { //判断中断标识
i++;
}
});
thread.start();
thread.interrupt(); //中断(友好)
}
}
除了while之外。线程处于阻塞状态下时,中断才有意义。(Join、Wait等)
所以除了while循环中判断中断标识来结束循环之外,阻塞时,可以调用中断,当JVM发现某段代码中可能存在阻塞并且在代码外调用了中断时,就会唤醒进程,并抛出 InterruptedException。
如下的这种情况就会抛出InterruptedException异常:
package demoThread;
public class StopDemo2 {
private static int i;
public static void main(String[] args) {
Thread thread = new Thread(()->{
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread.interrupt();
}
}
线程被中断的唯一情形是:sleep里面的run方法结束。
而当while(true)/Join/Wait/Sleep等时,就需要中断来结束线程。
4.4.2 interrupted方法
Thread.interrupted()对设置中断标识的线程复位,并且返回当前的中断状态。
假如Thread.currentThread().isInterrupted()==true,就说明被中断过。
当我们使用Thread.interrupted()方法后,会对中断标识复位,使其状态变为没有中断过。
import java.util.concurrent.TimeUnit;
public class StopDemo2 {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while(true) {
if(Thread.currentThread().isInterrupted()) {
System.out.println("before:"+Thread.currentThread().isInterrupted());
Thread.interrupted(); //对中断标志复位
System.out.println("after:"+Thread.currentThread().isInterrupted());
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt(); //中断
}
}