四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池

Thread类

Thread类实现了Runnable接口,所以Thread对象也是可运行Runnable对象,同时Thread类也是线程类

构造器

Thread()//一般用于在Thread类中覆盖定义run方法,可以使用匿名内部类进行定义
Thread(Runnable)//使用最多的情况,run方式是由Runnable参数对象提供
Thread(String name) //自定义线程名称
Thread(Runnable,String name)
… …
//常见简化写法
Thread t = new Thread(()->{
System.out.println(Thread.currentThread());
});
t.start();

常见方法:
四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第1张图片

Runnable接口

Runnable接口只定义了一个方法public void run(),这个方法要求实现Runnable接口的类实现,Runnable对象称为可运行对象,一个线程的运行就是执行该对象的run()方法

run()方法没有返回值void,而且不能抛出异常

class MyRunnable implements Runnable{
@Override
public void run()throws Exception {//语法报错,这里不允许抛出异常,如果其中有异常则需要使用
	try.catch处理
//没有返回值,如果需要记录处理结果,需要自己编程处理
}
}

//简化写法
new Thread(() -> {
	for (int i = 0; i < 10; i++) {
	System.out.println("左手画一条龙...");
	try {
		Thread.sleep(30);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}
}).start();

Callable接口

继承Thread或实现Runnable接口这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。

如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

call()方法有返回值,这个返回值可以通过泛型进行约束,允许抛出异常

class MyRunnable implements Callable<Number> {
// <>中用于指定返回值类型,必须使用引用类型,不能使用简单类型
public Number call() throws Exception {//允许抛出异常
	return null;
	}
}

//简化写法
new Thread(new FutureTask<>(()->{
	for(int i=0;i<10;i++){
		System.out.println("右手画彩虹");
		Thread.sleep(30);//因为call方法允许抛出异常
	}
	return null;
})).start();

Future接口

Future表示一个任务的生命周期,并提供了方法来判断是否已经完成或取消以及获取任务的结果和取消任务等

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消
    正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
  • isDone方法表示任务是否已经完成,若任务完成,则返回true
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就抛出TimeoutException超时异常。
Future f = new FutureTask(() -> {
	for (int i = 0; i < 1000; i++) {
		System.out.println(Thread.currentThread() + "...start..." + i);
		Thread.sleep(10);
		System.out.println(Thread.currentThread() + "...end..." + i);
}
return null;
});
if (f instanceof Runnable){
	new Thread((Runnable) f).start();
		int counter=0;
while (true) {
		Thread.sleep(20);
			System.out.println("任务是否被取消:" + f.isCancelled()+"--"+counter);
		System.out.println("任务是否执行完毕:" + f.isDone());
		counter++;
	if(counter>10)
		f.cancel(true);//取消任务的执行
	if(counter>12)
	break;
	}
}
Future f = new FutureTask(() -> {
		int res=0;
	for (int i = 0; i < 1000; i++) {
		Thread.sleep(10);
		res+=i;
}
return res;
});
if (f instanceof Runnable){
		new Thread((Runnable) f).start();
		int counter=0;
		long start=System.currentTimeMillis();
// Object obj=f.get();
		Object obj=f.get(5,TimeUnit.SECONDS);//参数1为超时时长,参数2为时长的单位,是一个枚举类型数据,超时TimeoutException
		long end=System.currentTimeMillis();
		System.out.println("get...执行时间为:"+(end-start)+"ms");
		System.out.println("线程执行结果为:"+obj);
	}

FutureTask

具体使用

FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();

FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

FutureTask是一个可取消的异步计算,FutureTask 实现了Future的基本方法,提供start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。结果只可以在计算完成之后获取,get方法会阻塞当计算没有完成的时候,一旦计算已经完成, 那么计算就不能再次启动或是取消。

一个FutureTask 可以用来包装一个 Callable 或是一个Runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution). 它同时实现了Callable, 所以也可以作为Future得到Callable的返回值。

ThreadPoolExecutor

ThreadPoolExecutor是线程池框架的一个核心类,线程池通过线程复用机制,并对线程进行统一管理

  • 降低系统资源消耗。通过复用已存在的线程,降低线程创建和销毁造成的消耗;
  • 提高响应速度。当有任务到达时,无需等待新线程的创建便能立即执行;
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗大量系统资源,还会降低系统的稳定性,使用线程池可以进行对线程进行统一的分配、调优和监控。

