Java多线程详解
基本概念
- 程序
保存在电脑硬盘上的可执行文件叫做程序 - 进程
在计算机内存中运行的程序,叫做进程 - 线程
在进程中 运行的程序叫做线程 - 单线程
单线程好比就是一个主线程,1个窗口排队买票
在java中,执行main方法的线程,成为主线程 - 多线程
在进程内部,同时运行多个程序,在java中体现在同时执行多个方法,多线程就是同一时间干不同的事情。例如:
小明上班打卡时,电话突然响了,边接电话边打卡,这就是多线程。
多线程的好处:
可以提高CPU的执行效率,充分利用了CPU的资源,带来良好的用户体验 - 并发与并行
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:多个人做同一件事。
多线程的应用场景:
1.高并发
2.后台处理
3.大任务
多线程的创建与启动
创建线程的四种方式:
方式1:继承Thread类实现多线程
需求:使用多线程技术打印奇数、偶数,其中一个线程打印奇数,另一个线程打印偶数,这两个线程同时运行。
- 自定义一个类,并继承java.lang.Thread类
- 重写run方法
- 调用start方法开启多线程
public class TestJiAndOu {
public static void main(String[] args) {
EvenThread even = new EvenThread();
OddThread odd = new OddThread();
//注意:需要同时调用start方法,不可创建后,立即调用,这样会达不到多线程
even.start();
odd.start();
}
}
/**
*
* 打印1-100之间的偶数
*/
class EvenThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
System.out.println("偶数" + i);
}
}
}
}
/**
*
* 打印1-100之间的奇数
*/
class OddThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 != 0) {
System.out.println("奇数" + i);
}
}
}
}
多线程执行流程
- 执行main方法的线程,称为主线程,执行run方法的线程,称为子线程
- 执行start方法之前,只执行一次主线程,当调用start方法之后,线程数瞬间由1个变成2个,其中主线程继续执行main方法,然后新创建的子线程执行run方法
- main方法执行完毕,主线程结束,run方法执行完毕,子线程结束
Thread类常用方法:
- Thread() 创建新对象
- Thread(String threadname) 指定线程名称创建线程
- void start() 启动线程
- run() 线程的主要实现业务在该方法内实现
- getName() 返回线程的名称
- setName() 设置线程名称
- static void sleep(long milis) 让当前线程指定毫秒数休眠
- static Thread currentThread() 获得当前线程
- setPriority(int newPriority) 更改线程的优先级
- void join() 等待该线程终止
- void interrupt() 中断线程
- booleans isAlive() 测试线程是否出于活动状态
Thread类方法练习
public class ThreadTest {
public static void main(String[] args) {
//创建线程,并调用start方法启动多线程
new Thread(new Thread1()).start();
//将主线程优先级调为最大,但不会保证一定会是主线程执行
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
if (i == 5) {
//当i==5时等待主线程终止
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
//当i==8时终止线程,判断线程是否存活
// if (i == 8) {
// Thread.currentThread().interrupt();
// System.out.println(Thread.currentThread().getName() + " - :线程终止");
// boolean flag = Thread.currentThread().isAlive();
// System.out.println(Thread.currentThread().getName() + "是否存活:" + flag);
// }
//每循环一次休眠100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
龟兔赛跑小练习
需求:编写龟兔赛跑多线程程序,设赛跑长度为30米
兔子每跑完10米休眠10秒
乌龟跑完20米休眠1秒
乌龟和兔子每跑完1米输出一次结果,看看最后谁先跑完
public class Test {
public static void main(String[] args) {
Rabbit rabbit = new Rabbit();
Tortoise tortoise = new Tortoise();
rabbit.setName("兔子");
tortoise.setName("乌龟");
rabbit.start();
tortoise.start();
}
}
class Rabbit extends Thread {
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
if (i == 30) {
System.out.println(Thread.currentThread().getName() + "跑完了");
break;
}
System.out.println(Thread.currentThread().getName() + "跑了" + i + "米");
if (i % 10 == 0) {
System.out.println(Thread.currentThread().getName() + "开始休眠");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "休眠完毕,开始跑了");
}
}
}
}
class Tortoise extends Thread {
@Override
public void run() {
for (int i = 1; i <= 30; i++) {
if (i == 30) {
System.out.println(Thread.currentThread().getName() + "跑完了");
break;
}
System.out.println(Thread.currentThread().getName() + "跑了" + i + "米");
if (i % 20 == 0) {
System.out.println(Thread.currentThread().getName() + "开始休眠");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "休眠完毕,开始跑了");
}
}
}
}
方式2:实现Runnable接口实现多线程
public class ThreadTest {
public static void main(String[] args) {
Thread1 t = new Thread1();
new Thread(t).start();
}
}
class Thread1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
方式3:实现Callable+FutureTask实现多线程
实现Callable接口的方式可以将子线程的结果返回到主线程,也可以处理异常
步骤:
- 创建一个类,实现Callable接口
- 重写call方法,编写多线程代码,并返回结果
- 创建FutrueTask对象,并将Callable接口的实现类对象传递到FutrueTask构造方法
- 创建Thread对象,并将FutrueTask对象传递到构造方法,并调用start方法
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableTest call = new CallableTest();
FutureTask<Integer> ftask = new FutureTask<>(call);
new Thread(ftask).start();
Integer i = ftask.get();
System.out.println("返回的结果为:" + i);
}
}
class CallableTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
System.out.println(i);
sum += i;
Thread.sleep(100);
}
return sum;
}
}
运行结果如下:
使用Callable实现多线程会返回一个指定类型的结果,get方法获得返回的具体数据。
方式4:线程池实现多线程
思路:
可以先初始化一个线程池,需要时从线程池中取出线程执行异步任务,使用完再归还到线程池,这样可以避免频繁的创建、销毁线程,从而提升系统的性能
步骤:
- 初始化一个线程池,并指定线程个数【通常是CPU内核数*2】
- 从线程池中取出线程执行异步任务
public class ThreadTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//初始化线程池,创建方式为指定线程池数量创建
ExecutorService pool = Executors.newFixedThreadPool(15);
for (int i = 1; i <= 10; i++) {
CallableTest call = new CallableTest("callable" + i);
Future<Integer> ft = pool.submit(call);
System.out.println(call.getName() + "随机到的数字 :" + ft.get());
}
pool.shutdown();
}
}
class CallableTest implements Callable<Integer> {
private String name;
public CallableTest(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception {
//当前线程睡一会在执行
Thread.sleep(100);
//返回一个10-100的整数
return (int)(Math.random()*90)+10;
}
public String getName() {
return name;
}
}
原生方式创建线程池:
核心7个参数:
corePoolSize:核心线程池的数量,初始化线程池时创建的线程个数
maximumPoolSize,线程池中最多创建多少个线程
keepAliveTime,保持存活的时间,异步任务执行完毕后,等待多长时间销毁多余的线程
unit,存活时间单位,例如:时、分、秒
workQueue,工作队列/阻塞队列,如果任务有很多,就会将多余的的任务放到队列里面,只要有线程空闲,就会去队列里面取出新的任务执行,通常使用 LinkedBlockingQueue(无限队列)
threadFactory,创建线程的工厂,采用默认值{Executors.defaultThreadFactory}
handler,阻塞队列满了,按照我们指定的拒绝策略拒绝执行任务
//初始化线程池,创建方式为指定线程池数量创建
public static ThreadPoolExecutor pool =
new ThreadPoolExecutor(
//核心线程池的数量
5,
//最大线程能开到多少
100,
//线程空闲后,多开的线程多长时间休眠
10,
//线程休眠的时间单位
TimeUnit.SECONDS,
//工作/阻塞队列,一旦核心线程放满后,多余的任务会放进该队列,采用链表阻塞队列
new LinkedBlockingDeque<>(100),
//默认工厂模式
Executors.defaultThreadFactory(),
//阻塞队列满后,采用拒绝策略拒绝服务
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws InterruptedException, ExecutionException {
for (int i = 1; i <= 10; i++) {
CallableTest call = new CallableTest("callable" + i);
Future<Integer> ft = pool.submit(call);
System.out.println(call.getName() + "随机到的数字 :" + ft.get());
}
pool.shutdown();
}
}
class CallableTest implements Callable<Integer> {
private String name;
public CallableTest(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception {
//当前线程睡一会在执行
Thread.sleep(100);
//返回一个10-100的整数
return (int)(Math.random()*90)+10;
}
public String getName() {
return name;
}
}
线程安全
当多个线程操作同一个共享数据时,当一个线程还未结束时,其他的线程参与进去,此时就会导致共享数据的不一致。
例:
public class ThreadTest {
//初始化线程池,创建方式为指定线程池数量创建
public static ThreadPoolExecutor pool =
new ThreadPoolExecutor(
//核心线程池的数量
5,
//最大线程能开到多少
100,
//线程空闲后,多开的线程多长时间休眠
10,
//线程休眠的时间单位
TimeUnit.SECONDS,
//工作/阻塞队列,一旦核心线程放满后,多余的任务会放进该队列,采用链表阻塞队列
new LinkedBlockingDeque<>(100),
//默认工厂模式
Executors.defaultThreadFactory(),
//阻塞队列满后,采用拒绝策略拒绝服务
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableTest call = new CallableTest();
Future<Integer> ft = null;
for (int i = 1; i <= 100; i++) {
ft = pool.submit(call);
}
System.out.println(ft.get());
pool.shutdown();
}
}
class CallableTest implements Callable<Integer> {
private int num = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 100; i++) {
num ++;
}
return num;
}
}
结果应该是10000,因为多个线程同时去改变资源,导致数字不是10000,共有2个问题导致数字不对:
- 主线程提前运行,不管子线程是否执行完毕
- 子线程互相争夺资源,导致资源不一致,A线程取出数字,改变数字后想要放进去,但B线程已经拿走了A线程未改变之前的值,A线程改变完后放入,B线程取出改变后放入,这样就看起来是1一个线程改变了值,所以说原本是3,结果确是2,线程多了,资源也就差的越来越多。
改变:
利用synchronized同步代码块
利用synchronized同步方法
利用Lock显示锁
synchronized隐式锁实现线程安全
public class ThreadTest {
//初始化线程池,创建方式为指定线程池数量创建
public static ThreadPoolExecutor pool =
new ThreadPoolExecutor(
//核心线程池的数量
5,
//最大线程能开到多少
100,
//线程空闲后,多开的线程多长时间休眠
10,
//线程休眠的时间单位
TimeUnit.SECONDS,
//工作/阻塞队列,一旦核心线程放满后,多余的任务会放进该队列,采用链表阻塞队列
new LinkedBlockingDeque<>(100),
//默认工厂模式
Executors.defaultThreadFactory(),
//阻塞队列满后,采用拒绝策略拒绝服务
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableTest call = new CallableTest();
List<Future<Integer>> fts = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
Future<Integer> ft = pool.submit(call);
fts.add(ft);
}
int num = -1;
for (Future<Integer> future : fts) {
num = future.get() > num ? future.get() : num;
}
System.out.println(num);
pool.shutdown();
}
}
class CallableTest implements Callable<Integer> {
private int num = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 100; i++) {
//同步代码块实现线程同步
// synchronized (this) {
// num ++;
// }
intPlus();
}
return num;
}
//同步方法实现线程同步
public synchronized void intPlus() {
num++;
}
}
Lock锁显示锁实现线程安全
public class ThreadTest {
//初始化线程池,创建方式为指定线程池数量创建
public static ThreadPoolExecutor pool =
new ThreadPoolExecutor(
//核心线程池的数量
5,
//最大线程能开到多少
100,
//线程空闲后,多开的线程多长时间休眠
10,
//线程休眠的时间单位
TimeUnit.SECONDS,
//工作/阻塞队列,一旦核心线程放满后,多余的任务会放进该队列,采用链表阻塞队列
new LinkedBlockingDeque<>(100),
//默认工厂模式
Executors.defaultThreadFactory(),
//阻塞队列满后,采用拒绝策略拒绝服务
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableTest call = new CallableTest();
List<Future<Integer>> fts = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
Future<Integer> ft = pool.submit(call);
fts.add(ft);
}
int num = -1;
for (Future<Integer> future : fts) {
num = future.get() > num ? future.get() : num;
}
System.out.println(num);
pool.shutdown();
}
}
class CallableTest implements Callable<Integer> {
private int num = 0;
//该类实现于Lock接口,是Lock接口的实现类,一般使用该类进行开锁及关锁操作
ReentrantLock lock = new ReentrantLock();
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 100; i++) {
lock.lock();
//为什么要加上try/finally呢?
//因为在开锁之后有可能会发生异常导致当前线程一直占用资源不释放,不加的话后面的代码不会被执行
//加上try/finally后不管有没有异常都能正常执行关锁操作,不会导致一个线程一直占用资源发生死锁
try {
num++;
} finally {
lock.unlock();
}
//同步代码块实现线程同步
// synchronized (this) {
// num ++;
// }
// intPlus();
}
return num;
}
//同步方法实现线程同步
// public synchronized void intPlus() {
// num++;
// }
}
线程通信
线程通信:线程通信就是A线程满足一定条件后通知B线程执行操作。
例子:生产者与消费者模型
生产者生产好产品后通知消费这消费。
wait让当前线程进入阻塞状态,同时会释放锁
线程通信用到的3个方法
wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
上题:
商城有100个商品上架,等待买家购买
假设买家100个,需要让每个买家买到的商品都不一样,买家买到商品后直接走,限购1份
//采用链表阻塞队列实现商家上架商品与买家购买商品
//方法:add()表示向队列中添加元素,poll()表示从队列取出元素
static Queue<Integer> duilie = new LinkedBlockingDeque<Integer>();
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
EMallRun emall = new EMallRun();
for (int i = 1; i <= 100; i++) {
Customor customor = new Customor("customor" + i);
pool.submit(customor);
}
pool.submit(emall);
pool.shutdown();
}
}
class EMallRun implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
synchronized (StoreTest.duilie) {
StoreTest.duilie.add(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("商家已上货,等待买家购买...");
//通知买家有货,可以购买
StoreTest.duilie.notify();
}
}
}
}
class Customor implements Runnable {
private String num;
public Customor(String num) {
super();
this.num = num;
}
@Override
public void run() {
synchronized (StoreTest.duilie) {
try {
System.out.println("等待商家上货");
//阻塞当前线程,并释放同步监听器
StoreTest.duilie.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//获取队列中的商品
Integer i = StoreTest.duilie.poll();
if (i != null) {
System.out.println(num + "买到第" + i + "个商品");
}
}
}
}
再来道题巩固下:
基于生产/消费模式实现网上商城抢货模式
网上商城推出高性价比产品,
10个买家来抢货,
商城共放出100个产品
最后输出每个买家抢到的产品数量
思路:商城推出商品后,通知全体买家有货,让所有买家一块来抢货,谁抢到是谁的,抢到后,商城继续上架,直到上架产品到100不再上架,抢货结束
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
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;
import java.util.concurrent.LinkedBlockingDeque;
public class MallTest2 {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(15);
EMallRun2 emall = new EMallRun2();
//实例化顾客集合,为了好获取数据
List<CustomorRun2> customorRuns = new ArrayList<>();
//实例化顾客的返回值集合,为了将排出来的线程有序
List<Future<Boolean>> futures = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
//实例化线程,并设置名称
CustomorRun2 customorRun = new CustomorRun2("customor" + i);
//放入顾客集合
customorRuns.add(customorRun);
//将顾客先提交到线程池,然后接收返回值,
Future<Boolean> future = pool.submit(customorRun);
//再将返回的值放入顾客返回值集合中
futures.add(future);
}
//将商城类提交到线程池中
pool.submit(emall);
//关闭线程池
pool.shutdown();
//存放总共抢到的数量
int sumCount = 0;
//循环取出数据
for (int i = 0; i < 10; i++) {
//获取当前索引对应的线程的返回值
try {
futures.get(i).get();
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//运行到这里表示已经返回值,从顾客集合中取出顾客
CustomorRun2 customor = customorRuns.get(i);
//输出顾客信息
System.out.println(customor.getCustomorName() + " - 抢了" + customor.getMyBuyedCount() + "个");
//循环加入每个顾客抢到的商品数量
sumCount += customor.getMyBuyedCount();
}
System.out.println("总共抢了" + sumCount + "个商品");
}
}
class EMallRun2 implements Runnable {
static Queue<Integer> queue = new LinkedBlockingDeque<Integer>();
//商家总共上架的数量
static int proCount = 0;
@Override
public void run() {
//商家上架100个货物
for (int i = 1; i <= 100; i++) {
//进行线程同步,当前线程监听器与买家监听器需相同
synchronized (queue) {
//满足要求后,向队列中添加商品
queue.add(i);
//当前线程队列通知全体买家已有货物,可以抢货
queue.notifyAll();
}
try {
Thread.sleep((int)(Math.random()*100)+50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class CustomorRun2 implements Callable<Boolean> {
private String customorName;
private int myBuyedCount = 0;
public CustomorRun2(String customorName) {
super();
this.customorName = customorName;
}
@Override
public Boolean call() throws Exception {
//无限循环去抢商品,10个买家去抢购
while (true) {
//上锁,数据会不安全,商家队列作为锁的监听器,
synchronized (EMallRun2.queue) {
//当商家的货全部上架完毕后,退出循环,抢货结束
if (EMallRun2.proCount >= 100) {
break;
}
//当前买家去阻塞线程,同时释放锁,等待商家的线程通知全体线程后,根据CPU调用线程优先级,来决定谁能够抢到
EMallRun2.queue.wait();
//获取商家商家的商品数量,是否上架
Integer i = EMallRun2.queue.poll();
//上架商品
if (i != null) {
//当前线程抢到货物,
System.out.println(customorName + " - 抢到第" + i + "个商品");
//抢到货后需改变商家的后台余量,总共上架100个,
EMallRun2.proCount ++;
//改变当前线程抢到的货物数量
myBuyedCount ++;
}
}
}
//返回一个结果,表示当前线程执行完毕
return true;
}
public String getCustomorName() {
return customorName;
}
public int getMyBuyedCount() {
return myBuyedCount;
}
}
好了,线程问题到此告一环节,需要掌握知识远不止这一点,代码之路任重道远,我们携手并肩作战!,都看到这里了,给我一个鼓励,一键三连吧~~🤞💕