多线程学习笔记
一、概念
-
线程是一个程序内部的一条执行流程。
-
程序中如果只有一条执行流程,那这个程序就是单线程的程序。
-
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
-
Java虚拟机允许应用程序同时执行多个执行线程。
-
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。
二、如何创建线程
- 启动线程必须调用start方法而不是run方法,start方法可以开启一个线程,直接调用run方法会当初普通对象的方法执行,此时没有启动线程执行。
- main方法是一条默认的主线程负责执行
方法一:继承Thread类
-
任意类继承线程类Thread。
-
重写run方法,描述线程任务。
//继承Thread线程类 public class MyThread extends Thread{//重写run方法,描述线程的执行任务@Overridepublic 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接口
基础版
-
任务类实现Runnable接口。
-
重写run方法,描述 线程任务。
//任务类 public class MyRunnable implements Runnable{//重写run方法,描述线程任务@Overridepublic 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() {@Overridepublic 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(){@Overridepublic 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接口
- 这个方法可以获取线程执行完后的返回值,但是编码稍微复杂一点点。
-
任何一个类实现Callable接口,指定返回值泛型,然后重写call方法,描述线程任务。
-
主线程创建该类对象,封装到FutrueTask未来任务对象中,然后创建线程对象,直接启动线程。
//实现Callable接口,指定返回值类型 public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}//重写call方法,描述线程任务@Overridepublic 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.方法
常用方法
// 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)
2.案例
线程类
//继承Thread线程类
public class MyThread extends Thread{//无参构造public MyThread() {}//有参构造public MyThread(String name) {super(name);//调用父类构造函数Thread(String name)创建线程时指定线程名称}//重写run方法,描述线程的执行任务@Overridepublic 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、守护线程、线程优先级等线程的控制方法,在开发中很少使用。
四、线程安全
1. 线程安全问题
-
多个线程同时操作同一个共享资源的时候,可能会出现业务安全的问题。
-
线程安全问题出现的原因
- 存在多个线程在同时执行
- 同时访问一个共享资源
- 存在修改该共享资源
2. 用程序模拟线程安全问题
- 小明和小红同时取钱,银行会亏十万,这就发生了线程安全问题。
1.账户类
//账户类
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 + "取钱失败:余额不足!");}}
}
2.线程类
//继承Thread线程类
public class MyThread extends Thread {private Account acc;//构造函数传入待操作账户和线程名public MyThread(Account acc, String name) {super(name);//利用父类Thread构造方法指定线程名this.acc = acc;}//重写run方法,描述线程的执行任务@Overridepublic void run() {acc.toriMoney(100000.0);//取钱}
}
3.两个账户同时取钱
/*** 测试线程安全问题*/
public class ThreadTest {public static void main(String[] args) {//创建一个账户,余额十万元Account account = new Account("ICBC-110", 100000);//小明小红线程同时取钱new MyThread(account, "小明").start();new MyThread(account, "小红").start();}
}
五、线程同步(线程安全的解决办法)
1.线程思想概述
- 线程同步是解决线程安全问题的方案(放生线程安全问题时就是因为线程时异步的,所以我们选择同步就没问题了)。
- 思想;让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
- 线程同步的原理:就是加锁,每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动释放锁,然后其他线程才能再加锁进来。
2.方式一:同步代码块
原理:把访问共享资源的核心代码给上锁,以此保证线程安全。
-
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出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 + "取钱失败:余额不足!");}}}
}
3.方式二:同步方法(推荐)
原理:把访问共享资源的核心方法给上锁,以此保证线程安全。
- 同步方法其实底层也是有隐式锁对象的,也就是默认有锁对象,锁的范围是整个方法代码。
- 如果是普通方法,锁对象就是当前对象;如果是静态方法,锁对象就是当前类的字节码。
修饰符 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 + "取钱失败:余额不足!");}}
}
优点:可读性比同步代码块好。
缺点:同步方法锁的范围比同步代码块的范围更大,类似提前排队,性能略差,但是对于当今社会的计算机性能而言,可以忽略不记。
4.方式三:Lock锁
- Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
- Lock是接口,不能直接实例化,可以采用它的实现类ReentranLock来构建Lock锁对象。
- 手动加锁和解锁。
案例:
//账户类
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();//无论业务是否成果执行,都会解锁,代码健壮性更强}}
}
六、线程通信(了解)
- 线程通信:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。
- 线程通信的常见模型(生产者与消费者模型)
- 生产者线程负责生产数据
- 消费者线程负责消费生产者生产的数据。
- 注意:生产者生产完数据应该等待自己,通知消费者;消费者消费完数据也应该等待自己,再通知生产者生产!(等待就是释放锁,不再竞争cpu资源)
- 线程通信的前提是保证线程安全。
- Object类的等待和唤醒方法(这些方法必须使用锁对象调用):
//让当前线程等待并释放所占锁,直到另一个线程调用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();}}}
}
七、线程池
1.概述
- 线程池是一个可以复用线程的技术。
- 不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。
- 在线程池中,咱们的线程叫工作线程(WorkThread),需要处理的任务会排队进入任务队列(WorkQueue)依次被工作线程处理,这个任务必须要实现Runnable接口或者callable接口。
- 线程池创建后会一直存活,除非手动关闭,核心线程只要被创建,就会一直存在。
2.线程池的创建
-
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)
- 参数一:corePoolSize:指定线程池的核心线程的数量。
- 参数二:maximumPoolSize:指定线程池的最大线程数量。
- 参数三:keepAliveTime:指定临时线程的存活时间。
- 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
- 参数五:workQueue:指定线程池的任务队列。
- 参数六:threadFactory:指定线程池的线程工厂。
- 参数七:handler:指定线程池的任务拒绝策略(线程都很忙,任务队列也满了的时候,新任务来了该怎么处理)
-
线程池的注意事项
- 线程池中临时线程的创建时间:新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
- 线程池中拒绝新任务的时间:核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
3.处理Runnable任务
1.ExecutorService的常用方法
//1.执行Runnable任务
void execute(Runnable command)
//2.执行Callable任务,返回未来任务对象,用于获取线程返回的结果
Future<T> submit(Callable<T> task)
//3.等全部任务执行完毕后,再关闭线程池
void shutdown()
// 4.立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
List<Runnable> shutdownNow()
2.任务拒绝策略
//1. 丢弃任务时并抛出RejectedExecution异常。是默认的策略
ThreadPoolExecutor.AbortPolicy
//2. 丢弃任务,但是不抛出异常这是不推荐的做法
ThreadPoolExecutor.DiscardPolicy
//3. 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.DiscardOldestPolicy
//4. 由主线程负责调用任务的run()方法从而绕过线程池的直接执行
ThreadPoolExecutor.CallerRunsPolicy
3.案例:
Runnable类
public class MyRunnable implements Runnable{@Overridepublic 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);}
}
4.处理Callable任务
callable类
//实现Callable接口,指定返回值类型
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}//重写call方法,描述线程任务@Overridepublic 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());}
}
5.使用Executors得到线程池
- Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
- 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
//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);
注意:
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险,出现OOM内存溢出(因为newFixedThreadPool和newSingleThreadExecutor的最大任务数默认是Integer.MAX_VALUE,newCachedThreadPool的最大线程数也是Integer.MAX_VALUE,容易内存溢出)。
八、并发、并行
- 正在运行的程序(软件)就是一个独立的进程。
- 线程是属于进程的,一个进程中可以同时运行很多个线程。
- 进程中的多个线程其实是并发和并行执行的。
- 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
- 在同一时刻上,同时有多个线程在被CPU调度执行,这就是并行。
- 多线程到底是怎样执行的:并发和并行同时执行的。
九、线程的生命周期
- 线程的生命周期从生到死的过程中,经历的各种状态及状态转换。
- java总共定义了6种状态,6种状态都定义在Thread的内部枚举类中。
线程状态 | 说明 |
---|---|
NEW(新建) | 线程刚被创建,但并未启动 |
Runnable(可运行) | 线程已经调用了start(),等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态。 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Time Waiting(计时等待) | 同waiting状态,有几个方法(sleep,wait)有超时参数,调用它们将进入Timed Waiting状态。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止run方法而死亡。 |
十、乐观锁、悲观锁
- 悲观锁:一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差。
- 乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等到要出现线程安全问题的时候才开始控制。线程安全,性能较好。
- CAS算法(乐观锁的原理):Compare and set,比较并且修改。
- 内置乐观锁是由原子类实现的,比如整数修改的乐观锁,我们就用AtomicInteger作为整数变量类型,然后使用incrementAndGet()方法自增并返回自增后的值。
Runnable类
//任务类
public class MyRunnable implements Runnable{//原子类整数类型实现了乐观锁private AtomicInteger count = new AtomicInteger();//普通int类型
// private int count;//重写run方法,描述线程任务@Overridepublic void run() {for (int i = 1; i <= 100; i++) {System.out.println(Thread.currentThread().getName()+"增加后的数字:"+count.incrementAndGet());
// System.out.println(Thread.currentThread().getName()+"增加后的数字:"+ (++count));}}
}
主线程类
/*** 原子类内置乐观锁* @author 鹿先生* @date 2023/09/1*/
public class TreadTest {public static void main(String[] args) {//创建任务类对象MyRunnable runnable = new MyRunnable();//调用一百个线程同时自增for (int i = 1; i <= 100; i++) {new Thread(runnable).start();}}
}