线程池的运行状态总共有5种,其值和含义分别如下:

  • RUNNING: 高3位为111,接受新任务并处理阻塞队列中的任务
  • SHUTDOWN: 高3位为000,不接受新任务但会处理阻塞队列中的任务
  • STOP:高3位为001,不会接受新任务,也不会处理阻塞队列中的任务,并且中断正在运行的任务
  • TIDYING: 高3位为010,所有任务都已终止,工作线程数量为0,线程池将转化到TIDYING状态,即将要执行terminated()结束钩子方法
  • TERMINATED: 高3位为011,terminated()方法已经执行结束

构造器中各个参数的含义:

1.corePoolSize

线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。

2.maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。

3.keepAliveTime

线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到线程池中的线程数为
0。

4.unit

keepAliveTime参数的时间单位。

5.workQueue

任务缓存队列,用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的任务就会被保存到任务缓存队列中,等待被执行。

一般来说,这里的BlockingQueue有以下三种选择:

  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速度),可能出现无限制的线程增长。
  • LinkedBlockingQueue:基于链表结构的阻塞队列,如果不设置初始化容量,其容量Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。
  • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。

6.threadFactory

线程工厂,创建新线程时使用的线程工厂。

7.handler

任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,就会采取任务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:

  • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
  • CallerRunsPolicy:由调用execute方法的线程执行该任务;
  • DiscardPolicy:丢弃任务,但是不抛出异常;
  • DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。

当然也可以根据应用场景实现RejectedExecutionHandler接口自定义饱和策略,如记录日志或持久化存储不能处理的任务。

Executors创建线程池

newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程

  • 用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务

newFixedThreadPool 创建一个固定大小的定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

  • 因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行

  • 可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景

  • public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

  • 适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景

newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数

  • 创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

提交任务的方式

线程池框架提供了两种方式提交任务,submit()和execute(),通过submit()方法提交的任务可以返回任务执行的结果,通过execute()方法提交的任务不能获取任务执行的结果。

关闭线程池

  • shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表
  • shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务
ExecutorService es = Executors.newFixedThreadPool(2);
		es.submit(()->{
			for(int i=0;i<10;i++){
			System.out.println("Hello "+i);
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		}
	});
		System.out.println("main......");
	ExecutorService es = Executors.newFixedThreadPool(2);
		Future f = es.submit(() -> {
			for (int i = 0; i < 10; i++) {
		System.out.println("Hello " + i);
		Thread.sleep(200); }
		return 100;
});
Object obj=f.get(); 阻塞当前main线程
System.out.println("main......");

Java内存模型

Java内存模型定义了一种多线程访问Java内存的规范。

  • Java内存模型将内存分为了主内存和工作内存。类的状态也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
  • 定义了几个原子操作,用于操作主内存和工作内存中的变量
  • 定义了volatile变量的使用规则
  • happens-before即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
    四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第2张图片
    四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第3张图片

多个线程之间是可以使用PipedInputStream/PipedOutputSteam互相传递数据通信的,它们之间的沟通只能通过共享变量来进行

  • Java内存模型JMM规定了JVM有主内存,主内存是多个线程共享的
  • 当new一个对象时,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的
public class T1 {
	private int num;
public static void main(String[] args) {
	T1 t=new T1();
	t.pp();
}
public void pp(){
//通过对num的操作实现了t1和t2之间的通信,这里目前不保证输出的正确性.可以通过同步锁
	synchronized保证数据的正确性
	Thread t1=new Thread(()->{
	for(int i=0;i<100;i++)add();
});
new Thread(()->{
	for(int i=0;i<100;i++)sub();
		}).start();
		t1.start();
}
public void add(){
	num++;
	System.out.println(Thread.currentThread()+"加法:"+num);
}
public void sub(){
	num--;
	System.out.println(Thread.currentThread()+"减法:"+num);
	}
}

Java中堆和栈有什么不同

每个线程都有自己的栈内存(栈帧),用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。

而堆是所有线程共享的一片公用内存区域

JDK1.6+引入了逃逸分析,对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值

如何在Java中获取线程堆栈

对于不同的操作系统,有多种方法来获得Java进程的线程堆栈。当获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows可以使用Ctrl + Break组合键来获取线程堆栈Linux下用kill -3命令。也可以用jstack这个工具来获取,它对线程id进行操作,可以用jps这个工具找到id。

通过使用 jps 检查当前正在运行的JAVA进程的 PID。jps –lvm

