Java学习——多线程
1.多线程介绍
1.1 什么是多线程
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
1.2 并发与并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
高并发是什么意思:
cpu2核4线程表示可并行处理4个线程,当有更多的线程过来时,就会并发处理
2.线程实现方法
2.1 继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("helloWorld"+i);
};
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第一种实现线程的方法
Thread thread1 = new MyThread();
thread1.start();
}
}
注意, thread1.start()是开启线程,会执行run方法。
run()方法和start()方法的区别?
- run():封装线程执行的代码,直接调用,相当于普通方法的调用
- start():启动线程;然后由JVM调用此线程的run()方法
2.2 实现Runnable接口
实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
};
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 第二种实现线程的方法
MyRun run = new MyRun();
// 第一个参数target – the object whose run method is invoked when this thread is started. If null, this thread's run method is invoked.
Thread thread2 = new Thread(run, "thread2");
thread2.start();
}
}
2.3 实现Callable接口
实现步骤
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
public class MyCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum++;
}
return sum;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第三种实现线程的方法 第三种是第一、二种的补充,可以返回结果
MyCall call = new MyCall();
// 该类用于传递线程参数,同时获取返回结果
FutureTask<Integer> ft = new FutureTask<>(call);
Thread thread3 = new Thread(ft);
thread3.start();
// 获取返回结果
Integer i = ft.get();
System.out.println("thread3 result:"+i);
}
}
三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
3.线程常用方法
3.1 name 方法介绍
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为等于参数name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName());
}
}
上面的super方法是继承了父类中的构造方法,构造方法无法自动继承
3.2 sleep方法介绍
相关方法
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
3.3 线程优先级
线程调度
-
两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
- Java使用的是抢占式调度模型
-
随机性
假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的 -
优先级相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 |
public class ThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// priority 线程优先级
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("线程1");
thread2.setName("线程2");
// 获取线程优先级 默认是5
System.out.println(thread1.getPriority());
System.out.println(Thread.currentThread().getPriority());
// 设置线程优先级 优先级只是扩大被执行的概率,不能一定说明线程会被首先执行
thread1.setPriority(1);
thread2.setPriority(10);
thread1.start();
thread2.start();
}
优先级只是扩大被执行的概率,不能一定说明线程会被首先执行
3.4 守护线程
- 相关方法
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
public class DaemonThread {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("线程1");
thread2.setName("线程2");
// 设置线程2为守护线程 线程1结束后 线程2也会结束(注意不是立刻结束)
thread2.setDaemon(true);
thread1.setPriority(10);
thread2.setPriority(1);
thread1.start();
thread2.start();
}
}
上面开启线程2为守护线程,当线程1结束后,无论线程2是否运行完毕,线程2都会开始结束(不会立刻结束)。应用场景:线程2依赖于线程1的存在,例如QQ文件传输和QQ窗口启动,如果QQ窗口被关闭,文件传输也会关闭。
3.5 礼让线程
- 相关方法
方法名 | 说明 |
---|---|
public static native void yield() | 执行当前线程时,出让cpu的执行权。不让当前线程霸占cpu执行,而是让cpu释放,进行下一次资源的争抢。注意下一次资源争抢当前线程仍有可能抢到 |
public class MyYiled implements Runnable{
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
// 执行当前线程时,出让cpu的执行权。不让当前线程霸占cpu执行,而是让cpu释放,进行下一次资源的争抢。
// 下一次资源争抢当前线程仍有可能抢到
Thread.yield();
};
}
}
这块不太清楚应用场景
3.6 插入线程
- 相关方法
执行当前线程时,出让cpu的执行权。不让当前线程霸占cpu执行,而是让cpu释放,进行下一次资源的争抢。注意下一次资源争抢当前线程仍有可能抢到
方法名 | 说明 |
---|---|
void join() | 表示在当前线程前插入执行 |
public class JoinThread {
public static void main(String[] args) throws InterruptedException {
MyThread thread1 = new MyThread();
thread1.setName("线程1");
thread1.start();
// join的意思是当前线程(main线程)等待thread1执行完毕后再继续执行
thread1.join();
// 当前在主线程的执行中
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
上述过程中,让线程1直接插入到主线程之前执行(jvm会执行main方法的线程),join()方法会让线程执行完毕后才执行下一个线程。也可以设置参数,设置执行该线程的最长执行时间
4.锁
4.1 案例引导
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
- 实现1
public class SellTask extends Thread{
int tickets = 0;
@Override
public void run() {
while (tickets < 100){
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"张票");
}
}
}
- 问题1:从运行结果来看,线程1、2、3之间没有共享tickets变量
- 解决1:将
int tickets = 0;
改成static int tickets
- 问题2:修改后,出现票号重复问题和票号过100问题
- 解决2:出现该问题的原因是,线程控制权的随机性,线程的控制权随时发生变化,导致某个线程无法执行完所有过程就被其它线程抢占,导致了该问题
解决方法是将卖票的逻辑上锁,使得操作共享变量的代码在任意时刻只能有一个线程执行
public class SellTask extends Thread{
static int tickets = 0;
// 锁对象 可以是任意对象
static Object obj = new Object();
@Override
public void run() {
synchronized (obj){ // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
//t1进来后,就会把这段代码给锁起来
while (tickets < 100){
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"张票");
}
//t1出来了,这段代码的锁就被释放了
}
}
}
public class SellThread {
public static void main(String[] args) {
SellTask thread1 = new SellTask();
SellTask thread2 = new SellTask();
SellTask thread3 = new SellTask();
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果:
注意:多个操作共享同一变量的线程之前应该使用同个锁。锁可以是任意对象,一般是传入当前类的.class文件。例如:上述中的SellTask.class
- 问题3 按照上面的写,会出现同一个窗口一直买票的过程
- 解决3 因为将整个while循环放在了锁之间,所以一个线程会执行完100次循环才会推出。应该将循环放在锁之外
public class SellTask extends Thread{
static int tickets = 0;
// 锁对象 可以是任意对象
static Object obj = new Object();
@Override
public void run() {
while (tickets < 100){
//t1进来后,就会把这段代码给锁起来 注意锁写在一次循环上,即while之内
synchronized (obj){ // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"张票");
}
//t1出来了,这段代码的锁就被释放了
}
}
}
- 问题4:上述代码执行后,仍出现票号大于100问题
- 解决4:原因是将while(tickets<100)的判断放在了锁之外,假如某个线程在tickets==99的时候进入了睡眠,此时线程2和线程3也通过了tickets<100的判断,并进入到了锁之外进行等待,就会出现上述问题。因此要把可能引发线程安全的代码全部写在锁之内!!!
最终修正代码
public class SellTask extends Thread{
static int tickets = 0;
// 锁对象 可以是任意对象
static Object obj = new Object();
@Override
public void run() {
while (true){
//t1进来后,就会把这段代码给锁起来 注意锁写在一次循环上,即while之内
synchronized (obj){ // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
if(tickets==100){
break;
}else {
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"张票");
}
}
//t1出来了,这段代码的锁就被释放了
}
}
}
public class SellThread {
public static void main(String[] args) {
SellTask thread1 = new SellTask();
SellTask thread2 = new SellTask();
SellTask thread3 = new SellTask();
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
4.2 同步代码块
同步代码块格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
4.3 同步方法
同步方法的锁是默认的,无法自己设置
-
同步方法的格式
同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
同步方法的锁对象是什么呢?
this
-
静态同步方法
同步静态方法:就是把synchronized关键字加到静态方法上
修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体; }
同步静态方法的锁对象是什么呢?
类名.class
4.3.1 使用同步方法完成上述案例
public class SellRun implements Runnable{
// 因为多个线程的参数对象都是这一个实例,所有不用声明static也可
int ticket = 0;
@Override
public void run() {
while (true) {
if(synchronize()){
break;
}
}
}
synchronized boolean synchronize(){
if(ticket==100){
return true;
}else{
ticket++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");
return false;
}
}
}
/**
* 实例同步方法实现上锁
*/
public class SellThread {
public static void main(String[] args) {
SellRun sellRun = new SellRun();
Thread thread1 = new Thread(sellRun);
Thread thread2 = new Thread(sellRun);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread1.start();
thread2.start();
}
}
4.3.2 使用静态同步方法完成上述案例
public class SellTask extends Thread{
static int ticket = 0;
@Override
public void run() {
while (true) {
if(synchronize()){
break;
}
}
}
// 静态方法 锁的对象是类名.class
static synchronized boolean synchronize(){
try {
sleep(10);
if(ticket==100){
return true;
}else{
ticket++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket+"张票");
return false;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* 静态同步方法实现上锁
*/
public class SellThread {
public static void main(String[] args) {
Thread thread1 = new SellTask();
Thread thread2 = new SellTask();
thread1.setName("窗口1");
thread2.setName("窗口2");
thread1.start();
thread2.start();
}
}
4.4 锁对象
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁方法
方法名 说明 void lock() 获得锁 void unlock() 释放锁
4.4.1 使用锁对象完成上述案例
public class SellTask extends Thread{
static int tickets = 0;
static Object obj = new Object();
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();
//t1进来后,就会把这段代码给锁起来 注意锁写在一次循环上,即while之内
if(tickets==100){
break;
}else {
sleep(10);
tickets++;
System.out.println(Thread.currentThread().getName()+"卖出了第"+tickets+"张票");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
//t1出来了,这段代码的锁就被释放了
}
}
public class SellThread {
public static void main(String[] args) {
SellTask thread1 = new SellTask();
SellTask thread2 = new SellTask();
SellTask thread3 = new SellTask();
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
finally方法表示即使是break也会执行,要注意这一点。break跳出循环也需要解锁
4.5 死锁问题
-
概述
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
-
什么情况下会产生死锁
- 资源有限
- 同步嵌套
模拟
/**
* 模拟死锁
*/
public class RunThread {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.setName("线程1");
myThread2.setName("线程2");
myThread1.start();
myThread2.start();
}
}
/**
* 模拟死锁
*/
public class MyThread extends Thread {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
while(true){
if("线程1".equals(getName())){
synchronized (objA){
System.out.println("线程1拿到了资源A,准备获取资源B");
synchronized (objB){
System.out.println("线程1拿到了资源A和资源B,完成一次任务");
}
}
}
if("线程2".equals(getName())){
synchronized (objB){
System.out.println("线程2拿到了资源B,准备获取资源A");
synchronized (objA){
System.out.println("线程2拿到了资源A和资源B,完成一次任务");
}
}
}
}
}
}
执行结果
线程1和线程B都在等待对方释放资源,造成了死锁。
如何避免死锁:避免锁的互相嵌套
5 生产者与消费者模式
5.1 概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
Object类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
注意上述方法需要和锁对象绑定。唤醒的时候唤醒的是和该锁相关的所有等待线程
5.2 案例
-
案例需求
-
桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
-
生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
-
消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有包子,决定当前线程是否执行
2.如果没有包子,就进入等待状态,如果有包子,就消费包子
3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子
-
测试类(Demo):里面有main方法,main方法中的代码步骤如下
创建生产者线程和消费者线程对象
分别开启两个线程
-
代码实现:
-
//套路:
-
//1. while(true)死循环
-
//2. synchronized 锁,锁对象要唯一
-
//3. 判断,共享数据是否结束. 结束
-
//4. 判断,共享数据是否结束. 没有结束
消费者类:判断桌子上是否有食物,如果有就开吃,没有就等待并唤醒其它厨师线程
public class Consumer extends Thread{
@Override
public void run() {
while (true){
synchronized (lock){
if(Desk.ifFood){
Desk.foodNum--;
System.out.println("吃货吃了第"+(11-foodNum)+"碗饭");
Desk.ifFood = false;
lock.notifyAll();
if(Desk.foodNum==0){
break;
}
}else{
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
生产者类:生产者,判断是否有食物,如果有就等待,如果没有就做,并唤醒消费者取吃,如果消费者已经吃了十碗就停止
public class Producer extends Thread{
@Override
public void run() {
while (true){
synchronized (lock){
if(Desk.ifFood){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
// 表示消费者已经吃不下了,不会再生产
if(foodNum == 0){
break;
}
Desk.ifFood=true;
System.out.println("生产者生产了一碗饭");
// 唤醒消费者去吃
lock.notifyAll();
}
}
}
}
}
中间仓库类:判断是否有食物、消费者消费的容量、定义公共锁
public class Desk {
// 是否有食物
public static boolean ifFood = false;
// 包子数量 此处仅表示消费者消费的最大数量
public static int foodNum = 10;
// 锁对象
public static final Object lock = new Object();
}
运行类:
public class Run {
public static void main(String[] args) {
Consumer consumer = new Consumer();
Producer producer = new Producer();
consumer.setName("消费者");
producer.setName("生产者");
consumer.start();
producer.start();
}
}
实现结果:生产者不断生产、消费者不断消费,直到消费者吃了10碗饭
5.3 使用阻塞队列实现
- 阻塞队列继承结构
-
常见BlockingQueue:
ArrayBlockingQueue: 底层是数组,有界
LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
-
BlockingQueue的核心方法:
put(anObject): 将参数放入队列,如果放不进去会阻塞
take(): 取出第一个数据,取不到会阻塞
public class Cooker extends Thread {
private ArrayBlockingQueue<String> bd;
public Cooker(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
// 生产者步骤:
// 1,判断桌子上是否有汉堡包
// 如果有就等待,如果没有才生产。
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while (true) {
try {
bd.put("汉堡包");
System.out.println("厨师放入一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread {
private ArrayBlockingQueue<String> bd;
public Foodie(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
@Override
public void run() {
// 1,判断桌子上是否有汉堡包。
// 2,如果没有就等待。
// 3,如果有就开吃
// 4,吃完之后,桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
//套路:
//1. while(true)死循环
//2. synchronized 锁,锁对象要唯一
//3. 判断,共享数据是否结束. 结束
//4. 判断,共享数据是否结束. 没有结束
while (true) {
try {
String take = bd.take();
System.out.println("吃货将" + take + "拿出来吃了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) {
ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
Foodie f = new Foodie(bd);
Cooker c = new Cooker(bd);
f.start();
c.start();
}
}
在这里将队列作为构造函数的参数传了进去。put和take方法内部已经实现了锁,因此不用再自定义锁
6 多线程的6种状态
Java中没有运行状态,因为运行是由操作系统来实现的
Java中只有下述几种状态:
7 多线程应用
7.1 多个抽奖箱抽奖问题————插入线程、线程栈知识点
需求:1.抽奖池,抽奖池的奖项设置为{10,5,20,50,100,200,500,800,2,80,300,700}
2.两个抽奖箱,随机抽奖
3.抽完奖打印出每个箱子的最大奖金
7.1.1 我的解决方案:在运行类中定义很多静态变量
public class Mytask implements Runnable{
@Override
public void run() {
// 创建的变量将存放在栈内存中,栈内存的变量是不共享的
ArrayList<Integer> integers1 = new ArrayList<>();
while (true){
synchronized (Run5.lock){
if(list.isEmpty()){
System.out.println(Thread.currentThread().getName()+integers1);
break;
}else{
Random random = new Random();
int index = random.nextInt(list.size());
Integer removeMoney = list.remove(index);
System.out.println(Thread.currentThread().getName()+"产生了一个"+removeMoney+"元大奖");
if("箱子1".equals(Thread.currentThread().getName())){
list1.add(removeMoney);
}else{
list2.add(removeMoney);
}
integers1.add(removeMoney);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Run5 {
public static List<Integer> integers = List.of(10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700);
// 定义抽奖箱
public static ArrayList<Integer> list = new ArrayList<Integer>(integers);
// 抽奖箱1和2分别抽到的
public static ArrayList<Integer> list1 = new ArrayList<Integer>();
public static ArrayList<Integer> list2 = new ArrayList<Integer>();
// 定义锁
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Mytask mytask = new Mytask();
Thread thread1 = new Thread(mytask);
Thread thread2 = new Thread(mytask);
thread1.setName("箱子1");
thread2.setName("箱子2");
thread1.start();
thread2.start();
// join的含义是当前线程(main线程)出让cup执行权,等到thread2执行完毕。在等待期间,thread1和thread都有可能抢到执行权
thread2.join();
thread1.join();
System.out.printf("抽奖箱1共产生了%d个大奖,分别为%s,最高奖项为%d,总计额为%d",
list1.size(),
list1,
list1.stream().mapToInt(Integer::intValue).max().orElse(0),
list1.stream().mapToInt(Integer::intValue).sum());
}
}
这里面因为存在thead1和thread2线程可能晚于main线程执行的问题,main打印结果时不准确,所以使用了join方法使得main线程的打印方法在thread1和thread2之后执行。
7.1.2 网课解决方案:使用栈内存
每个线程都有自己的栈内存,线程中创建的变量彼此不共享(堆内存是共享的),这样可以将每个线程抽奖的结果单独定义起来。
方案:见上述代码线程任务run()方法中integers1列表。
8 线程池
8.1 为什么使用线程池
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。
针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
8.2 默认线程池
- static ExecutorService newCachedThreadPool() 创建一个默认的线程池
- static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池
public class Main {
public static void main(String[] args) {
// 创建线程池(无限定数量)
ExecutorService pool = Executors.newCachedThreadPool();
// 创建线程池(有限定数量)
ExecutorService pool1 = Executors.newFixedThreadPool(3);
// 提交任务
// pool.submit(new MyRun());
// pool.submit(new MyRun());
// pool.submit(new MyRun());
// pool.submit(new MyRun());
// pool.submit(new MyRun());
pool1.submit(new MyRun());
pool1.submit(new MyRun());
pool1.submit(new MyRun());
pool1.submit(new MyRun());
pool1.submit(new MyRun());
// 销毁线程池(实际服务运行中不用销毁)
// pool.shutdown();
}
}
8.3 自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
- 核心线程数量:这些线程不会销毁,一直在线程池中
- 最大线程数量:核心线程数量+临时线程数量
- 空闲线程最大存活时间:临时线程最长可以在线程池中存留多久
- 任务队列:核心线程处理不过来时,任务会加入到队列中等待
- 创建线程工厂:创建线程的工程
- 任务的拒绝策略:当达到最大线程数量、最大队列,剩下的任务如何处理
举例:最大线程5,核心线程2,任务队列容量4,当2个任务来时,核心线程先去处理,再来第3-6个任务,排在任务队列中,第7-9个任务,由创建的临时线程处理。 临时线程是在队列满了之后才创建的。
本文作者:风一样的我1
本文链接:https://www.cnblogs.com/pengu1998/p/18297326
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!