多线程
1.认识线程
什么是线程:线程是一个程序内部的一条执行流程,是操作系统能调度的最小单位,是进程的实际运作单位,一个进程可以包含多个线程
多线程:从软硬件上实现的多条执行流程的技术(多条线程由CPU复杂调度执行)
- 多线程示例:
2.创建线程
Thread
类
1.继承Thread类
- 实现一个类继承Thread类
- 重写Thread类的
run()
·方法 - 创建该线程类的对象
- 调用对象的
start()
方法启动一个线程
package com.itheima.demo1create;
public class test {
public static void main(String[] args) {
//3.创建线程类的对象:代表线程
Thread myThread = new MyThread();
//4.调用start()方法启动线程:还是调用run方法执行的
myThread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程输出:" + i);
}
}
}
//1,定义一个子类继承Thread类
class MyThread extends Thread{
//2.重写run方法,在run方法中编写线程的任务代码
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("子线程输出:" + i);
}
}
}
- 优点:编码简单
- 缺点:继承了Thread类,由于单继承规则,无法再继承其他类
- 注意事项:
- 直接调用run方法会当成普通方法执行,此时还是相当于单线程执行
- 只有调用start方法才是启动一个新的线程执行
2.实现Runnable接口
- 定义一个线程任务类
MyRunnable
实现Runnable
接口,重写run()
方法 - 创建
MyRunnable
对象 - 把
MyRunnable
任务对象交给Thread
线程对象处理 - 调用线程对象的
start()
方法启动线程
package com.itheima.demo1create;
public class test1 {
public static void main(String[] args) {
//2.创建MyRunnable线程任务类对象
MyRunnable r = new MyRunnable();
//3.将线程任务类对象交给Thread线程对象处理
Thread t1 = new Thread(r);
//4.调用线程对象的start方法启动线程
t1.start();
//使用Runnable接口的匿名内部类来创建
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程2输出:" + i);
}
}
};
Thread t2 = new Thread(r2);
//简化写法1:
new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程3输出:" + i);
}
}
}).start();
//简化写法2:
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程4输出:" + i);
}
}).start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程输出:" + i);
}
}
}
//1.定义MyRunnable线程任务类,实现Runnable接口,重写run方法
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程1输出:" + i);
};
}
}
- 优点:可扩展性强,实现该接口的同时还可以继承其他的类
- 缺点:编程相对复杂,无法返回线程执行的结果
3.实现Callable
接口,再用FutureTask
进行包装
- 1.实现
Callable
接口,并重写call
方法,在call方法中执行返回结果的逻辑 - 2.创建
Callable
接口实现类的对象, - 3.把Callable类型对象用
FutureTask
对象封装 - 4.将
FutureTask
线程任务对象交给Thread
线程对象 - 5.调用线程对象的
start()
方法启动线程 - 6.调用
FutureTask
对象的get()
方法获取线程执行的返回结果
package com.itheima.demo1create;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class test2 {
public static void main(String[] args) {
//2.创建Callable接口实现类对象
Callable<String> mc1 = new MyCallable(50);
//3.把Callable类型的对象封装成FutureTask(线程任务对象)
FutureTask<String> ft1 = new FutureTask<>(mc1);
//4.将线程任务对象封装成线程对象
Thread t1 = new Thread(ft1);
//5.调用线程对象的start方法启动线程
t1.start();
Callable<String> mc2 = new MyCallable(100);
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.start();
try {
//如果主线程发现第一个线程还没有执行完毕,会让出CPU,等一个线程执行完毕后,才会往下执行
System.out.println(ft1.get());
} catch (Exception e) {
e.printStackTrace();
}
try {
//6.调用线程任务对象的get方法获取线程任务执行的结果
System.out.println(ft2.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.创建Callable接口的实现类,重写call方法,方法体写结果的处理方法,返回线程执行完毕需要返回的结果
class MyCallable implements Callable<String> {
//创建成员变量
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "从1-" + n + "的结果是" + sum;
}
}
- 优点:可扩展性强,并且可以得到线程执行的结果
- 缺点:编程相对复杂
三种创建方式的比较
3.线程的常用方法
- 线程名字获取与设置:
getName()
和setName
public class test {
public static void main(String[] args) {
Thread myThread = new MyThread("线程0");
//调用Thread线程对象的getName方法获取线程名字
System.out.println(myThread.getName());
//调用线程对象的setName,给指定线程设置名字
myThread.setName("线程1");
System.out.println(myThread.getName());
myThread.start();
//哪个线程调用这个代码,这个代码就拿到哪个线程
Thread m = Thread.currentThread();
System.out.println(m.getName());
for (int i = 0; i < 10; i++) {
System.out.println("主线程输出:" + i);
}
}
}
//1,定义一个子类继承Thread类
class MyThread extends Thread{
//利用有参构造器给线程起名
public MyThread(String name){
super(name);
}
//2.重写run方法,在run方法中编写线程的任务代码
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println("子线程输出:" + i);
}
}
}
-
线程睡眠:
sleep()
Thread.sleep(1000); 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行 -
线程插队:
join()
让调用的线程先执行完
4.线程安全
线程安全问题出现的原因:多个线程同时对一个共享的资源进行修改操作
1.案例模拟
两个人同时对一个账号进行取钱操作,使得账户余额变成负值
package com.itheima.threadsecuritydemo;
public class threadsecurity {
public static void main(String[] args) {
Account acc = new Account(123456, 100000.0);
//小明和小红同时对一个共同账户对象操作
Thread xm = new drawMoney("小明", acc);
Thread xh = new drawMoney("小红", acc);
//启动线程,执行取钱操作
xm.start();
xh.start();
}
}
class drawMoney extends Thread{
//将账户对象传给线程
private Account acc;
public drawMoney(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run(){
//取钱
acc.draw(100000);
}
}
class Account {
private int cardId;
private double money;
public Account(int cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public void draw(double money) {
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "来取钱,吐出" + money + "!");
this.money -= money;
System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
} else {
System.out.println(name + "取钱失败,余额不足!");
}
}
}
2.线程同步
线程同步是线程安全问题的解决方案*
思想
- 让多个线程先后依次访问共享资源,从而避免线程安全问题
常见方案 - 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能继续加锁访问
1) 同步代码块
- 把访问共享资源的核心代码块给上锁,以此保证线程安全
- synchronized(同步锁) {
访问共享资源的核心代码;
} - 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁
package com.itheima.synchronized_code;
public class threadsecurity {
public static void main(String[] args) {
Account acc = new Account(123456, 100000.0);
//小明和小红同时对一个共同账户对象操作
Thread xm = new drawMoney("小明", acc);
Thread xh = new drawMoney("小红", acc);
//启动线程,执行取钱操作
xm.start();
xh.start();
}
}
class drawMoney extends Thread{
//将账户对象传给线程
private Account acc;
public drawMoney(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run(){
//取钱
acc.draw(100000);
}
}
class Account {
private int cardId;
private double money;
public Account(int cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public void draw(double money) {
String name = Thread.currentThread().getName();
//synchronized ("dlei") { //使用唯一对象作为锁对象
synchronized (this) { //一般使用类对象作为锁对象
if (this.money >= money) {
System.out.println(name + "来取钱,吐出" + money + "!");
this.money -= money;
System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
} else {
System.out.println(name + "余额不足,取钱失败!");
}
}
}
}
扩展
- 锁对象一般不选择唯一的对象,会影响其他无关线程的执行
- 建议使用共享资源作为锁对象,对于实例方法使用this作为锁对象
- 静态方法使用字节码(类名.class)作为锁对象:静态方法只有一份,是class文件所有的
2) 同步方法
- 把访问共享资源的核心方法给上锁,以此保证线程安全
- 修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
} - 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁
package com.itheima.synchronized_code2;
public class threadsecurity {
public static void main(String[] args) {
Account acc = new Account(123456, 100000.0);
//小明和小红同时对一个共同账户对象操作
Thread xm = new drawMoney("小明", acc);
Thread xh = new drawMoney("小红", acc);
//启动线程,执行取钱操作
xm.start();
xh.start();
}
}
class drawMoney extends Thread{
//将账户对象传给线程
private Account acc;
public drawMoney(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run(){
//取钱
acc.draw(100000);
}
}
class Account {
private int cardId;
private double money;
public Account(int cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public synchronized void draw(double money) {
if (this.money >= money) {
System.out.println(Thread.currentThread().getName() + "来取钱,吐出" + money + "!");
this.money -= money;
System.out.println(Thread.currentThread().getName() + "取钱成功,还剩" + this.money + "余额!");
} else {
System.out.println("余额不足,取钱失败!");
}
}
}
对比
同步代码块和同步方法:
举例:
锁整个方法,所有线程均要在方法外部排队,等加锁线程执行完在进入方法内部执行,
但是锁核心代码块,所有线程均可以先将处理核心共享资源之前的操作先执行完,等加锁线程执行完解锁之后立马能执行处理核心共享资源操作,性能会好一些。
3) lock锁
Lock锁是JDK5开始提供的一个新的锁定操作。
Lock
是接口,不能直接实例化,可以采用它的实现了ReentrantLock
来构建Lock
锁对象
Lock lock = new ReentrantLock()
package com.itheima.synchronized_code2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class threadsecurity {
public static void main(String[] args) {
Account acc = new Account(123456, 100000.0);
//小明和小红同时对一个共同账户对象操作
Thread xm = new drawMoney("小明", acc);
Thread xh = new drawMoney("小红", acc);
//启动线程,执行取钱操作
xm.start();
xh.start();
}
}
class drawMoney extends Thread{
//将账户对象传给线程
private Account acc;
public drawMoney(String name, Account acc) {
super(name);
this.acc = acc;
}
@Override
public void run(){
//取钱
acc.draw(100000);
}
}
class Account {
private int cardId;
private double money;
private final Lock lock = new ReentrantLock(); //加final修饰防止锁被篡改
public Account(int cardId, double money) {
this.cardId = cardId;
this.money = money;
}
public void draw(double money) {
String name = Thread.currentThread().getName();
lock.lock(); //上锁
try {
if (this.money >= money) {
System.out.println(name + "来取钱,吐出" + money + "!");
this.money -= money;
System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
} else {
System.out.println(name + "余额不足,取钱失败!");
}
} finally {
lock.unlock(); //解锁
}
}
}
5.线程池
1.认识线程池
线程池是一个可以复用线程的技术
不使用线程池会存在的问题:
- 用户每发起一个请求就需要创建一个新线程来处理,创建新线程开销较大
- 请求过多的话,会有多个线程同时执行的话,系统负载压力过大,严重影响系统性能
- 形象举例:吃一顿饭扔一个碗,下一顿就需要花钱买新的碗
线程池工作原理:
2.创建线程池
- 线程池接口:
ExecutorService
方式1:通过ThreadPoolExecutor
创建线程池
1.线程池创建:
package com.itheima.executorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//目标: 创建线程对象
//1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数量
5, //最大线程数量
10, //临时线程(最大线程数量-核心线程数量)的存活时间
TimeUnit.SECONDS, //指定临时线程存货的时间单位 SECONDS : 秒
new ArrayBlockingQueue<>(3), //任务队列: 最大排队的任务数量
Executors.defaultThreadFactory(), //创建线程池的线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
);
//2.使用线程池处理任务: 复用线程
Runnable target = new MyRunnable();
pool.execute(target); //提交第1个任务 创建第1个线程, 自动启动线程处理这个任务
pool.execute(target); //提交第2个任务 创建第2个线程, 自动启动线程处理这个任务
pool.execute(target); //提交第3个任务 创建第3个线程, 自动启动线程处理这个任务
//只要调用多少次execute就会执行几个任务,即使提交的任务对象是同一个
//由于之前线程执行较快,执行完之后会复用核心线程去执行任务
pool.execute(target); //复用线程
pool.execute(target); //复用线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
2.处理Runnable任务
临时线程的创建时机:
- 新任务提交时,发现核心线程都在忙,并且任务队列也满了,这时候会创建临时线程来缓解压力。
拒绝新任务的时机:
- 核心线程和临时线程都被占用,并且任务队列也满了,新的任务再过来就会开始拒绝新任务。
package com.itheima.executorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {
public static void main(String[] args) {
//目标: 创建线程对象
//1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数量
5, //最大线程数量
10, //临时线程(最大线程数量-核心线程数量)的存活时间
TimeUnit.SECONDS, //指定临时线程存货的时间单位 SECONDS : 秒
new ArrayBlockingQueue<>(3), //任务队列: 最大排队的任务数量
Executors.defaultThreadFactory(), //创建线程池的线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
);
//2.使用线程池处理任务
Runnable target = new MyRunnable();
pool.execute(target); //提交第1个任务 创建第1个线程, 自动启动线程处理这个任务
pool.execute(target); //提交第2个任务 创建第2个线程, 自动启动线程处理这个任务
pool.execute(target); //提交第3个任务 创建第3个线程, 自动启动线程处理这个任务
//只要调用多少次execute就会执行几个任务,即使提交的任务对象是同一个
pool.execute(target);
pool.execute(target);
pool.execute(target);
//核心线程都在忙,并且已经达到任务队列的最大等待数量,此时会创建临时线程进行新任务处理
pool.execute(target);
pool.execute(target); //仍然可以继续创建临时线程进行处理
//此时核心线程与临时线程均被占用,并且任务队列已经排满,开始执行任务拒绝策略:new ThreadPoolExecutor.AbortPolicy(),打印出拒绝日志
pool.execute(target);
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 0) {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
ThreadPoolExecutor.CallerRunsPolicy()
:老板亲自服务
当核心线程和临时线程都被占用,并且任务队列满了,有新任务来时,主线程亲自执行。
3.处理Callable任务
- 使用
ExecutorService
方法:Future<T> submit(Callable<T> command)
package com.itheima.executorService;
import java.util.concurrent.*;
public class ExecutorServiceDemo2 {
public static void main(String[] args) {
//目标: 创建线程对象
//1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数量
5, //最大线程数量
10, //临时线程(最大线程数量-核心线程数量)的存活时间
TimeUnit.SECONDS, //指定临时线程存货的时间单位 SECONDS : 秒
new ArrayBlockingQueue<>(3), //任务队列: 最大排队的任务数量
Executors.defaultThreadFactory(), //创建线程池的线程工厂
new ThreadPoolExecutor.AbortPolicy() //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
);
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.创建Callable接口的实现类,重写call方法,方法体写结果的处理方法,返回线程执行完毕需要返回的结果
class MyCallable implements Callable<String> {
//创建成员变量
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "从1-" + n + "的结果是" + sum;
}
}
方式2.通过Executors
创建线程池
package com.itheima.executorService;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class executors_demo {
public static void main(String[] args) {
//目标: 创建线程对象
//1. 使用Executors的静态方法创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
//2.使用线程池处理任务:
Future<String> f1 = pool.submit(new MyCallable2(100));
Future<String> f2 = pool.submit(new MyCallable2(200));
Future<String> f3 = pool.submit(new MyCallable2(300));
Future<String> f4 = pool.submit(new MyCallable2(400));
try {
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable2 implements Callable<String> {
private int n;
public MyCallable2(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "从1-" + n + "的结果是" + sum;
}
}
3.扩充
线程池的核心线程数量和最大线程数量一般如何配置:
- CUP密集型任务(计算密集型):
线程池大小通常设置为与系统可用处理器数量相等或者多一点: 线程处理数为16,那么则设置为16+1 - IO密集型任务:
线程等待IO操作时间较久,应设置更多线程来充分利用CPU等待的时间。
最大线程数量 = 核心线程数量 * 2 //或者更高
6.并发和并行
进程:一个正在运行的程序或者软件就是一个独立的进程
- 一个进程可以同时运行很多个进程
并发:进程中的线程是由CPU进行调度的,但CPU同时处理线程的能力有限,为了保证全部线程对能往前执行,CPU会轮询为系统的每个线程服务。由于CPU在线程之间的切换速度很快,让我们感觉这些线程在同时执行,这就是并发。
并行: 多个线程在同时执行。比如CPU的逻辑处理器(线程处理数量)是16,那么CPU同时可以操作16个线程,这16个线程在CPU统一执行的时候就称为并行。
多线程是并发和并行同时进行的
7.案例
100个员工抢200个红包雨
package com.itheima.executorService;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class redPacket {
public static void main(String[] args) {
List<Integer> redPacket = getRedPacket();
for (int i = 1; i <= 100; i++) {
new GetRedPacket(redPacket, i + "").start();
}
}
//准备两百个红包
public static List<Integer> getRedPacket() {
Random random = new Random();
List<Integer> redPacket = new ArrayList<Integer>();
for (int i = 1; i <= 160; i++) {
redPacket.add(random.nextInt(30) + 1);
}
for (int i = 1; i <= 40; i++) {
redPacket.add(random.nextInt(70) + 31);
}
return redPacket;
}
}
class GetRedPacket extends Thread {
private List<Integer> redPacket;
public GetRedPacket(List<Integer> redPacket, String name) {
super(name);
this.redPacket = redPacket;
}
@Override
public void run() {
while (true) {
//拿到锁的人进去抢红包
synchronized (redPacket) {
if (redPacket.size() == 0) {
break; //没有红包了,直接跳出
}
String name = Thread.currentThread().getName();
int index = (int) (Math.random() * redPacket.size());
Integer money = redPacket.remove(index);
System.out.println(Thread.currentThread().getName() + "号员工抢到了: " + money);
//如果是最后一个红包,那么活动结束
if (redPacket.size() == 0) {
System.out.println("红包抢完了,活动结束!");
break;
}
}
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}