多线程
- 1.线程创建的方法
- 1.1.方法一 继承Thread类
- 1.2.方法二 实现Runnable接口
- 1.3.方法三 实现Callable接口
- 2.线程安全
- 2.0.线程不安全的案例
- 2.1.方式一:同步代码块
- 2.2.方式二:同步方法
- 2.3.方式三:Lock锁
- 3.线程池
- 3.1.创建线程池
- 3.2.线程池处理Runnable任务
- 3.3.线程池处理Callable任务
- 4.并发和并行
1.线程创建的方法
1.1.方法一 继承Thread类
继承Thread类
- 1.定义一个子类MyThread继承线程类
java.lang.Thread
,重写run()方法 - 2.创建MyThread类的对象
- 3.调用线程对象的start()方法启动线程
优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
public class MyThread extends Thread{@Overridepublic void run() {// 线程的执行任务for (int i = 1; i <= 5; i++) {System.out.println("子线程MyThread -> " + i);}}
}
public class ThreadTest1 {public static void main(String[] args) {// 创建MyThread线程类的对象 代表一个线程Thread thread = new MyThread();// 启动线程(自动执行run方法)thread.start();for (int i = 1; i <= 5; i++) {System.out.println("主线程MainThread -> " + i);}}
}
1.2.方法二 实现Runnable接口
实现Runnable接口
- 1.定义线程任务类MyRunnable实现Runnable接口,重写run()方法
- 2.创建MyRunnable任务对象
- 3.把MyRunnable任务对象交给Thread处理
- 4.调用线程的start()方法启动线程
优缺点:
- 优点:任务类只是实现接口,可以继续继承其他类/实现其他接口,拓展性强
- 缺点:需要多一个Runnable对象
public class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程MyRunnable-> " + i);}}
}
public class ThreadTest2 {public static void main(String[] args) {// 创建任务对象Runnable target = new MyRunnable();// 把 任务对象 交给 线程对象 处理new Thread(target).start();for (int i = 1; i <= 5; i++) {System.out.println("主线程Main -> " + i);}}
}
匿名内部类的写法
- 1.创建Runnable的匿名内部类对象
- 2.再交给Thread线程对象
- 3.再调用线程对象的start()启动线程
public class ThreadTest2_2 {public static void main(String[] args) {// 1.创建匿名内部类Runnable target = new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程 -> " + i);}}};// 2.再交给Thread线程对象Thread thread = new Thread(target);// 3.启动线程thread.start();for (int i = 1; i <= 5; i++) {System.out.println("主线程Main -> " + i);}}
}
1.3.方法三 实现Callable接口
实现Callable接口
- 1.创建任务对象
- 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
- 把Callable类型的对象封装成FutureTask(线程任务对象)
- 2.把线程任务对象交给Thread对象
- 3.调用线程对象的start()启动线程
- 4.线程执行完毕后,通过FutureTask对象的get方法去获取线程任务的执行结果
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后获取线程执行的结果
- 缺点:编码复杂
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}@Overridepublic String call() throws Exception {int sum = 0;// 假设 求1~n的和 并返回for (int i = 1; i <= n; i++) {sum += i;}return "线程求出1~" + n + "的和:" + sum;}
}public class ThreadTest3 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建Runnable对象Callable<String> callable = new MyCallable(100);// 封装成FutureTaskFutureTask<String> futureTask = new FutureTask<>(callable);// 交给Thread对象Thread thread = new Thread(futureTask);// 启动线程thread.start();// 获取线程执行完毕后返回的结果String result = futureTask.get();System.out.println("result = " + result);}
}
2.线程安全
2.0.线程不安全的案例
案例:模拟线程安全问题。小红和小明同时取钱(同一个账户)
public class ThreadTest {public static void main(String[] args) {Account account = new Account("ICBC-100", 100000);new DrawThread(account, "小明").start(); // 小明new DrawThread(account, "小红").start(); // 小红}
}
public class DrawThread extends Thread{private final Account account;public DrawThread(Account account, String name) {super(name);this.account = account;}@Overridepublic void run() {account.drawMoney(100000);}
}
public class Account {private String cardId;private double money; // 余额public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}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 String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}
}
2.1.方式一:同步代码块
**作用:**把访问共享资源的核心代码给上锁,一次保证线程安全
synchronized (同步锁) {访问共享资源的核心代码
}
注意事项
- 对于当前同时执行的线程来说,同步锁必须是同意把(同一个对象),否则会出bug
在案例中添加同步代码块 (实例方法使用this
作为锁对象)
public void drawMoney(double money) {String name = Thread.currentThread().getName();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 + "来取钱,余额不足");}}
}
如果要求在静态方法中保证线程安全,同步锁应该是 类名.class
public static void test() {synchronized (类名.class) {// 核心代码}
}
2.2.方式二:同步方法
作用:把访问共享资源的核心方法给上锁,以此保证线程安全
原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进入执行
修饰符 synchronized 返回值类型 方法名称 (形参列表) {操作共享资源的代码
}
在案例中添加同步方法
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 + "来取钱,余额不足");}
}
底层原理
- 底层也是与隐式锁对象,锁的范围是整个方法代码
- 如果方法是实例方法:同步方法默认用
this
作为锁的对象 - 如果方法是静态方法:同步方法默认用
类名.class
作为锁的对象
2.3.方式三:Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活/方便/强大
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象
public class Account {// 创建一个锁对象private final Lock lock = new ReentrantLock();//略public void drawMoney(double money) {String name = Thread.currentThread().getName();try {lock.lock(); //加锁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 {lock.unlock(); // 解锁}}
}
注意事项
- 1.用
final
修饰 - 2.用
try...catch...finally
包裹住加锁与解锁操作,保证即使出现异常,也可确保能解锁
3.线程池
线程池就是一个复用线程的技术
不断创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来
3.1.创建线程池
线程池接口:ExecutorService
**方式一:**使用 ExecutorService
的实现类 ThreadPoolExecutor
创建一个线程池对象
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
- 参数一:
corePoolSize
指定线程池的核心线程数量 - 参数二:
maximumPoolSize
线程池的最大线程数量(maximumPoolSize > corePoolSize) - 参数三:
keepAliveTime
临时线程的存活时间 - 参数四:
unit
指定临时线程存活的时间单位(秒/时/分/天) - 参数五:
workQueue
指定线程池的任务队列 - 参数六:
ThreadFactory
指定线程池的线程工厂 - 参数七:
RejectedExecutionHandler
指定线程池的任务拒绝策略(线程都在忙,任务队列也满了,新任务来了该怎么处理)
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
策略 (参数七) | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy() | 丢弃任务并抛出RejectedExecutionException异常。默认策略 |
ThreadPoolExecutor.DiscardPolicy() | 丢弃任务,但是不抛出异常。(不推荐做法) |
ThreadPoolExecutor.DiscardOldestPolicy() | 抛弃队列等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy() | 有主线程负责调用任务的run()方法从二绕过线程池直接执行 (老板亲自执行任务) |
注意事项
- 临时线程什么时候创建
- 新任务提交时发现核心线程在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
- 什么时候开始拒绝新任务
- 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
**方式二:**使用 Excutors
(线程池的工具类) 调用方法返回不同特点的线程池对象
方法名称 | 说明 |
---|---|
ExcutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池 |
ExcutorService newSingleTHreadExecutor() | 创建只有一个线程的线程池 |
ExcutorService newCachedThreadPool() | 线程数量随着任务增加而增加 |
ExcutorService newScheduledThreadPool(int corePoolSize) |
ExecutorService pool = Executors.newFixedThreadPool(3);
ExecutorService pool = Executors.newSingleThreadExecutor();
核心线程数量到底配置多少
计算密集型的任务:核心线程数量 = CPU的核数 + 1
IO密集型的任务:核心线程数量 = CPU的核数 * 2
3.2.线程池处理Runnable任务
public static void main(String[] args) {// 1.通过ThreadPoolExecutor创建一个线程池对象ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());Runnable target = new MyRunnable();// 线程池会自动创建新线程,自动处理这个任务,自动执行pool.execute(target); // 第一个线程pool.execute(target); // 第二个线程pool.execute(target); // 第三个线程pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target);// 临时线程的创建时机pool.execute(target);pool.execute(target);// 到了新任务拒绝的时机pool.execute(target);// 线程池不会主动关闭,程序会一直运行pool.shutdown(); // 等待线程池任务全部完成后,再关闭线程池//pool.shutdownNow();// 立即关闭线程,不管是否还有任务在执行
}
3.3.线程池处理Callable任务
public class ThreadPoolTest2 {public static void main(String[] args) throws Exception{// 1.通过ThreadPoolExecutor创建一个线程池对象ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());// 2.使用线程处理Callable任务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));System.out.println("f1.get() = " + f1.get());System.out.println("f2.get() = " + f2.get());System.out.println("f3.get() = " + f3.get());System.out.println("f4.get() = " + f4.get());// 线程池不会主动关闭,程序会一直运行pool.shutdown(); // 等待线程池任务全部完成后,再关闭线程池//pool.shutdownNow();// 立即关闭线程,不管是否还有任务在执行}
}
4.并发和并行
**进程:**正常运行的程序/软件就是一个独立的进程
**线程:**线程是属于进程的,一个进程中可以同时运行很多个线程
进程中的多个线程是并发和并行执行的
并发:
- 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能执行,CPU会轮询为系统的每个线程服务。由于CPU切换的速度很快,给我们感觉就是线程都在同时执行,这就是并发
并行:
- 在同一个时刻上,同时有多个线程被CPU调度执行
多线程是怎么执行的
- 并发和并行同时执行