多线程是什么
- 多线程是指从软硬件上实现多条执行流程的技术
- 多线程:
- 继承Thread类
- 实现Runnable接口
- 不要把主线程任务放在子线程之前,要不然就是单一线程
方式一:
- 继承Thread类; 重写run方法; 创建线程对象; 调用start方法启动
- 优缺点:优点编码简单 缺点:存在单继承的局限性,线程类继承Thread后不能继承其他类,不便于扩展。
启动线程调用start();调用run就是普通调用
//线程的开始
public class ThreadTest {
public static void main(String[] args) {
//创建线程对象
Thread ct = new CreateThread();
//调用start 启动线程
ct.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程"+i);
}
}
}
/*
* 多线程的创建方式之一:继承Thread类实现
* */
public class CreateThread extends Thread{
/*
* 重写run方法,里面是定义线程以后要干啥
* */
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程"+i);
}
}
}
多线程的实现方案二:实现Runnable接口
- 定义一个线程任务类MyRunnable实现Runnable接口,实现run()方法
- 创建线程任务对象MyRunnable
- 把MyRunnable任务对象交给Thread处理
- 调用线程对象的start()方法启动线程
Runnable接口:
- 优缺点:
- 优:线程任务类只是实现接口,可以继承类和实现接口,扩展性强。
- 缺:编程多一层对象包装,如果线程执有执行结果是不可以直接返回的
- 第一种和第二种都是没有返回结果的 ```java package com.h.runnable; /*
- 第二种线程创建方式
- */
public class Test {
public static void main(String[] args) {
} } 实现的接口 package com.h.runnable;//创建一个任务对象
Runnable ctr = new CreatThreadRunnable();
//把任务对象交给线程对象
new Thread(ctr).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
/实现Runnable接口/ public class CreatThreadRunnable implements Runnable{ /*
* 重写run方法
*
* */
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程"+i);
}
}
}
实现Runnable(匿名内部类)<br />写法:
```java
public class InnerRunnable {
public static void main(String[] args) {
Runnable target = new Runnable(){
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println("匿名内部类线程"+i);
}
}
};
new Thread(target).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
}
}
简化
public class InnerRunnable {
public static void main(String[] args) {
new Thread(new Runnable(){
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println("匿名内部类线程"+i);
}
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程"+i);
}
}
}
//使用Lambda表达式简化
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("匿名内部类线程"+i);
}
}).start();
方式三:
JDK5.0提供:Callable和FutureTask来实现
- 前两种方式重写run方法均不能直接返回结果
- 不适合需要返回线程执行结果的业务场景
实现方案三:
- 得到任务对象
- 定义类实现Callable接口,重写call方法,封装要做的事情。
- 用FutureTask把Callable对象封装成线程任务对象
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务
线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果
线程返回值需要怎么做:
创建任务对象MyCallable并实现Callable
接口 泛型自己需要什么类型返回申明什么类型 - 重写call方法 并返回你所需要的值
- 到主线程创建Callable任务对象 Callable
call = new MyCallable(100); - 把Callable任务对象交给FutureTask对象
- FutureTask对象实现了RunnableFuture接口
- RunnableFuture继承了Runnable接口
- 所以Thread类构造器中可以放FutureTask对象
- FutureTask可以使用get方法来获取Callable所返回的值
- 把FutureTask交给Thread
- Thread类调用start方法启动线程
- FutureTask对象调用get()方法获取call()方法返回的值
- get方法返回两个结果一个是正常结果,另一个是异常结果所以得使用try{}catch(){}
- get()方法有一个监测就是看call方法是否跑完没有如果没有跑完,代码执行到调用get()方法的时候就会等待,等待跑完后才会提取结果
FutureTask的API:
public FutureTask<>(Callable call);把Callable对象封装成FutureTask对象;
public V get() throws Exception; 获取线程执行call方法返回的结果。如果线程没有执行完,就会等待
优点:
- 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
- 可以在线程执行完毕后去获取线程执行结果
- 缺点
- 编码复杂
代码:
package com.h.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
* 学会线程创建方式三、实现Callable接口,结合FutureTask完成
* */
public class CallableTest {
public static void main(String[] args) {
//创建Callable任务对象
Callable<String> mc = new MyCallable(100);
//把Callable任务对象,封装给FutureTask对象
//为什么要交给FutureTask对象因为
// 作用1、FutureTask是Runnable对象(实现了Runnable接口),可以交给Thread了
// 作用2、可以在线程执行完毕之后通过调用其get方法得到线程执行完完成的结果
FutureTask<String> ft = new FutureTask<>(mc);
Thread th = new Thread(ft);
th.start();
try{
String str = ft.get();
System.out.println("第一个结果"+str);
}
}
}
/*
* 1、定义一个任务类,实现Callable类 应该申明线程任务执行完毕后的结果的数据类型
* */
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
/*重写call方法 线程任务方法*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum+=i;
}
return "子线程的结果是" +sum;
}
}
Thread常见方法
给线程设置名字
- 使用Thread.setName();
- 使用构造器来给线程设置名字
获取线程名字
- 使用Thread.getName();
获取当前线程的方法:
谁调用的,谁就是当前线程
- Thread.currentThread();
使用Runnable的时候进行设置名称
- new Thread(Runnable runnable,String name);
线程休眠:
- Thread.sleep(long time);当前线程休眠指定的时间后再继续执行,单位为毫秒 ```java
public class MyThreadTest { public static void main(String[] args) { Thread t1 =new MyThread(“构造器1号”); //给线程取名字 t1.setName(“1号”); t1.start();
//拿到当前线程对象
Thread m = Thread.currentThread();
m.setName("主线程");
for (int i = 0; i < 5; i++) {
System.out.println(m.getName()+""+i);
}
}
}
public class MyThread extends Thread{ //使用构造器给线程设置名字 public MyThread(String name) { super(name); }
@Override
public void run() {
for (int i = 0; i < 5; i++) {
//获取线程名字
System.out.println(Thread.currentThread().getName()+"子线程");
}
}
}
<a name="UoKwb"></a>
#### 线程安全问题:
- **多个线程同时操作同一个共享资源**的时候可能会出现业务安全问题,称为线程安全问题
- 同步代码块
代码:
```java
package com.h.h_account;
public class Account {
private String cardId;
private double money;
public Account() {
}
public Account(double money) {
this.money = money;
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
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;
}
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("当前剩余余额:"+this.money);
}else {
System.out.println("余额不足");
}
}
}
package com.h.h_account;
/*
* 取钱的线程
* */
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc,String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
//同时取钱
acc.drawMoney(100);
}
}
package com.h.h_account;
public class AccountTest {
public static void main(String[] args) {
/*小明与小红共同使用一个账户*/
Account ac = new Account(100);
Thread t1 = new DrawThread(ac ,"小明");
Thread t2 = new DrawThread(ac,"小红");
t1.start();
t2.start();
}
}
同步锁解决问题
//在这加锁的 然后当两个线程到这的时候只能有一个线程能抢到,抢到以后另一个线程就等待
synchronized ("hh") {
if (this.money >= money) {
//取钱
System.out.println(name + "取钱:" + money);
this.money -= money;
System.out.println("当前剩余余额:" + this.money);
} else {
System.out.println("余额不足");
}
}
解决上面线程安全问题:线程同步
线程同步核心思想
- 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来
- 作用:把出现线程安全问题的核心代码给上锁
- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
synchronized(同步锁对象){
操作共享资源的代码(核心代码)
}
锁对象要求
- 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可
锁对象用任意唯一的对象不好
- 会影响其他无关线程的执行
同步锁解决问题
//在这加锁的 然后当两个线程到这的时候只能有一个线程能抢到,抢到以后另一个线程就等待
synchronized (this) {
if (this.money >= money) {
//取钱
System.out.println(name + "取钱:" + money);
this.money -= money;
System.out.println("当前剩余余额:" + this.money);
} else {
System.out.println("余额不足");
}
}
锁对象的规范要求
- 规范上:建议使用共享资源作为锁对象。
- 对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
静态方法
class Acc{
//100个线程来也只有一个人能拿到
public static void run(){
synchronized(Acc.class)
}
}
synchronized (“hh”)这样写是唯一性所有线程都必须从他这拿锁
synchronized (this)这样写是每个对象拿自己的那把锁,其他线程不受干扰
为什么写this呢
因为this是那个类所调用的那个方法传过来的对象
问题:
- 同步代码块是如何实现线程安全的?
- 对出现问题的核心代码使用synchronzed()进行加锁
- 每次只能一个线程占锁进入访问
- 同步代码块的同步锁对象有什么要求?
- 对于实例化方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
END
同步方法:
同步锁可以作为修饰符加上
同步方法的底层原理
同步方法与同步代码块原理一样 只是不需要你自己添加this或类名.class
class Acc{
//同步方法
public synchronized void run(){
//核心代码块
}
}
性能方面:同步代码块可以选择你所想锁的代码 同步方法是将整个方法锁起来,性能方面差别微乎其微。
同步代码块 与 同步方法原理一样
实际开发中同步方法使用较多因为代码简单LOCK锁:
创建方式:
把锁写在类里面,每次new类的时候都会创建出一个锁Lock lock = new ReentrantLock();
- Lock是接口ReentrantLock是Lock的实现类
- lock.lock();//上锁
- lock.unlock();//解锁
看代码:
package com.h.h_lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String cardId;
private double money;
//自创锁 唯一不可替换锁 非常专业 每次创建对象都有一个锁
private final Lock lock = new ReentrantLock();
public Account() {
}
public Account(double money) {
this.money = money;
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
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;
}
//在核心代码块上调用lock方法就上锁
public void drawMoney(double money) {
//获取当前线程是谁在调用
String name = Thread.currentThread().getName();
//为了代码的强健性加上finally
lock.lock(); //上锁
try {
if (this.money >= money) {
//取钱
System.out.println(name + "取钱:" + money);
this.money -= money;
System.out.println("当前剩余余额:" + this.money);
} else {
System.out.println("余额不足");
}
}finally {
//加锁核心代码出现异常,而不会影响解锁
lock.unlock();//解锁
}
}
}
线程池(重点)
谁代表线程池
- ExecutorService接口
线程池概述:
- 线程池就是一个可以复用线程的技术,反复使用;
不使用线程池的问题
- 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务又来了又要创建新线程,而这样创建开销是很大的,这样会严重影响系统性能,所以我们需要用线程池。
如何得到线程池
- 方式一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池
- 方式二:使用Executors(线程池的工具类)调用方法返回不同的特点的线程池对象
线程池常见面试题:
临时线程什么时候创建啊?
- 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 什么是还可以创建临时线程
- 当线程池给的最大线程数还小于等于当前线程的时候还可以创建
什么时候会开始拒绝任务
- 核心线程与临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
Runnable添加进线程池:
package com.h.h_threadpool;
import java.util.concurrent.*;
/*
* 自定义线程池对象,并测试特性
* */
public class ThreadPoolDemo1 {
//1、创建线程池对象
/*int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
*/
public static void main(String[] args) {
ExecutorService es = new ThreadPoolExecutor(3,5,6,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//2、给任务线程池处理
Runnable runnable = new MyRunnable();
//把线程丢入线程池 核心线程3个
es.execute(runnable);
es.execute(runnable);
es.execute(runnable);
//新任务排队5个
es.execute(runnable);
es.execute(runnable);
es.execute(runnable);
es.execute(runnable);
es.execute(runnable);
//临时线程
es.execute(runnable);
es.execute(runnable);
//不创建 拒绝策略被触发
// es.execute(runnable);
//关闭线程池 开发中一般不会使用
// es.shutdownNow(); //立即关闭 即使任务没有完成
// es.shutdown(); //等待任务完成以后才能关闭
}
}
Runnable线程
package com.h.h_threadpool;
public class MyRunnable implements Runnable {
public void run() {
for (long i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"输出了Hello World"+i);
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Callable添加进线程池
package com.h.h_threadpool;
import java.util.concurrent.*;
/*
* 自定义线程池对象,并测试特性
* */
public class ThreadPoolDemo2 {
//1、创建线程池对象
/*int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
*/
public static void main(String[] args) throws Exception {
ExecutorService es = new ThreadPoolExecutor(3,5,6,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//直接把MyCallable对象扔进线程池里面 可以通过返回值 拿到对象
Future<String> f1 = es.submit(new MyCallable(100));
Future<String> f2 = es.submit(new MyCallable(200));
Future<String> f3 = es.submit(new MyCallable(300));
Future<String> f4 = es.submit(new MyCallable(400));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
Callable:
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
/*重写call方法 线程任务方法*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i <= n; i++) {
sum+=i;
}
return "子线程的结果是" +sum;
}
}
可以直接把Callable放入线程池中
Executors工具类:
Executors的底层其实也是基于线程池的实现类ThreadPoolExecutors创建线程池对象
- Executors工具类底层是基于什么方式实现的线程池对象?
- 线程池ExecutorService的实现类:ThreadPoolExecutor
- Executors是否合适大型互联网场景的线程池方案?
- 不适合
- 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确的运行规则,规避资源耗尽的风险。
定时器:
- 定时器是一种控制任务延时调用,或者周期调用技术
定时器的实现方式
- 方式一、Timer
- 构造器:public Timer();//创建定时器对象
- 方式二、ScheduuledExecutorService
Timer定时器的特点和存在的问题
- Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
- 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行
是单线程,出现异常程序就会死掉
单线程定时器: ```java package com.h.h_timer;
import java.util.Timer; import java.util.TimerTask;
/使用定时器/
public class TimerDemo { public static void main(String[] args) { //创建定时器 Timer t = new Timer(); //调用方法处理定时任务 //每隔3秒触发一次,每隔两秒执行一次 //每隔3秒触发一次是这个任务的触发,每隔两秒是再次执行这个任务 t.schedule(new TimerTask() { @Override public void run() { //定时器本身就是一个线程 System.out.println(Thread.currentThread().getName()+”执行一次”);
}
},3000,2000);
}
}
**ScheduledExecutorService**
- 是为了弥补Timer的缺陷
- Executors的方法:public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);//得到线程池对象
- ScheduleExecutorService的方法:public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimerUnit unit)
**ScheduledExecutorService的优点 **
1. 基于线程池,某个任务的执行情况不会影响其他定时任务的执行
其他线程挂掉的时候别的线程不会挂掉<br />互不干扰
线程池定时器:
```java
package com.h.h_timer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/*使用定时器*/
public class TimerDemo2 {
public static void main(String[] args) {
//1、创建ScheduledExecutorService线程池,做定时器
//以后使用这种定时器进行开发
ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
//2、开启定时任务
//参数列表 1:定时器 2:触发时间 3:周期时间 4:单位 秒?毫秒?分?时?
ses.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("定时任务1");
}
}, 0, 2, TimeUnit.SECONDS);
}
}
并发与并行
- 正在运行的程序(软件)就是一个,线程是属于进程的,多个线程其实是并发与并行同时进行
并发理解
- CPU同时处理线程的数量有限
- CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程是同时执行,这就是并发。
- 并发:同一时间段,抢占CPU执行自己
- 并行:同一时刻同时执行
新的线程知识
如何让两个线程依次输出结果
例如:线程1 ABCDE 线程2: 12345
使用最新技术:
park是叫醒 unpark阻塞
LockSupport.park();与LockSupport.unpark();
notify()是按特定算法去叫醒某一个线程没法精确叫醒某一个线程
使用notifyAu()
char aI = {1,2,3,4,5,6};
char aI=C = {"a","b","c","d","e"};
TransferQueue<Character> queue = new LinkedTransferQueue<~>();
TransferQueue 相当于一个套筒 一个人把手伸进去 另一个也把手伸进去 一个人交出东西 另一个接收东西
new Thread(() -> {
try{
for(char c : aI){
System.out.print(queue.take());
//传一个东西进行
queue.transfer(c);
}
}catch(InterruptedException e){
e.printStackTrace();
}
},"t1").start;
new Thread(() ->{
try{
for(char c : aC){
//拿一个东西出来
queue.transfer(c);
System.out.print(queue.take());
}
}catch(InterruptedException e){
e.printStackTrace();
}
},"t2").start;
})