使用明确的 PID 作为 jstack 的参数来获取 thread dumps。jstack -f 5824

一般用于死锁的分析和线程执行速度很慢时的分析

JVM中哪个参数是用来控制线程的栈堆栈小的

-Xss参数用来控制线程的堆栈大小
四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第4张图片

线程操作某个对象的执行顺序

  • 从主存赋值变量到当前工作内存read and load
  • 执行代码,改变共享变量值use and assign
  • 用工作内存数据刷新主存相关内容store and write

四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第5张图片

volatile关键字

volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行原子性。而最彻底的同步要保证有序性、可见性和原子的synchronized

任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是原子的

public class VolatileTest{
public volatile int a;
public void add(int count){
a=a+count; }
}

volatile存在的意义是,任何线程对a的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作内存和主存的同步。所以,volatile的使用场景是有限的,在有限的一些情形下可以使用 volatile 变量替代锁

要使 volatile 变量提供理想的线程安全,必须同时满足两个条件

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

volatile特性

  • 保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其他线程立即可见
  • 保证有序性:当变量被修饰为 volatile 时,JMM
    会禁止读写该变量前后语句的大部分重排序优化,以保证变量赋值操作的顺序与程序中的执行顺序一致
  • 部分原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性

volatile的认识

public class Test1 {
	private static boolean flag=false;
	private static int i=0;
public static void main(String[] args) {
	new Thread(()->{
	try {
		TimeUnit.MILLISECONDS.sleep(100);//Thread.sleep(100)
		flag=true;
		System.out.println("flag changed...");
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}).start();
	while(!flag){
		i++;
	}
	System.out.println("progress end...");
	}
}

程序不能执行结束,会进入死循环状态。
解决方案:flag上添加关键字volatile

总结

Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。

1、线程内的代码能够按先后顺序执行,这被称为程序次序规则。

2、对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。

3、前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。

4、一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。

5、一个线程的所有操作都会在线程终止之前,线程终止规则。

6、一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。

7、可传递性。如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

线程状态切换

四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第6张图片
线程变化的5状态转换:

1、新建状态(New):新创建了一个线程对象。new Thread()

2、就绪状态(Runnable):线程对象创建后,其它线程调用了该对象的start()方法。只能针对处于新建状态的线程对象调用start方法,否则IllegalThreadStateException

该状态的线程位于可执行线程池中,变得可执行,等待获取CPU的使用权。

3、执行状态(Running):就绪状态的线程获取了CPU。执行程序代码。注意在一个多处理器的机器上会有多个
线程并行执行

现在大部分桌面和服务器操作系统都采用时间片轮转法的抢占式调度策略,在选择下一个执行线程时系统会考虑线程的优先级

调用yield方法可以让运行状态的线程转入就绪

4、堵塞状态(Blocked):堵塞状态是线程由于某种原因放弃CPU使用权。临时停止执行。直到线程进入就绪状态,才有机会转到执行状态。线程切换是由底层平台控制的,具有一定的随机性

堵塞的情况分三种:

(一)、等待堵塞:执行的线程执行wait()方法,JVM会把该线程放入等待池中。

(二)、同步堵塞:执行的线程在获取对象的同步锁时,若该同步锁被别的线程占用。则JVM会把该线程放入锁
池中。

(三)、其它堵塞:执行的线程执行sleep()或join()方法,或者发出了I/O请求时。JVM会把该线程置为堵塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完成时。线程又一次转入就绪状态。

5、死亡状态(Dead):线程运行完了或者因异常退出了run()方法,该线程结束生命周期。
直接调用该线程的stop方法也可以结束线程,但是这个方法容易导致数据不一致的问题,通常不推荐使用

当主线程结束时,其它线程不受任何影响,并不会随之结束,一旦子线程启动后则拥有和主线程相同的地位,并不受主线程的影响

注意:不要试图对一个已经死亡的线程调用start方法使其重新启动,该线程将不可再次作为线程执行,否则异常

不要用stop方法来停止一个线程。因为stop方法太极端,会出现同步问题,使数据不一致。所以可以考虑通过设置标志,通过return, break,异常等手段来控制流程自然停止

Thread t1=new Thread(() -> {
 	for(int i=0;i<100;i++)
	System.out.println(Thread.currentThread()+"--"+i);
});
	t1.start();
	TimeUnit.MICROSECONDS.sleep(10);
	t1.stop();
//可以通过其它方式实现线程的停止
class MyThread extends Thread {
	private boolean flag = true;
@Override
	public void run() {
	for (int i = 0; i < 100 && flag; i++)
	System.out.println(Thread.currentThread() + "--" + i);
}
	public void setFlag(boolean flag) {
	this.flag = flag;
}
}

sleep方法使得当前线程休眠

Thread类中定义的

public static native void sleep(long millis)throws InterruptedException

让当前线程休眠指定时间。休眠时间的准确性依赖于系统时钟和CPU调度机制。如果需要可以通过调用interrupt()方法来唤醒休眠线程

public void interrupt() {
if (this != Thread.currentThread()) checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}

目前流行写法:TimeUnit.SECONDS.sleep(1);

不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的。

练习:实现一个时间显示:每隔一秒钟更新一次(例如倒计时)

自定义格式的时间显示 DateFormat---SimpleDateFormat
DateFormat df=new SimpleDateFormat("yyyy-MM-ddE hh:mm:ss");
for (;;) { //相当于while(true){}
Date now = new Date();
System.out.println(df.format(now));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

当线程进入休眠态,它会定时结束休眠。如果需要提前唤醒,则需要通过interrupt方法实现。所谓的interrupt方法实际上会产生一个异常InterruptedException

public static void main(String[] args) throws Exception {
  	DateFormat df=new SimpleDateFormat("yyyy-MM-ddE hh:mm:ss");
		Thread t1=new Thread(()->{
			while(true){
				Date now=new Date();
				System.out.println(df.format(now));
		try {
//TimeUnit.SECONDS.sleep(10);
			Thread.sleep(10000);//子线程进入休眠阻塞状态,当超时后自动进入就绪状态,等待下次调度执行
			} catch (InterruptedException e) {
				e.printStackTrace();
}
}
});
		t1.start();
		Thread.sleep(200);
		t1.interrupt(); //唤醒处于休眠状态的线程,在子线程中会导致InterruptedException
		System.out.println("main.....");
}

wait(Object中定义的)

  • wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的 notify()
    方法或 notifyAll()方法,当前线程被唤醒,进入就绪状态
  • notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
  • wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或
    notifyAll()方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

让当前线程进入等待状态,当别的其他线程调用notify()或者notifyAll()方法时,当前线程进入就绪状态

wait方法必须在同步上下文中调用,例如:同步方法块或者同步方法中,这也就意味着如果你想要调用wait方法,前提是必须获取对象上的锁资源

当wait方法调用时,当前线程将会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源。

// 创建锁对象,保证唯一性
Object obj = new Object();
new Thread() {
public void run() {
// 保证等待和唤醒只能执行一个,需要使用同步技术
synchronized (obj) {
System.out.println("点外卖");
// 调用wait方法,放弃CPU的执行权,进入WAITING状态(无限等待)
obj.wait(1000);
// 唤醒之后的代码
System.out.println("外卖已到达");
}
}.start();

sleep vs wait

四种启动线程的方法:Thread、Runnable、Callable/FutureTask以及线程池_第7张图片
Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。

join方法

主要作用是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。调用这个方法的线程将被阻塞

方法join(long)的功能在内部是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点。但是sleep(long)不释放锁。

练习:编写10个线程,第一个线程从1加到10,第二个线程从11加到20…第十个线程从91加到100,最后再把十个线程结果相加

使用Callable接口,因为future.get()可以阻塞当前线程的执行

FutureTask[] fs=new FutureTask[10];
for(int i=0;i<10;i++){
fs[i]=new FutureTask<>(new MyCallable(i*10+1,(i+1)*10));
new Thread(fs[i]).start();
}
int res=0;
for(FutureTask ft:fs) res+=(Integer)ft.get();
System.out.println(res);

使用Runnable接口,必须通过join使子线程执行结束后才在主线程中累加10个子线程的结果

MyRunnable[] arr=new MyRunnable[10];
	Thread[] ts=new Thread[10];
	for(int i=0;i<10;i++){
		MyRunnable mr=new MyRunnable(i*10+1,(i+1)*10);
		arr[i]=mr;
		ts[i]=new Thread(mr);
		ts[i].start();
	}
	int res=0;
	for(int i=0;i<10;i++){
	ts[i].join();
	res+=arr[i].getRes();
}
System.out.println("计算结果为:"+res);

你可能感兴趣的:(java,多线程,线程池)