线程是一个程序内部的一条执行流程。
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。
任意类继承线程类Thread。
重写run方法,描述线程任务。
//继承Thread线程类
public class MyThread extends Thread{
//重写run方法,描述线程的执行任务
@Override
public void run() {
for (int i = 1; i < 5; i++) {
System.out.println("子线程Mythread:"+i);
}
}
}
主线程创建类对象,调用start方法启动线程。
/**
* 线程测试一:继承Thread创建线程
* @author 鹿先生
* @date 2023/08/29
*/
public class TreadTest1 {
//main方法是一条默认的主线程负责执行
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();//启动一个子线程
//主线程的业务
for (int i = 1; i < 5; i++) {
System.out.println("主线程:"+i);
}
}
}
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展,也就是具有单继承的问题。
任务类实现Runnable接口。
重写run方法,描述 线程任务。
//任务类
public class MyRunnable implements Runnable{
//重写run方法,描述线程任务
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyRunable:"+i);
}
}
}
主线程中创建任务类,并作为参数创建线程对象,启动子线程。
/**
* 线程测试二:实现Runnable接口
* @author 鹿先生
* @date 2023/08/29
*/
public class TreadTest2 {
//main方法是一条默认的主线程负责执行
public static void main(String[] args) {
Runnable runable = new MyRunnable();//创建任务类对象
Thread thread = new Thread(runable);//使用任务类创建线程
thread.start();//启动线程
//主线程的业务
for (int i = 1; i <= 5; i++) {
System.out.println("主线程:"+i);
}
}
}
/**
* 线程测试二:实现Runnable接口(使用匿名内部类简写)
* 三种简写方法
* @author 鹿先生
* @date 2023/08/29
*/
public class TreadTest2_2 {
public static void main(String[] args) {
//简写方法一:先使用Runnable创建匿名内部类,然后创建线程传入Runnable对象启动
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:"+i);
}
}
};
new Thread(runnable).start();
//简写方法二:创建线程时,Runnable接口创建匿名内部类,然后直接启动。
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2:"+i);
}
}
}).start();
//简写方法三:由于Runnable是函数式接口,所以我们可以使用lambda表达式简写
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程3:"+i);
}
}).start();
}
}
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多创建一个Runnable对象。
任何一个类实现Callable接口,指定返回值泛型,然后重写call方法,描述线程任务。
主线程创建该类对象,封装到FutrueTask未来任务对象中,然后创建线程对象,直接启动线程。
//实现Callable接口,指定返回值类型
public 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 = 1; i <= n; i++) {
sum+=i;
}
return "1-"+n+"的累加和结果为:"+sum;
}
}
使用FutrueTask的get方法获取线程执行完成后的返回结果。
/**
* 线程测试三:实现Callable接口
* @author 鹿先生
* @date 2023/08/29
*/
public class TreadTest3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new MyCallable(100);//创建Callable对象,描述线程任务
FutureTask<String> futureTask = new FutureTask<String>(callable);//使用Callable对象封装成未来任务对象
new Thread(futureTask).start();//启动线程执行未来任务对象的线程任务
//注意:当线程未执行完时,futureTask.get()会阻塞在这,等待线程执行完毕,直到线程执行完,才能获取到结果
System.out.println(futureTask.get());//打印线程执行完后的返回结果
}
}
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后获取线程执行的结果。
缺点:编码复杂一点。
常用方法
// 1.线程的任务方法
public void run()
// 2.启动线程
public void start()
// 3.获取当前线程的名称,线程名称默认是Thread-索引
public String getName()
// 4.为线程设置名称
public void setName(String name)
// 5.获取当前执行的线程对象
public static Thread currentThread()
// 6.让当前执行的线程休眠多少毫秒后,再继续执行
public static void sleep(long time)
// 7.让调用这个方法的线程先执行完,再继续执行其他代码
public final void join()
构造方法
// 1.可以为当前线程指定名称
public Thread(String name)
// 2.封装Runnable对象成为线程对象
public Thread(Runnable target)
// 3.封装Runnable对象成为线程对象,并指定线程名称
public Thread(Runnable target,String name)
线程类
//继承Thread线程类
public class MyThread extends Thread{
//无参构造
public MyThread() {
}
//有参构造
public MyThread(String name) {
super(name);//调用父类构造函数Thread(String name)创建线程时指定线程名称
}
//重写run方法,描述线程的执行任务
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
//打印线程名称
System.out.println(Thread.currentThread().getName()+"线程:"+i);
}
}
}
主线程类
/**
* 线程常用方法案例
* @author 鹿先生
* @date 2023/08/30
*/
public class TreadTest1 {
public static void main(String[] args) throws Exception {
MyThread thread1 = new MyThread();
thread1.setName("1号线程");
thread1.start();//启动一个子线程
MyThread thread2 = new MyThread("2号线程");
thread2.start();//启动一个子线程
thread2.join();//必须等到2号线程执行完,程序才会往下走(也就是说3号线程永远在2号线程后面)
MyThread thread3 = new MyThread("3号线程");
thread3.start();//启动一个子线程
//获取主线程名
String mainName = Thread.currentThread().getName();
//主线程的业务
for (int i = 1; i <= 5; i++) {
if(i==5)Thread.sleep(5000);//当i等于5时,线程休息5秒钟
System.out.println(mainName+"线程:"+i);
}
}
}
注意:Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用。
多个线程同时操作同一个共享资源的时候,可能会出现业务安全的问题。
线程安全问题出现的原因
//账户类
public class Account {
private String cardId;//卡号
private double money;//余额
public Account() {
}
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 toriMoney(Double wantMoney) {
String name = Thread.currentThread().getName();//获取线程名
if (money >= wantMoney) {//余额大于十万元,就取走
System.out.println(name + "来取钱了,要取十万元");
money -= wantMoney;
System.out.println(name + "取钱成功了,还剩余额:" + money);
} else {
System.out.println(name + "取钱失败:余额不足!");
}
}
}
//继承Thread线程类
public class MyThread extends Thread {
private Account acc;
//构造函数传入待操作账户和线程名
public MyThread(Account acc, String name) {
super(name);//利用父类Thread构造方法指定线程名
this.acc = acc;
}
//重写run方法,描述线程的执行任务
@Override
public void run() {
acc.toriMoney(100000.0);//取钱
}
}
/**
* 测试线程安全问题
*/
public class ThreadTest {
public static void main(String[] args) {
//创建一个账户,余额十万元
Account account = new Account("ICBC-110", 100000);
//小明小红线程同时取钱
new MyThread(account, "小明").start();
new MyThread(account, "小红").start();
}
}
原理:把访问共享资源的核心代码给上锁,以此保证线程安全。
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。
建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
对于静态方法建议使用字节码(类名.class)对象作为锁对象。
字符串常量在常量池只会保留一份。
synchronized(同步锁){
访问共享资源的核心代码
}
案例:
//账户类
public class Account {
......
//静态代码块实现线程同步,对于静态方法,锁对象使用类的字节码
public static void count(){
synchronized (Account.class){
System.out.println("我是静态方法。");
}
}
......
//取钱
public void toriMoney(Double wantMoney) {
//静态代码块实现线程同步,建议使用共享资源作为锁对象(这个地方小明和小红的共享资源就是账户,也就是当前的账户对象)
//而且这样锁的范围小一些,不会出bug锁住其他账户
//多个线程操作同一个共享资源,同一时刻只有一个线程能获取锁,也就是一个一个来。
synchronized (this) {
String name = Thread.currentThread().getName();//获取线程名
if (money >= wantMoney) {//余额大于十万元,就取走
System.out.println(name + "来取钱了,要取十万元");
money -= wantMoney;
System.out.println(name + "取钱成功了,还剩余额:" + money);
} else {
System.out.println(name + "取钱失败:余额不足!");
}
}
}
}
原理:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
案例:
//账户类
public class Account {
......
//同步方法实现线程同步,对于静态方法,锁对象默认使用类的字节码
public synchronized static void count() {
System.out.println("我是静态方法。");
}
......
//取钱
public synchronized void toriMoney(Double wantMoney) {
//同步方法实现线程同步,synchronized对于实例方法,锁对象默认就是当前对象
String name = Thread.currentThread().getName();//获取线程名
if (money >= wantMoney) {//余额大于十万元,就取走
System.out.println(name + "来取钱了,要取十万元");
money -= wantMoney;
System.out.println(name + "取钱成功了,还剩余额:" + money);
} else {
System.out.println(name + "取钱失败:余额不足!");
}
}
}
优点:可读性比同步代码块好。
缺点:同步方法锁的范围比同步代码块的范围更大,类似提前排队,性能略差,但是对于当今社会的计算机性能而言,可以忽略不记。
案例:
//账户类
public class Account {
......
//使用final修饰表示锁唯一,不能二次修改;并且在对象的属性里创建lock对象属性表示一个对象一个锁。
private final Lock lk = new ReentrantLock();
......
//取钱
public void toriMoney(Double wantMoney) {
try {
lk.lock();//加锁
String name = Thread.currentThread().getName();//获取线程名
if (money >= wantMoney) {//余额大于十万元,就取走
System.out.println(name + "来取钱了,要取十万元");
money -= wantMoney;
System.out.println(name + "取钱成功了,还剩余额:" + money);
} else {
System.out.println(name + "取钱失败:余额不足!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock();//无论业务是否成果执行,都会解锁,代码健壮性更强
}
}
}
//让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法
void wait()
//唤醒正在等待的单个线程
void notyfy()
//唤醒正在等待的所有线程
void notifyAll()
桌子类
//桌子实体类
public class Desk {
private List<String> list = new ArrayList<>();//桌子上放包子的地方
//put和get两个方法都添加了synchronized,它们锁的是同一个对象,就是桌子对象,会同时锁住三个厨师和两个吃货。
//先唤醒其他线程,然后再等待
//完成线程任务就需要释放锁,所以需要先唤醒其他线程,然后等待当前线程。
//生产包子
public synchronized void put() {
String name = Thread.currentThread().getName();//获取线程名
if (list.isEmpty()){
//没有包子,需要做包子
list.add(name+"做的肉包子!");
System.out.println(name+"做了一个肉包子!");
try {
Thread.sleep(3000);
this.notifyAll();//唤醒所有等待线程
this.wait();//当前线程释放锁,等待
} catch (Exception e) {
e.printStackTrace();
}
}else {
//有包子,不用做
this.notifyAll();//唤醒所有等待线程
try {
this.wait();//当前线程释放锁,等待
} catch (Exception e) {
e.printStackTrace();
}
}
}
//吃包子
public synchronized void get() {
String name = Thread.currentThread().getName();//获取线程名
if (!list.isEmpty()){
//有包子,可以吃
System.out.println(name+"吃了"+list.get(0));
list.clear();
try {
Thread.sleep(1000);
this.notifyAll();//唤醒所有等待线程
this.wait();//当前线程释放锁,等待
} catch (Exception e) {
e.printStackTrace();
}
}else {
//没有包子
this.notifyAll();//唤醒所有等待线程
try {
this.wait();//当前线程释放锁,等待
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
主线程类
//包含五个线程:三个生产者线程和两个消费者线程
//线程通信案例
public class ThreadTest {
public static void main(String[] args) {
//创建一个桌子对象
Desk desk = new Desk();
// 启动五个线程
new Thread(() -> {
while (true) {
desk.put();//生产包子
}
}, "厨师1").start();
new Thread(() -> {
while (true) {
desk.put();//生产包子
}
}, "厨师2").start();
new Thread(() -> {
while (true) {
desk.put();//生产包子
}
}, "厨师3").start();
new Thread(() -> {
while (true) {
desk.get();//吃包子
}
}, "吃货1").start();
new Thread(() -> {
while (true) {
desk.get();//吃产包子
}
}, "吃货2").start();
}
}
//桌子实体类
public class Desk {
private List<String> list = new ArrayList<>();//桌子上放包子的地方
//put和get两个方法都添加了synchronized,它们锁的是同一个对象,就是桌子对象,会同时锁住三个厨师和两个吃货。
//先唤醒其他线程,然后再等待
//完成线程任务就需要释放锁,所以需要先唤醒其他线程,然后等待当前线程。
//生产包子
public synchronized void put() {
String name = Thread.currentThread().getName();//获取线程名
if (list.isEmpty()){
//没有包子,需要做包子
list.add(name+"做的肉包子!");
System.out.println(name+"做了一个肉包子!");
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//吃包子
public synchronized void get() {
String name = Thread.currentThread().getName();//获取线程名
if (!list.isEmpty()){
//有包子,可以吃
System.out.println(name+"吃了"+list.get(0));
list.clear();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
JDK5.0起提供了代表线程池的接口:ExecutorService;常用的实现类是ThreadPoolExecutor。
对于核心线程数量如何选择
计算密集型的任务:核心线程数量 = CPU的的核数(电脑逻辑处理器个数) + 1;
IO密集型的任务: 核心线程数量 = CPU核数 * 2;
如何得到线程池对象
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(
//核心线程数3,最大线程数5,临时线程剔除8秒剔除
3, 5, 8, TimeUnit.SECONDS,
//任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
//任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
new ThreadPoolExecutor.AbortPolicy());
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
构造器
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
线程池的注意事项
//1.执行Runnable任务
void execute(Runnable command)
//2.执行Callable任务,返回未来任务对象,用于获取线程返回的结果
Future<T> submit(Callable<T> task)
//3.等全部任务执行完毕后,再关闭线程池
void shutdown()
// 4.立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
List<Runnable> shutdownNow()
//1. 丢弃任务时并抛出RejectedExecution异常。是默认的策略
ThreadPoolExecutor.AbortPolicy
//2. 丢弃任务,但是不抛出异常这是不推荐的做法
ThreadPoolExecutor.DiscardPolicy
//3. 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.DiscardOldestPolicy
//4. 由主线程负责调用任务的run()方法从而绕过线程池的直接执行
ThreadPoolExecutor.CallerRunsPolicy
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) {
// e.printStackTrace();
// }
}
}
主线程类
//创建线程池
public class ThreadTest {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(
//核心线程数3,最大线程数5,临时线程剔除8秒剔除
3, 5, 8, TimeUnit.SECONDS,
//任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
//任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
new ThreadPoolExecutor.AbortPolicy());
MyRunnable runnable = new MyRunnable();
//线程池启动后,会一直存活,除非手动关闭
//创建3个核心线程处理任务
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
//这4个任务加入任务队列等待
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
//创建两个临时线程
pool.execute(runnable);
pool.execute(runnable);
//这个时候根据任务拒绝策略来处理任务,这里是抛出异常
// pool.execute(runnable);
// pool.shutdown();//等线程执行完再关闭线程池
List<Runnable> list = pool.shutdownNow();//立即关闭线程池
list.stream().forEach(System.out::println);
}
}
callable类
//实现Callable接口,指定返回值类型
public 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 = 1; i <= n; i++) {
sum+=i;
}
return Thread.currentThread().getName()+"求出1-"+n+"的累加和结果为:"+sum;
}
}
主线程类
//线程池处理Callable任务
public class ThreadTest2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(
//核心线程数3,最大线程数5,临时线程剔除8秒剔除
3, 5, 8, TimeUnit.SECONDS,
//任务队列采用数组的阻塞队列,大小为4(也可以创建链表的阻塞队列,那样任务可以无限存放);使用默认的线程工厂
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
//任务拒绝策略是:任务队列满了,无法处理该任务时,抛出异常
new ThreadPoolExecutor.AbortPolicy());
//线程池处理Callable任务返回未来任务对象
Future<String> future1 = pool.submit(new MyCallable(100));
Future<String> future2 = pool.submit(new MyCallable(200));
Future<String> future3 = pool.submit(new MyCallable(300));
Future<String> future4 = pool.submit(new MyCallable(400));//这个地方会复用线程
//调用未来任务对象获取线程返回结果
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());
System.out.println(future4.get());
}
}
//1.创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newFixedThreadPool(int nThreads)
//2.创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新的线程。
public static ExecutorService newSingleThreadExecutor()
//3.线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉
public static ExecutorService newCachedThreadPool()
//4.创建一个线程池,可以实现再给定的延迟后运行任务,或者定期执行任务。
public static ScheduleExecutorService newScheduleThreadPool(int corePoolSize)
案例:
//创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
线程状态 | 说明 |
---|---|
NEW(新建) | 线程刚被创建,但并未启动 |
Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态。 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Time Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用它们将进入Timed Waiting状态。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止run方法而死亡。 |