关于线程以及多线程的学习,包括创建和常用方法还有解决线程安全的措施,最后学习线程池和了解并发和并行,对于悲观锁和乐观锁的部分没有学习
目录
1.线程概述
线程
- 线程(Thread)是一个程序内部的一条执行流程
- 程序中如果只有一条执行流程,那么这个程序就是单线程的程序
- 多线程是指从软硬件上实现的多条执行流程的技术(多线程由cpu负责调度)
2.多线程的创建
java通过java.lang.Thread类的对象来代表线程
(1)继承Thread类
- 定义线程子类继承java.lang.Thread.重写run方法
- 创建线程子类对象
- 调用线程对象的start方法启动线程(启动后执行run方法)
多线程的注意事项
1.启动线程必须在调用start方法,不是调用run方法2.不要把主线程任务放在启动子线程之前。
package Create_Thrread;
/*子类继承Thread线程类*/
public class MyThread extends Thread {
//重写Run方法
@Override
public void run(){
//描述线程的执行任务
for (int i = 1; i <= 5; i++) {
System.out.println("MyThread线程输出"+i);
}
}
}
package Create_Thrread;
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建线程类对象代表线程
Thread t = new MyThread();
//4.启动线程(自动执行run方法)
t.start(); //main线程 他线程
for (int i = 1; i <= 5; i++) {
System.out.println("主线程输出"+i);
}
}
}
方式一的优缺
- 优点:编码简单
- 缺点: 线程类已经继承Thread,无法再继承其他类,不利于功能的拓展
(2)实现Runnable接口
- 定义一个线程任务类实现Runnable接口,重写run()方法
- 创建任务对象
- 把任务对象交给Thread处理
- 调用线程对象的start方法启动线程
package Create_Thrread;
//定义一个任务类,实现Runnable接口
public class MyRunnable implements Runnable{
//2.重写run方法
@Override
public void run() {
//线程执行的任务
for (int i = 1; i <= 5; i++) {
System.out.println("MyRunner输出"+i);
}
}
}
package Create_Thrread;
//实现Runnable接口
public class ThreadText2 {
public static void main(String[] args) {
//3.创建任务对象
Runnable target = new MyRunnable();
//4.把一个任务对象交给一个线程对象处理
// public Thread(Runnable target) //有参构造器
new Thread(target).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程输出"+i);
}
}
}
方法二的优缺
- 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,拓展性强
- 缺点:需要多一个Runnable对象
方法二的匿名类写法:
- 创建Runnerabled的匿名内部类对象
- 把任务对象交给Thread处理
- 调用线程对象的start方法启动线程
package Create_Thrread;
//实现Runnable匿名内部类
public class ThreadText2 {
public static void main(String[] args) {
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runner接口类实现"+i);
}
}
};
new Thread(target).start();
//简化形式1:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runner接口类实现"+i);
}
}
}).start();
//简化形式2
new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("Runner接口类实现"+i);
}
}).start();
}
}
(3)实现Callable接口
假设线程执行完需要返回一些数据,前两个种方法重写的run方法均不能直接返回结果
创建步骤
- 创建任务对象:定义一个类实现Callable接口,重写call方法,封装内容和返回数据把Callable类对象封装成FutureTask(线程任务对象)
- 把线程任务对象交给Thread对象
- 调用Thread对象的start方法启动线程
- 执行完毕通过FutureTask对象的grt方法去获取线程任务执行的结果
package Create_Thrread;
import java.util.concurrent.Callable;
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 = 1; i < n; i++) {
sum+=i;
}
return "线程求出了1-"+n+"的和"+sum;
}
}
package Create_Thrread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadText3{
public static void main(String[] args) throws Exception {
Callable<String> call = new MyCallable(10);
//未来任务对象,是一个任务对象实现Runnable对象
//可以在线程执行完毕后,用未来任务对象调用get方法获取执行后的结果
FutureTask<String> fu = new FutureTask<>(call);
new Thread(fu).start();
//获取线程执行完毕后的结果
//注意;如果执行到这里,线程还没有执行完毕
//这里的代码会暂停等待上面的线程执行完毕才会获取结果
System.out.println(fu.get());
}
}
方式三的优缺点
- 优点:线程任务类只是实现接口可以继续继承类和实现接口,拓展性强,可以在线程执行完毕后获取线程执行的结果。
- 缺点:编码复杂一点。
3.Thread的常用方法
常用方法 | 说明 |
public void run() | 线程的任务方法 |
public void start() | 启动线程 |
public String getName() | 获取当前线程的名称 |
public void setName(String name) | 为线程设计名称 |
public static Thread current | 获取当前执行的线程对象 |
public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
public final void join().. | 让调用这个方法的线程先执行完 |
常见构造器 | 说明 |
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象为线程对象 |
public Thread(Runnable target,String name) | 封装Runnable对象为线程对象,并指定线程名称 |
package Create_Thrread;
/*子类继承Thread线程类*/
public class MyThread extends Thread {
public MyThread(String name){
super(name); //为当前线程设置名字
}
public MyThread() {
}
@Override
public void run(){
Thread m = Thread.currentThread();
//描述线程的执行任务
for (int i = 1; i <= 5; i++) {
System.out.println(m.getName()+"子线程输出"+i);
}
}
}
package Create_Thrread;
public class ThreadText4 {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.start();
System.out.println(t1.getName()); //线程默认是有名字的 Thread-0
Thread t2 = new MyThread();
t2.setName("二号线程"); //起名一般放置在启动之前
t2.start();
System.out.println(t2.getName());
//主线程对象的名字
//哪个线程在执行就拿到哪个线程
Thread m = Thread.currentThread();
System.out.println(m.getName());
Thread t3 = new MyThread("三号线程");
t3.start();
for (int i = 1; i <= 5; i++) {
System.out.println("main线程输出"+i);
}
}
}
package Create_Thrread;
//掌握Sleep方法,join方法
public class ThreadText5 {
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
//休眠5秒
if(i == 3){
Thread.sleep(5000);//sleep内的数值单位为毫秒
}
}
Thread t1= new MyThread("一号线程");
t1.start();
t1.join();//让调用这个方法的线程先执行完
Thread t2 = new MyThread("二号线程");
t2.start();
t2.join();
Thread t3 = new MyThread("三号线程");
t3.start();
t3.join();
}
}
4.线程安全
线程安全问题
多个线程在操作同一个共享资源时,可能会出现的业务安全问题
模拟线程安全问题
需求
- 小明和小红是一对夫妻,有一个共同账号,余额为10万元,模拟两个人同时去取10万元
存在线程安全问题!
package Create_safe;
public class Draw extends Thread{
private Account acc;
Thread m = Thread.currentThread();
public Draw(Account acc,String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
//取钱
DrawMonry(100000);
}
private void DrawMonry(int number) {
if (acc.getMoney() >= number) {
System.out.println(m.getName()+"取钱成功");
acc.setMoney(acc.getMoney() - number);
System.out.println("剩余余额"+acc.getMoney());
}
else{
System.out.println("余额不足");
}
}
}
5.线程同步
(1)同步思想概述
解决线程安全问题的方案
线程同步的思想是控制多个线程实现先后访问共享资源
常见方案
- 加锁:每次只允许一个线程加锁,加锁后才能进一步访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
(2)方式一:同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全
原理:每次只允许一个线程加锁,加锁后才能进一步访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
注意事项:
- 对于同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出现bug
- 锁对象使用时建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码对象作为锁对象
package Create_safe;
public class Account {
private double money;; //余额
Thread m = Thread.currentThread();
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void DrawMonry(int number) {
String name = Thread.currentThread().getName();
synchronized (this) { //加锁 使用“楠楠”String时:不可变常量 //而使用this代表共享资源
if (this.money >= number) {
System.out.println(name+"取钱成功");
money = money- number;
System.out.println("剩余余额"+money);
}
else{
System.out.println("余额不足");
}
}
}
//静态方法上锁
public static void test(){
synchronized (Account.class){ //使用字节码文件
System.out.println("静态方法的锁");
}
}
}
(3)方式二:同步方法
作用:把访问共享资源的核心代码给上锁,以此保证线程安全,相比代码块锁的范围比较大
原理:每次只允许一个线程加锁,加锁后才能进一步访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
同步方法底层原理
- 同步方法,其实底层也有隐式对象锁,只是锁的范围是整个方法代码,
- 如果方法是实例方法,同步方法默认this作为锁的对象,
- 如果方法是静态方法。同步方法默认用类名.class作为锁的对象。
package Create_safe;
public class Account {
private double money;; //余额
Thread m = Thread.currentThread();
public Account(double money) {
this.money = money;
}
public Account() {
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public synchronized void DrawMonry(int number) {
String name = Thread.currentThread().getName();
if (this.money >= number) {
System.out.println(name+"取钱成功");
money = money- number;
System.out.println("剩余余额"+money);
}
else{
System.out.println("余额不足");
}
}
}
(4)方式三:Lock锁
Lock锁可以创建出锁对象进行加锁,更灵活,方便,强大
Lock是接口,不能直接实例化,可以采用实现类ReentrantLock来构建Lock锁对象
6.线程通信
线程通信适当多个线程共同操作共享的资源,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源竞争。
线程通信的常见模型(生产者与消费者模型)
- 生产者线程数的生产数据
- 消费者现场负责消费生产者生产的数据
- 注意:生成者生产的数据应该等待自己,通知消费者消费,消费者消费完数据也应该等待自己,再通知生产者生产。 (案例略)
7.线程池
(1)线程池概述
线程池就是一个复用线程的技术,是为了解决请求发起创建新线程,请求过多,影响性能
工作原理
线程池中存有固定数量的线程(工作线程),当任务占用全部线程后,其他任务进行等待(任务队列),有线程空出进行占用,可以控制线程数量和任务数量来避免资源耗尽的风险
(2)线程池创建
ExecutorService 接口 代表线程池。可以通过ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象或者使用Executors调用方法返回不同特点的线程池对象
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数
1.corePoolSize :指定线程池的核心线程数量
2.maximumPoolSize:指定线程池的最大线程数量(其他为临时线程)
3.keepAliveTime:指定临时线程的存活时间
4.unit:指定临时线程存活的时间单位(秒,分,时,天)
5.workQueue:指定线程池的任务队列
6.threadFactory:指定线程池的线程工厂
7.handler :指定线程池的任务拒绝策略(如何处理队列和线程都满时的新任务)
package Create_safe;
import java.util.concurrent.*;
public class ThreadPoolText1 {
public static void main(String[] args) {
//创建一个线程池对象
/*int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory,
RejectedExecutionHandler handler);*/
ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS
,new ArrayBlockingQueue<>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
}
}
注意:1.临时线程在核心线程在忙,队列也满了,并且还可以创建线程时才会创建
2.在核心线程和临时线程都在忙,任务队列也满了的时候会拒绝新任务
(3)处理Runnable任务
常用方法 | 说明 |
void execute(Runnable command) | 执行Runnable任务 |
Future<T> submit(Callable<T> task) | 执行Callable任务,返回未来任务对象,用于获取线程返回结果 |
void shutdown() | 等全部任务执行完,再关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
新任务拒绝策略
策略 | 详解 |
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常,是默认策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常,不推荐 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,并把当前任务加入到队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 有主线负责调用的run()方法从而绕过线程池直接执行 |
package Create_safe;
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
package Create_safe;
import java.util.concurrent.*;
public class ThreadPoolText1 {
public static void main(String[] args) {
//创建一个线程池对象
/*int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory,
RejectedExecutionHandler handler);*/
ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS
,new ArrayBlockingQueue<>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
Runnable ru = new MyRunnable();
pool.execute(ru); //自动创建 自动处理 自动执行
pool.execute(ru); //自动创建 自动处理 自动执行
pool.execute(ru); //自动创建 自动处理 自动执行
pool.execute(ru); //自动创建 自动处理 自动执行
pool.shutdown(); //任务执行完毕后关闭线程池
//pool.shutdownNow();//立刻关闭线程池,不管任务是否执行完毕
}
}
(4)处理Callable任务
ExecutorService常用方法
常用方法 | 说明 |
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 |
Future<T>submit(Callable<T> task) | 执行任务,返回未来对象获取线程结果 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutddownNow() | 立刻关闭,停止正在执行任务,并返回队列中未执行的任务 |
(5)Executors工具类实现
线程池的一个工具类,提供了很多静态方法用于返回不同特点的线程池对象
方法名称 | 说明 |
public static ExecutorService newFixedThreadPool(int nThread) | 创建固定线程数量的线程时如果某个线程因执行异常而结束那么会补充一个新线程 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池,如果该线程出现异常而结束那么会补充一个新线程 |
public static ExecutorService newachedThreadPool() | 线程数量随任务增加而增加,如果线程任务执行完毕且空闲的60秒则会被回收掉。 |
public static ScheduledExecutorService newScheduleThreadPool(int corePoolsize) | 创建一个线程池,可以实现给定延迟后运行任务或者定期执行任务。 |
注意:大型项目使用会出现问题(系统风险)
8.并发 并行
- 进程:正在运行的程序(软件)就是一个独立的进程
- 线程是属于进程的,一个进程同时运行很多个线程
- 进程中的多个线程其实是并发和并行执行的
并发
进程中的线程都是由CPU负责调度执行,但是CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会巡轮为系统每个线程服务,由于CPU的切换的速度很快,给我们的感觉是这些限制的同时执行,这就是并发。
并行
- 在同一时刻上,同时有多个线程在被CPU调度执行
多线程是并行和并发同时执行就是多线程
9.线程的生命周期
- 线程从生到死的过程,经历的各种状态以及状态转换
- 有利于理解多线程
学习时间:2024-08-30