多线程基础
线程池
概念及作用
- 什么是“线程池”:它是一个“容器”,里面可以存储一些“线程对象”。
- 它的作用:
- 它可以“反复的”执行同一个线程对象,可以避免每次使用这个线程对象,而去创建这个线程对象。
- 它内部可以控制多个线程的“并发数量”;
线程池的类层次结构
- java.util.concurrent.Executor(接口):所有线程池的父接口
|–java.util.concurrent.ExecutorService(接口):我们学习使用的“线程池”: - 常用方法:
1. submit(Runnable run):用于执行Runnable线程
2. submit(Callable cal):用于执行Callable线程。 - 可以通过一个“Executors工具类”来获取ExecutorService的子类对象:
java.util.concurrent.Executors(工具类)的静态方法;
public static ExecutorService newFixedThreadPool(int nThreads)
代码演示:
package com.itheima.demo01_线程池的使用;
public class MyThread extends Thread {
public MyThread(){
System.out.println("构造线程对象很耗时,需要5秒...");
for (int i = 0; i < 5; i++) {
System.out.println("i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("鼓掌" + i);
}
}
}
package com.itheima.demo01_线程池的使用;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo01 {
public static void main(String[] args) {
/*MyThread t1 = new MyThread();//需要5秒
t1.start();
MyThread t2 = new MyThread();//需要5秒
t2.start();*/
//改用线程池
//1.获取线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
//2.创建自定义线程对象
MyThread t = new MyThread();//需要5秒
//3.使用"线程池"去执行t线程
service.submit(t);//开始执行
//可以让线程池反复的执行同一个t线程
service.submit(t);//开始执行
service.submit(t);//由于核心线程只有2,所以前两个线程会"同时执行",第三个线程会排队
//结束线程池
service.shutdown();//会等待所有的线程执行完毕,然后再关闭。
}
}
代码图解
实现线程的第三种方式:可以通过返回值的Callable接口
- 之前我们实现线程的两种方式:
- 继承Thread类,重写run()方法。
- 实现Runnable接口,重写run()方法。
- 之前的两种方式都有两个弊端:
- 子类重写run()方法时,不能抛出“编译期异常”;
- run()方法没有返回值,不能给它的启动线程返回一个值;
- 从JDK1.5开始,Java提供了第三种实现线程的方式:
- 自定义类,实现Callable接口,并重写call()方法。
- 启动线程(注意:启动Callable线程,必须要用线程池)
- 获取一个线程池对象:
- 使用线程池启动Callable线程,并通过Future对象获取线程的返回值;
代码演示
package com.itheima.demo02_实现线程的第三种方式_可以返回值的Callable接口;
import java.util.concurrent.Callable;
//要求:此线程要计算1--100的累加和,并返回给调用线程
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100 ; i++) {
sum += i;
Thread.sleep(50);
}
System.out.println("【线程】计算完毕!");
return sum;
}
}
package com.itheima.demo02_实现线程的第三种方式_可以返回值的Callable接口;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Demo02 {
public static void main(String[] args) {
//1.获取线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
//2.创建自定义线程对象
MyThread t = new MyThread();
//3.使用线程池去执行我们的线程
Future<Integer> future = service.submit(t);//不阻塞,启动线程
while (!future.isDone()){//判断线程是否结束,如果没结束,就可以做点其他事情,可以避免提前调用get()而产生的阻塞。
System.out.println("线程没执行完,可以做一些其他的事情!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
System.out.println("获取future的结果:");
Integer result = future.get();//阻塞的,等待计算完成
System.out.println("线程执行结果:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//关闭线程池
service.shutdown();
}
}
代码图解
线程状态
线程状态概述
-
一个线程,从创建对象开始,到最后被销毁,中间由于调用线程的各种方法,会使线程进入不同的“状态”。
-
一个线程可能的几种状态:
等待和唤醒
- 等待和唤醒机制:它是“两个线程”之间相互协作的一种机制。一个线程先开始工作,当发现一些问题时,可以主动“退出—释放锁”,让另一个线程开始执行,第二个线程拿到锁后,就可以开始工作—解决问题,解决完问题后,再“唤醒-让之前等待的线程,从’无限等待’状态进入到’可运行状态’去抢锁”。这样就实现了:两个线程相互协作,共同完成一项任务。
- 实现前提:
- 要有两个线程;
- 两个线程要共用一把锁;
- 注意:
- wait()和notify()、notifyAll()都是Object的方法,因为“任何对象都可以做锁”,所以任何对象都可以“进入等待”。
- wait(long s)和sleep(long s)的区别:
- wait(long s)可以被唤醒;
sleep(long s)必须时间到,然后自己醒; - wait()方法会释放锁;
sleep()方法不会释放锁。
- wait(long s)可以被唤醒;
代码演示
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
//1.定义一把锁
Object obj = new Object();
//2.定义两个线程
//第一个线程:主工作线程
new Thread(){
//必须要先启动,先拿到锁
@Override
public void run() {
synchronized (obj) {
for (int i = 0; i < 50; i++) {
System.out.println("白雪公主线程:i = " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 20) {
System.out.println("白雪公主线程 i = 20,主动退出,释放锁,进入到'无限等待状态',等待白马王子...");
try {
obj.wait();//无限等待,等待被其他线程唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}.start();
Thread.sleep(1000);//为了保证让"白雪公主"线程先运行
//白马王子线程
new Thread(){
@Override
public void run() {
System.out.println("【白马王子】线程启动,获取锁......");
synchronized (obj){
System.out.println("【白马王子】拿到锁,终于可以娶白雪公主了!!!");
System.out.println("【白马王子】和白雪公主举行婚礼!");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【白马王子】婚礼举行完毕,和白雪公主愉快的度过余生!");
//唤醒白雪公主线程,继续生活
obj.notifyAll();//【特别注意】唤醒,不释放锁-会让在这个锁上等待的线程,由'无限等待状态'转入到'可运行状态',去抢锁。
}//执行完同步代码块,才释放锁
}
}.start();
}
}
包子铺案例
- 包子铺案例:这个案例需要两个线程:一个线程无限“生产包子”,一个线程无限“获取包子”,我们会有一个集合,来存放包子。
- 我们的程序必须要控制:生产一个,卖出一个。不会生产多个!生产一个,来买包子的,也必须要买走一个。
代码演示
- 生产者线程:
public class SetThread extends Thread {
@Override
public void run() {
int index = 1;
while (true) {
synchronized (Demo05.bzList){
if (Demo05.bzList.size() > 0) {//有包子
try {
System.out.println("【生产者】有包子,等待消费者...");
Demo05.bzList.wait();
System.out.println("【生产者】被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没有包子,或者被唤醒
//生产一个包子
String s = "包子" + index++;
Demo05.bzList.add(s);
System.out.println("【生产者】生产:" + s);
//唤醒消费者线程
Demo05.bzList.notify();
}
}
}
}
- 消费者线程
public class GetThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Demo05.bzList) {
if (Demo05.bzList.size() == 0) {//没有包子
try {
Demo05.bzList.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有包子,或者被唤醒
//取走一个包子
String s = Demo05.bzList.remove(0);
System.out.println("【消费者】拿走:" + s);
//唤醒生产者线程
Demo05.bzList.notify();
}
}
}
}
- 测试类
public class Demo05 {
public static ArrayList<String> bzList = new ArrayList<>();
public static void main(String[] args) {
//1.启动两个线程
new SetThread().start();
new GetThread().start();
}
}
定时器Timer的使用
- 创建一个Timer对象
Timer t = new Timer(); - 设置时间、任务,并会启动任务。
- public void schedule(TimerTask task, long delay) 在指定的延迟之后安排指定的任务执行。一次性定时器
- public void schedule(TimerTask task, long delay,long period)在指定的delay延迟之后开始task任务,然后会每隔period时间,会反复执行task任务。往复定时器。
- public void schedule(TimerTask task, Date time) 在指定的时间安排指定的任务执行。
- public void schedule(TimerTask task, Date firstTime, long period)从指定的时间开始,对指定的任务执行重复的 固定延迟执行 。
代码演示
public class Demo06 {
public static void main(String[] args) throws ParseException {
//1.public void schedule(TimerTask task, long delay):一次性
/*Timer t1 = new Timer();
t1.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("嘣......");
t1.cancel();//结束"定时器"
}
},1000 * 2);
System.out.println("t1已启动,2秒后开始!");*/
//2.public void schedule(TimerTask task, long delay,long period):往复定时器
/* Timer t2 = new Timer();
t2.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("嘣......");
}
},1000 * 2,1000);*/
//3).public void schedule(TimerTask task, Date time) 在指定的时间安排指定的任务执行。
/*Timer t3 = new Timer();
String str = "2020-07-28 14:35:00";
t3.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("扫描全盘......");
t3.cancel();
}
},new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str));
*/
//4).public void schedule(TimerTask task, Date firstTime, long period)从指定的时间开始,对指定的任务执行重复
//的 固定延迟执行 。
Timer t4 = new Timer();
String str = "2020-07-28 14:37:15";
t4.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("扫描全盘...");
}
},new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str),1000);
}
}
死锁
- 当两个线程,交替使用“两把锁”的时候,就可能产生“死锁”的情况,我们编程时,应该避免这种情况。
- 如果项目需要两个线程交替使用两把锁,应该有一些预案,来防止死锁。
代码演示
package com.itheima.demo03_死锁的现象演示;
public class ThreadA extends Thread {
@Override
public void run() {
System.out.println("【线程A】去拿锁1...");
synchronized (Demo03.obj1) {
System.out.println("【线程A】拿到锁1,去拿锁2...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Demo03.obj2) {
System.out.println("【线程A】拿到锁2...");
}
}
}
}
package com.itheima.demo03_死锁的现象演示;
public class ThreadB extends Thread {
@Override
public void run() {
System.out.println("【线程B】去拿锁2...");
synchronized (Demo03.obj2) {
System.out.println("【线程B】拿到锁2,去拿锁1...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Demo03.obj1) {
System.out.println("【线程B】拿到锁1...");
}
}
}
}
package com.itheima.demo03_死锁的现象演示;
public class Demo03 {
public static Object obj1 = new Object();//锁1
public static Object obj2 = new Object();//锁2
public static void main(String[] args) {
//1.启动两个线程
new ThreadA().start();
new ThreadB().start();
}
}