Java多线程的介绍
Java创建多线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现CallAble
继承Thread类
使用方法
- 定义一个子类MyThread继承Thread类,然后重写run()方法。
- 创建MyThread类对象
- 调用线程对象的start()方法启动线程,线程启动后会执行run方法
Thread代码实现
/**
* 子类继承Thread类
*/
public class MyThread extends Thread{
// 重写Run方法
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 10; i++) {
System.out.println("MyThread执行");
}
}
}
public class ThreadTest01 {
// main是有一条主线程在执行
public static void main(String[] args) {
// 创建MyThread线程类表示一个实例
Thread myThread = new MyThread();
// 启动线程,自动指定Run方法
myThread.start();
// 主线程的方法执行
for (int i = 0; i < 10; i++) {
System.out.println("主线程输出");
}
}
}
继承Thread的优缺点
优点:实现简单,编码方便。
缺点:线程类继承了Thread类,就无法继承其他的类,不利于功能的扩展。
注意事项
- 启动线程一定是调用start方法,不能是调用run方法,要不然只是系统只是把它当做方法调用,不会当做一个独立的线程,只用调用start方法才是启动一个新的线程
- 不要把主线程任务放到子线程启动之前,因为这样主线程一直是先跑完的,相当于还是一个单线程的效果。
实现Runnable
使用方法
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable任务对象
- 把MyRunnable任务对象交给Thread处理。(因为Thread有一个有构造器传入的参数就是Runnable)
- 调用线程对象的start()方法启动线程
Runnable代码实现
public class MyRunnable implements Runnable{
@Override
public void run() {
// 线程的任务
for (int i = 0; i < 5; i++) {
System.out.println("子线程");
}
}
}
public class ThreadTest02 {
public static void main(String[] args) {
// 创建任务对象
Runnable myRunnable = new MyRunnable();
// 把任务对象交给线程对象处理
new Thread(myRunnable).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程");
}
}
}
实现Runnable的优缺点
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。
Runnable代码实现其他方式
public class ThreadTest02_1 {
public static void main(String[] args) {
// 直接创建Runnable接口的匿名内部类
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1");
}
}
};
new Thread(target).start();
// 简化形式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程2");
}
}
}).start();
// 简化形式
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("子线程3");
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程");
}
}
}
前两种方式存在的问题,如果我需要线程执行完毕之后,获取线程的返回的结果该如何处理???
利用Callable接口、FutureTask类来实现
使用方法
- 创建任务对象
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
- 把Callable类型的对象封装成FutureTask(线程任务对象)。
- 把线程任务对象交给Thread对象。
- 调用Thread对象的start方法启动线程。
- 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。
Callable代码实现
import java.util.concurrent.Callable;
/**
* <String>是执行结束之后要返回的结果
*/
public 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 = 0; i <= n; i++) {
sum += i;
}
return "线程求出1-" + n + "的和是:" + sum;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程任务对象
MyCallable result = new MyCallable(100);
// 把result对象封装成FutureTask的对象
FutureTask<String> f1 = new FutureTask<>(result);
new Thread(f1).start();
String s = f1.get();
System.out.println(s);
}
}
实现Callable的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
缺点:编码复杂一点。
Java创建多线程方式总结
Java线程常用的一些方法
线程安全问题
什么是线程安全问题?
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
举个例子
你和你女朋友在银行有一个共同的账户,余额是10万元,如果你和你女朋友同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
你去银行柜台取钱10万,银行判断你的账户余额足够10万,然后正准备给你取钱的时候,你女朋友在ATM同时在取钱10万,正好她在取钱的时候也判断余额足够,所以你们都取10万,银行亏了10万。
出现线程安全的原因
- 在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
程序模拟线程安全的问题
/**
* 账户类
*/
public class Account {
// 账户余额
private double money;
// 账户id
private String cardId;
public Account() {
}
public Account(double money, String cardId) {
this.money = money;
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public void drawMoney(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 + "来取钱但是钱不够");
}
}
}
/**
* 取钱线程
*/
public class DrawThread extends Thread{
private Account account;
public DrawThread(Account account, String name){
super(name);
this.account = account;
}
// 线程任务就是去取钱
@Override
public void run() {
// 取钱方法
account.drawMoney(100000);
}
}
// 取钱操作
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
// 创建一个账户对象
Account account = new Account(100000, "XL-111");
// 创建两个线程
new DrawThread(account,"小李").start();// 小李
new DrawThread(account,"小王").start(); // 小王
}
}
结果展示
如何解决线程安全问题
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题
解决线程安全三种实现方法
同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步代码块注意事项
- 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
public class Account {
private double money;
private String cardId;
public Account() {
}
public Account(double money, String cardId) {
this.money = money;
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public void drawMoney(double money) {
// 是谁来取钱
String name = Thread.currentThread().getName();
// 判断余额
// 利用同步代码块
// 这个this代表他们自己的账户account
// 如果使用字符串来锁,那范围太大了
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作为的锁对象。
实现代码
/**
* 账户类
*/
public class Account {
// 账户余额
private double money;
// 账户id
private String cardId;
public Account() {
}
public Account(double money, String cardId) {
this.money = money;
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public synchronized void drawMoney(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 + "来取钱但是钱不够");
}
}
}
其他代码与代码块锁相同
是同步代码块好还是同步方法好一点?
范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
可读性:同步方法更好。
Lock锁
- Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
代码实现
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private double money;
private String cardId;
// 创建一个锁对象
private final Lock lk = new ReentrantLock();
public Account() {
}
public Account(double money, String cardId) {
this.money = money;
this.cardId = cardId;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public void drawMoney(double money) {
// 是谁来取钱
String name = Thread.currentThread().getName();
// 判断余额
// 利用同步代码块
// 这个this代表他们自己的账户account
// 如果使用字符串来锁,那范围太大了
synchronized (this) {
// 获取锁
lk.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 + "来取钱但是钱不够");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 不管中间出现什么异常,一定要释放锁
lk.unlock();
}
}
}
}
Java线程池
什么是线程池
线程池就是一个可以复用线程的技术
不使用线程池的问题
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
如何创建线程池
JDK 5.0起提供了代表线程池的接口:ExecutorService。
如何得到线程池对象?
- 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
- 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
ThreadPoolExecutor构造器
/*int corePoolSize, // 核心线程数量
int maximumPoolSize, // 最大线程数量
long keepAliveTime,// 临时线程的最大存货时间
TimeUnit unit,// 存货时间的单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 谁来创这些线程
RejectedExecutionHandler handler*/ // 线程使用完了,任务也占据满了,新来的任务怎么处理
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
线程池代码示例
线程池处理Runnable任务
public class MyRunnable implements Runnable{
@Override
public void run() {
// 哪个线程做什么任务
System.out.println(Thread.currentThread().getName()+"正在输出");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
import java.util.concurrent.*;
public class ThreadPoolTest1 {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
// 使用线程池处理Runnable任务
MyRunnable target = new MyRunnable();
// 线程池执行
pool.execute(target); // 线程池创建一个线程然后执行这个任务
pool.execute(target); // 线程池创建一个线程然后执行这个任务
pool.execute(target); // 线程池创建一个线程然后执行这个任务
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
// 此时核心线程3个满了,任务队列4个也满了,然后就会创建临时线程
pool.execute(target);
pool.execute(target);
//此时核心线程3个满了,任务队列4个也满了,2个临时线程也满了,再来就拒绝
pool.execute(target);
// 等着线程池的任务全部执行完毕之后然后再关闭线程池
// pool.shutdown();
// 立即关闭线程池,不管任务有没有执行完毕
// pool.shutdownNow();
}
}
线程池处理Callable任务
/**
* <String>是执行结束之后要返回的结果
*/
public 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 = 0; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName()+"线程计算出了1-" + n + "的和是:" + sum;
}
}
import java.util.concurrent.*;
public class ThreadPoolTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*public ThreadPoolExecutor(
int corePoolSize, // 核心线程数量
int maximumPoolSize, // 最大线程数量
long keepAliveTime,// 临时线程的最大存货时间
TimeUnit unit,// 存货时间的单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 谁来创这些线程
RejectedExecutionHandler handler)*/ // 线程使用完了,任务也占据满了,新来的任务怎么处理
// 创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
// 返回未来任务对象
Future<String> future1 = pool.submit(new MyCallable(100));
Future<String> future2 = pool.submit(new MyCallable(100));
Future<String> future3 = pool.submit(new MyCallable(100));
Future<String> future4 = pool.submit(new MyCallable(100));
// 获取返回的结果
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());
System.out.println(future4.get());
}
}