java基础——线程相关
本人在阿里暑期实习单招落榜(2025.6.4)之后,下定决心补全java基础知识时做的笔记
1.多线程
1.1 多线程实现的三种方式
1.1.1 继承Thread类
- 定义MyThread类,重写run方法:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Hello,"+ getName());
}
}
}
- 实现多线程:
public static void main(String[] args) {
//1.创建MyThread对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
//2.利用Thread对象启动线程
t1.start();
t2.start();
}
1.1.2 实现Runnable接口
- 定义MyRunnable接口,实现run方法:
public class MyRunnable implements Runnable{
@Override
public void run() {
//获取到当前执行该方法的Thread对象
Thread t = Thread.currentThread();
for (int i = 0; i < 100; i++) {
System.out.println("Hello,"+ t.getName());
}
}
}
- 实现多线程:
public static void main(String[] args) {
//1.创建MyRunnable对象
MyRunnable myRunnable = new MyRunnable();
//2.利用MyRunnable对象创建Thread对象
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.setName("线程1");
t2.setName("线程2");
//3.利用Thread对象启动线程
t1.start();
t2.start();
}
1.1.2 实现Callable接口(可获取线程运行结果)
- 定义MyCallable类
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//求1~100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
- 实现多线程
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建MyCallable对象
MyCallable myCall = new MyCallable();
//2.创建FutureTask对象,用于管理单线程运行的结果
FutureTask<Integer> ft = new FutureTask<>(myCall);
//3.通过FutureTask对象来创建线程对象
Thread t1 = new Thread(ft);
//4.利用Thread对象启动线程
t1.start();
//5.利用FutureTask的get()方法获取线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
1.2 三种方式的比较
| 实现方式 | 优点 | 缺点 |
|---|---|---|
| 继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类 |
| 实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
| 实现Callable接口 | - 扩展性强(同样可继承其他类)- 支持返回值(通过FutureTask获取)- 支持抛出检查异常 | - 编程最复杂- 需配合ExecutorService使用- 不能直接使用Thread类方法 |
1.3 Thread的常见成员方法
| 方法名称 | 说明 |
|---|---|
String getName() |
返回此线程的名称 |
void setName(String name) |
设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() |
获取当前线程的对象 |
static void sleep(long time) |
让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) |
设置线程的优先级 |
final int getPriority() |
获取线程的优先级 |
final void setDaemon(boolean on) |
设置为守护线程 |
public static void yield() |
出让线程/礼让线程 |
public static void join() |
插入线程/插队线程 |
1.3.1 sleep(long time)
-
描述: 让使用当前函数的线程休眠指定时间。
-
代码示例:
public static void main(String[] args) throws InterruptedException {
Thread currThread = Thread.currentThread();
System.out.println(currThread.getName());
Thread.sleep(5000);
System.out.println("111");
}
- 输出结果:先输出main(执行当前main方法的是main线程),5秒之后输出111。
1.3.2 线程的优先级
-
描述: 表示线程抢占CPU的概率的大小,范围为1~10,默认为5。
-
注意:只能表示抢占到CPU的概率,如设置为10并不表示一定先抢占到CPU,只是概率较高
1.3.3 守护线程(Daemon)
-
描述:
-
可以看作是备胎
-
当非守护线程运行结束之后,守护线程会陆续结束(记忆:女神不存在了,备胎也没有存在的必要了)
-
没有马上结束,是因为通知守护进程结束需要一定的时间,在这段时间内守护进程看继续执行
-
守护进程也会抢占非守护进程的CPU使用权
-
-
代码示例:
- MyThread1类:
public class MyThread1 extends Thread{ //继承父类的构造方法,实现通过构造方法设置线程名称 public MyThread1(String name) { super(name); } @Override public void run() { for (int i = 1; i <= 10; i++) { System.out.println(getName()+"@"+i); } } }- MyThread2类:
public class MyThread2 extends Thread{ public MyThread2(String name) { super(name); } @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println(getName()+"@"+i); } } }- 测试:
public static void main(String[] args) { MyThread1 t1 = new MyThread1("女神"); MyThread2 t2 = new MyThread2("备胎"); //设置t2为守护进程 t2.setDaemon(true); //结果:1.当“女神”进程打印到10结束之后,守护进程未打印到100也结束了 //2.但是在“女神进程”打印过程中,备胎也有可能抢占CPU进行打印,不会完全让给“女神”进程 t1.start(); t2.start(); }
1.3.4 线程礼让 :yield()
-
描述: Thread.yield()能够让出当前线程占用的CPU的使用权,调用该方法能使得多线程执行得比较均匀,但并不是完全均匀的。
-
注意: 让出CPU的使用权并不是当前线程之后就不会去抢占了,它下次还是会继续抢占的。
-
代码示例:
- MyThread类:
public class MyThread extends Thread{ @Override public void run() { for (int i = 1; i <= 100 ; i++) { System.out.println(getName() + "@" + i); //礼让:让出CPU使用权 Thread.yield(); } } }- 测试:
public static void main(String[] args) { MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); mt1.setName("飞机"); mt2.setName("坦克"); mt1.start(); mt2.start(); }- 结果:“坦克”和“飞机”出现得较为均匀。
1.3.4 线程插入:join()
-
描述: 实现把调用join方法的线程插入到当前方法对应线程之前。
-
注意:join()方法要放在start()之后才会生效,以下是原因:
-
start()方法的作用是启动线程,使其进入就绪状态(Runnable),随后由JVM调度执行。 -
如果在调用
start()之前调用join(),目标线程尚未启动(甚至未进入就绪队列),此时join()的等待毫无意义,因为线程根本不会执行。
-
-
代码示例:
- MyThread类:
public class MyThread extends Thread{ @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println(getName()+"@"+i); } } }- 测试:
public static void main(String[] args) throws InterruptedException { MyThread t1 = new MyThread(); t1.setName("土豆"); t1.start(); t1.join(); for (int i = 1; i <= 10 ; i++) { System.out.println(Thread.currentThread().getName() + "@" +i); } }- 结果: “土豆”进程全部打印完之后才轮到main进程的main方法中的for循环进行打印。
1.4 线程安全
-
引出:现在有一个需求:实现三个窗口一起卖100张票。
-
实现:
- 定义一个MyThread类:
public class MyThread extends Thread{ private static int ticket = 0; @Override public void run() { while (true){ if(ticket < 100){//让打印的过程慢一点 try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(getName()+" sell ticket"+ticket); ticket++; } else{ break; } } } public int getTicket(){ return ticket; } }- 测试:
public static void main(String[] args) throws InterruptedException { //需求:实现三个窗口一起卖100张票 MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); MyThread my3 = new MyThread(); my1.start(); my2.start(); my3.start(); my1.join(); my2.join(); my3.join(); System.out.println(my1.getTicket()); }-
结果:
- 出现多个线程操作同一张票的现象:
Thread-2 sell ticket99 Thread-1 sell ticket99 Thread-0 sell ticket99- 出现超卖现象: 输出最终卖出票数为102。
-
原因: 多个线程会随机抢占CPU的资源,导致每个线程执行run方法中扣减库存的操作未完成,CPU就马上被其他线程抢占了。
-
解决方法: 使用synchronized关键字,实现线程同步(一个一个线程按顺序来执行,一个线程执行完了,才轮到下一个线程执行)。使用synchronized之后,将为指定区域内的操作进行上锁,在次期间其他线程都不能抢占CPU,只能等待当前线程将指定区域内的操作执行完之后,锁会自动打开,此时其他线程才能抢占CPU使用权。分别有三种实现方式:同步代码块 、同步方法 和 加锁。
1.4.1 同步代码块
- 实现:
synchronized(锁对象){
需要上锁的操作...
}
- 将上述MyThread类改造:
public class MyThread extends Thread{
private static int ticket = 0;
public Object myObj = new Object();//当前示例的锁对象
public static Object classObj = new Object();//MyThread类的锁对象
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if(ticket < 100){//让打印的过程慢一点
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+" sell ticket"+ticket);
ticket++;
} else{
break;
}
}
}
}
public int getTicket(){
return ticket;
}
}
-
范围控制: 其中,锁对象能够指定需要同步的范围,如:需要使同一类的所有对象都同步时,可以让锁对象为当前类的字节码(如MyThread.class)或者当前类的static对象(如上述代码中的classObj);若使用上述代码中的myObj或this关键字,则只能将范围限制到当前线程对应的示例对象,即此时不同线程之间就无法实现同步了。
-
运行结果:不同线程消费同一张票和库存超卖的现象均不出现了。
1.4.2 同步方法
-
实现: 在方法前加上synchronized即可。
-
范围控制: 需要使同一类的所有对象都同步时,synchronized修饰的方法需要为整个类中所有对象共享的static方法;若修饰的是非静态方法,则设置同步的范围就为当前线程。
-
实现思路:
-
先按照同步代码块的方式来写,则MyRunnable类一开始为:
public class MyRunnable implements Runnable{ private int ticket = 0; @Override public void run() { while (true){ synchronized (MyRunnable.class){ if(ticket >= 100){ break; }else{ System.out.println(Thread.currentThread().getName()+" sell ticket :"+ticket); ticket++; } } } } public int getTicket(){ return ticket; } } -
再选中synchronized修饰的代码块部分,按下ctrl+alt+m,即可将其剥离成一个方法:
public class MyRunnable implements Runnable{ private int ticket = 0; @Override public void run() { while (true){ if (sellTicket()) break; } } private synchronized boolean sellTicket() { synchronized (MyRunnable.class){ if(ticket >= 100){ return true; }else{ System.out.println(Thread.currentThread().getName()+" sell ticket :"+ticket); ticket++; } } return false; } public int getTicket(){ return ticket; } } -
注意: 因为此时的MyRunnable类是实现Runnable类来构造的,因此在实际使用中只需要创建一个MyRunnable对象和三个Thread对象,故ticket只属于当前线程,并不是线程共享的,因此并不需要避免多个线程同时修改ticket,使用sellTicket方法也不需要设置为static。
-
-
测试:
public static void main(String[] args) throws InterruptedException {
//需求:实现三个窗口一起卖100张票
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
Thread t3 = new Thread(myRunnable);
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(myRunnable.getTicket());
}
- 结果: 也没有出现不同线程消费同一张票和库存超卖的现象。
1.4.3 锁lock
-
原理: 在上述过程中,用synchronized实现线程安全,本质上也是加了锁,只是我们看不见这个过程:进入synchronized修饰的代码块或者方法时,会先上锁,退出时再解锁,因此我们也可以用java的Lock接口来保证线程安全。
-
实现: 由于Lock是接口,无法实例化,因此我们可以用实现了Lock接口的ReentrantLock 类来实例化对象:
- MyThread类:
public class MyThread extends Thread{ static int ticket = 0; static Lock lock = new ReentrantLock();//实例化锁对象 @Override public void run() { while (true){ try { lock.lock();//上锁 if(ticket >= 100){ break; }else{ System.out.println(getName()+" sell ticket:"+ticket); ticket++; } } catch (Exception e) { throw new RuntimeException(e); } finally { //最终一定会执行finally中的方法,即使提前break lock.unlock();//解锁 } } } public int getTicket(){ return ticket; } }- 测试:
public static void main(String[] args) throws InterruptedException { //需求:实现三个窗口一起卖100张票 MyThread my1 = new MyThread(); MyThread my2 = new MyThread(); MyThread my3 = new MyThread(); my1.start(); my2.start(); my3.start(); //使得main方法能输出最终的票数 my1.join(); my2.join(); my3.join(); System.out.println(my1.getTicket()); } -
结果: 也没有出现不同线程消费同一张票和库存超卖的现象。
1.5 等待唤醒机制
1.5.1 实现方式一:生产者消费者模式
-
介绍: 生产者消费者模式是一个十分经典的多线程协作的模式。
-
实现思路:以消费者和厨师为例子,此时共有三个对象,分别是消费者,生产者(厨师)和 暂存产品的第三方(放食物的桌子),三者分别负责:
-
消费者:
-
判断桌子上是否有食物
-
如果没有,则等待生产者生产食物
-
如果有,则消费食物并唤醒生产者继续生产食物(吃完了)
-
-
生产者:
-
判断桌子上是否有食物
-
如果有,则等待消费者吃完
-
如果没有,则生产食物,并唤醒消费者吃食物(有食物了)
-
-
第三方:
-
用于指定线程运行(如通过本例中的foodFlag)
-
为多个线程提供共同的锁,实现wait() 与notify() 功能(如本例中的lock)
-
记录生产者与消费者需要的一些公共信息(如本例中的count)
-
-
-
代码实现:
-
Consumer类:
/** * 消费者:用于消费食物 */ public class Consumer extends Thread{ @Override public void run() { while (Desk.count > 0){//还能再吃食物,那就再吃 synchronized (Desk.lock){ if(Desk.foodFlag == 0){//无食物 try { Desk.lock.wait();//等待生产者生产食物 } catch (InterruptedException e) { throw new RuntimeException(e); } }else{//有食物 Desk.count--; System.out.println("消费者了食物,还能再吃:"+Desk.count+"份"); Desk.foodFlag=0;//改变桌子的状态 Desk.lock.notifyAll();//唤醒生产者,让生产者开始生产食物 } } } } } -
Producer类:
/** * 生产者:用于生产食物 */ public class Producer extends Thread{ @Override public void run() { while (Desk.count > 0 ){//消费者还能再吃食物,那就再生产 synchronized (Desk.lock){ if(Desk.foodFlag == 0){//没食物了,则生产食物 Desk.foodFlag = 1; System.out.println("生产者生产了食物"); Desk.lock.notifyAll();//唤醒消费者,让他来吃食物 }else{//此时桌子还有食物,等待消费者吃食物 try { Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } } -
Desk类:
/** * 生产者与消费者的第三方,用于暂存食物 */ public class Desk { //设置为int类型是方便后续拓展功能,如2、3分别代表允许第3、4个进程运行 public static int foodFlag = 0;//0表示无食物,1表示有食物 public static Object lock = new Object();//锁对象 public static int count = 10;//表示消费者最多能吃的食物个数 }
-
-
测试:
public static void main(String[] args) {
Consumer consumer = new Consumer();
Producer producer = new Producer();
producer.start();
consumer.start();
}
- 运行结果:
生产者生产了食物
消费者了食物,还能再吃:9份
生产者生产了食物
消费者了食物,还能再吃:8份
生产者生产了食物
消费者了食物,还能再吃:7份
生产者生产了食物
消费者了食物,还能再吃:6份
生产者生产了食物
消费者了食物,还能再吃:5份
生产者生产了食物
消费者了食物,还能再吃:4份
生产者生产了食物
消费者了食物,还能再吃:3份
生产者生产了食物
消费者了食物,还能再吃:2份
生产者生产了食物
消费者了食物,还能再吃:1份
生产者生产了食物
消费者了食物,还能再吃:0份
- 可以看到二者是交替运行的,且消费者吃不下之后,两线程都停止运行了。
1.5.2 练习:实现两线程交替打印01
-
Consumer类:
/** * 消费者:负责打印1 */ public class Consumer extends Thread{ @Override public void run() { while (true){ synchronized (Desk.lock){ if(Desk.flag == 0){//消费者只能打印1 try { Desk.lock.wait();//此时等待生产者打印完1 } catch (InterruptedException e) { throw new RuntimeException(e); } }else{//有食物 try { Thread.sleep(100);//打印得慢一点 } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("1"); Desk.flag = 0; Desk.lock.notifyAll();//唤醒生产者,轮到他打印"0" } } } } } -
Producer类:
/** * 生产者:负责打印0 */ public class Producer extends Thread{ @Override public void run() { while (true){//消费者还能再吃食物,那就再生产 synchronized (Desk.lock){ if(Desk.flag == 0){//此时开始打印0 Desk.flag = 1; System.out.println("0"); try { Thread.sleep(100);//打印得慢一点 } catch (InterruptedException e) { throw new RuntimeException(e); } Desk.lock.notifyAll();//唤醒消费者,让他来打印1 }else{//生产者只能打印0,此时等待消费者打印完1 try { Desk.lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } } -
Desk类:
public class Desk { public static int flag = 0;//0表示打印0,1表示打印1 public static Object lock = new Object();//锁对象 } -
运行:
public static void main(String[] args) { Consumer consumer = new Consumer(); Producer producer = new Producer(); producer.start(); consumer.start(); } -
运行结果: 持续、交替打印01。
1.5.2 实现方式二:阻塞队列
-
介绍: 该方式与生产者消费者模式类似,只是第三方变成了阻塞队列,并且此时产品可以放置多个。生产者将生产的产品放入阻塞队列,消费者从阻塞队列接收产品。当阻塞队列满时,生产者等待;当阻塞队列空时,消费者等待。
-
阻塞队列的两种实现方式:
-
ArrayBlockingQueue: 底层是数组,有界,初始化时需要指定数组大小。
-
LinkedBlockingQueue: 底层是链表,无界(但并不是真正的无界,最大值为int的最大值)。
-
-
代码实现:
-
Consumer类:
/** * 消费者:用于消费食物 */ public class Consumer extends Thread{ ArrayBlockingQueue<String> queue; public Consumer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){//还能再吃食物,那就再吃 try { Thread.sleep(100); System.out.println("消费者吃食物"); queue.take(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } -
Producer类:
/** * 生产者:用于生产食物 */ public class Producer extends Thread{ ArrayBlockingQueue<String> queue; public Producer(ArrayBlockingQueue<String> queue) { this.queue = queue; } @Override public void run() { while (true){ try { Thread.sleep(100); System.out.println("生产者生产了食物"); queue.put("食物"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } -
测试类:
public static void main(String[] args) { ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1); Consumer consumer = new Consumer(queue); Producer producer = new Producer(queue); producer.start(); consumer.start(); }
-
-
运行结果:
消费者吃食物 消费者吃食物 生产者生产了食物 消费者吃食物 生产者生产了食物 生产者生产了食物 消费者吃食物 消费者吃食物 生产者生产了食物 -
解释:
-
首先,读者可能会疑惑:“在上述两个类中分别加个put()和take()方法就可以了,那么简单吗?难道不用加锁或者用synchronized吗?“,是的,直接调用put()和take()方法就可以了,因为两方法的源码已经实现了加锁的过程:
-
ArrayBlockingQueue.take()源码
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(//与lock()类似,但是可中断地获取锁); try { while (count == 0)//队列为空,即无产品 notEmpty.await(); return dequeue();//消费产品 } finally { lock.unlock(); } } -
ArrayBlockingQueue.put()源码
public void put(E e) throws InterruptedException { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly();//与lock()类似,但是可中断地获取锁 try { while (count == items.length)//队列已满,即产品已满 notFull.await(); enqueue(e);//放入产品 } finally { lock.unlock(); } } -
可以发现,上述两个方法的操作与生产者消费者模式类似:
-
当进行生产(调用put方法)和消费(调用take方法)时,均进行了上锁,当操作结束均会释放锁,与synchronized功能一致;
-
对于消费者使用的take()方法,当队列为空时,消费者会等待;反之消费者会消费产品。
-
对于生产者使用的put()方法,当队列已满时,生产者会等待;反之生产者会生产产品并放入队列take。
-
-
-
其次,为了让生产者和消费者两个线程用的是同一个阻塞队列,需要在main方法中创建一个公共的阻塞队列,分别传入生产者与消费者的构造方法中(引用传递)。
-
读者还可能疑惑:”为什么输出结果中生产者与消费者不会交替出现呢?“。因为此时的打印操作是在put与take方法之外的,即在上锁或者synchronized之外的,因此打印操作不能实现线程同步,但是实际上生产者与消费者通过阻塞队列生产和消费产品的操作是同步的,只是我们看不见罢了。
-
1.6 综合练习
1.6.1 分发礼物
-
需求: 由两人同时发送100份礼物,当剩下的礼物小于10份时不再送出;利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来
-
代码实现:
-
MyThread类:
public class MyThread extends Thread{ private static int cnt = 100; private static Lock lock = new ReentrantLock(); public MyThread(String name) { super(name); } @Override public void run() { while (true){ try { lock.lock(); if(cnt < 10){ break; }else{ cnt--; System.out.println(getName()+"发送礼物"+",剩余礼物:"+cnt); } //Thread.sleep(1); //便于观察打印结果 } catch (Exception e) { throw new RuntimeException(e); }finally { lock.unlock(); } } } } -
测试:
public static void main(String[] args) { //需求:由两人同时发送100份礼物,当剩下的礼物小于10份时不再送出 //利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来 MyThread my1 = new MyThread("线程1"); MyThread my2 = new MyThread("线程2"); my1.start(); my2.start(); //输出结果: // 首位两条分别为:线程1发送礼物,剩余礼物:99 和 线程2发送礼物,剩余礼物:9 //且线程1和线程2随机发送礼物 }
-
1.6.2 发红包
-
需求: 将100元红包分成3份,一共有五个人去抢。打印最终结果
-
代码实现:
- MyThread类:
public class MyThread extends Thread{ private static double money = 100;//红包总金额 private static int cnt = 1;//表示第几个人抢红包 private double minNum = 0.01;//红包的最小金额 public MyThread(String name) { super(name); } @Override public void run() { while (true){ synchronized (MyThread.class){ double randomNum; if(cnt > 3){ System.out.println(getName()+"抢红包失败"); break; }else{ if(cnt == 3){//此时是最后一个红包 randomNum = money; }else{ Random random = new Random(); randomNum = random.nextDouble(money-minNum+1)+minNum; } System.out.println(getName()+"第"+cnt+"个抢到了红包,金额为:"+randomNum); money-=randomNum; cnt++; } } try { Thread.sleep(100);//避免一个线程由于执行迅速,将所有红包同时抢完 } catch (InterruptedException e) { throw new RuntimeException(e); } } } }- 测试:
public static void main(String[] args) { //需求:将100元红包分成3份,一共有五个人去抢。打印最终结果 MyThread m1 = new MyThread("线程1"); MyThread m2 = new MyThread("线程2"); MyThread m3 = new MyThread("线程3"); MyThread m4 = new MyThread("线程4"); MyThread m5 = new MyThread("线程5"); m1.start(); m2.start(); m3.start(); m4.start(); m5.start(); }- 结果:
线程1第1个抢到了红包,金额为:7.062189430902235 线程4第2个抢到了红包,金额为:5.080076826933999 线程5第3个抢到了红包,金额为:87.85773374216376 线程3抢红包失败 线程2抢红包失败 线程1抢红包失败 线程4抢红包失败 线程5抢红包失败
1.6.3 抽奖功能(基本实现)
-
需求: 有一个抽奖池,其中存放了奖励的金额,该抽奖池的奖项为{10,5,20,50,100,200,500,800,2,80,300,700}。创建两个抽奖箱,随机从奖池中获取奖项元素并打印在控制台上,格式为:“抽奖箱x产生了一个 xxx元大奖”
-
代码实现:
- MyThread类
public class MyThread extends Thread{ List<Integer> list; public MyThread(List<Integer> list) { this.list = list; } @Override public void run() { while (true){ synchronized (MyThread.class){ if(list.size() == 0){ break; }else{ Collections.shuffle(list); int money = list.remove(0); System.out.println(getName() + "产生了一个"+money+"元大奖"); } } try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }- 测试:
public static void main(String[] args) { //需求:有一个抽奖池,其中存放了奖励的金额,该抽奖池的奖项为{10,5,20,50,100,200,500,800,2,80,300,700} //创建两个抽奖箱,随机从奖池中获取奖项元素并打印在控制台上,格式为:“抽奖箱x产生了一个 xxx元大奖” List<Integer> list = new ArrayList<>(); Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700); MyThread mt1 = new MyThread(list); MyThread mt2 = new MyThread(list); mt1.setName("抽奖箱1"); mt2.setName("抽奖箱2"); mt1.start(); mt2.start(); }- 结果:
抽奖箱1产生了一个5元大奖 抽奖箱2产生了一个700元大奖 抽奖箱1产生了一个100元大奖 抽奖箱2产生了一个20元大奖 抽奖箱1产生了一个500元大奖 抽奖箱2产生了一个800元大奖 抽奖箱1产生了一个200元大奖 抽奖箱2产生了一个300元大奖 抽奖箱1产生了一个80元大奖 抽奖箱2产生了一个2元大奖 抽奖箱1产生了一个50元大奖 抽奖箱2产生了一个10元大奖
1.6.4 抽奖功能(多线程之间的比较)
-
需求: 在上一题的基础上进行改造。每次抽的过程中不打印,抽完一次性打印(随机),如:在此次抽奖过程中,抽奖箱1共产生了x个奖项,分别为…,总金额为xx元。
-
代码实现:
- MyThread类:
public class MyThread extends Thread{ private List<Integer> list; private int cnt = 0;//总共产生的奖项个数 private int amount = 0;//抽到的总金额 private static String maxThreadName; private static int maxAmount; public MyThread(List<Integer> list) { this.list = list; } @Override public void run() { List<Integer> myList = new ArrayList<>(); while (true){ synchronized (MyThread.class){ if(list.size() == 0){ int maxValue = 0; System.out.println(getName() + "共产生了"+cnt+"个奖项,分别为:"); for (int i=0;i<myList.size();i++){ maxValue = Math.max(maxValue,myList.get(i)); System.out.printf(myList.get(i)+" , "); } if(maxThreadName == null){ maxThreadName = getName(); maxAmount = maxValue; }else if(maxValue > maxAmount){ maxThreadName = getName(); maxAmount = maxValue; } System.out.println("最高金额为:"+maxValue+"元,总金额为:"+amount); break; }else{ cnt++; Collections.shuffle(list); int money = list.remove(0); myList.add(money); amount+=money; } } try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static void printMaxAmount(){ System.out.println(maxThreadName+"产生了最大奖项,该金额为"+maxAmount+"元"); } }- 测试:
public static void main(String[] args) throws InterruptedException { //需求:在上一题的基础上进行改造: //每次抽的过程中不打印,抽完一次性打印(随机) //如:在此次抽奖过程中,抽奖箱1共产生了x个奖项,分别为...,总金额为xx元 List<Integer> list = new ArrayList<>(); Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700); MyThread mt1 = new MyThread(list); MyThread mt2 = new MyThread(list); mt1.setName("抽奖箱1"); mt2.setName("抽奖箱2"); mt1.start(); mt2.start(); mt1.join(); mt2.join(); MyThread.printMaxAmount(); }- 结果:
抽奖箱2共产生了6个奖项,分别为: 50 , 800 , 80 , 100 , 2 , 700 , 最高金额为:800元,总金额为:1732 抽奖箱1共产生了6个奖项,分别为: 500 , 300 , 10 , 200 , 20 , 5 , 最高金额为:500元,总金额为:1035 抽奖箱2产生了最大奖项,该金额为800元- 备注: 若不想两个线程抽到的奖品数如此均匀,可以将sleep去掉。
1.7 线程池
为什么要用线程池:为了复用线程,避免每次执行一个任务都要创建销毁线程,造成资源和时间的浪费。
1.7.1 线程池的分类
-
线程数没有上限的线程池(实际上还是有上限的,上限为Integer.MAX_VALUE,但是由于二十多亿过于庞大,当创建完这么多数量之前,电脑就会先崩溃了,因此可视为没有上限)
-
线程数有上限的线程池(上限一般可以自己设定)
1.7.2 线程池的代码实现
-
利用Executors类(线程池的工具类)
-
步骤:
首先自定义myRunnable()类:
public class myRunnable implements Runnable{ @Override public void run() { for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } }-
创建线程池:
//没有上限的线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //有上限的线程池,此时指定线程数上限为3 ExecutorService executorService = Executors.newFixedThreadPool(3); -
提交任务:
//submit的参数可为Runnable或者Callable executorService.submit(new myRunnable()); -
所有任务都结束之后,关闭线程池:
executorService.close();
-
1.7.3 初次尝试
-
没有上限的线程池:
-
创建线程池:
ExecutorService executorService = Executors.newCachedThreadPool(); -
提交多个任务:
executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); -
结果(截取部分内容):
pool-1-thread-3:0 pool-1-thread-3:1 pool-1-thread-3:2 pool-1-thread-3:3 pool-1-thread-3:4 pool-1-thread-3:7 pool-1-thread-1:0 pool-1-thread-5:0 pool-1-thread-4:0 pool-1-thread-4:1 -
分析:此时发现线程池自动为我们创建了多个线程来执行任务(如thread-3,thread-1,thread-5,thread-4),且似乎没有上限。(若想让多个任务共同复用一个线程,可以在submit之间加上:Thread.sleep(100)
-
-
有上限的线程池:
-
创建线程池:
//指定线程池上限为3 ExecutorService executorService = Executors.newFixedThreadPool(3); -
执行多个任务(此时为5个,超过线程池上限):
executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); executorService.submit(new myRunnable()); -
结果(截取部分内容):
pool-1-thread-1:0 pool-1-thread-1:1 pool-1-thread-1:2 pool-1-thread-1:3 pool-1-thread-1:4 pool-1-thread-1:5 pool-1-thread-3:0 -
分析:发现始终都只是在复用thread-1、2、3,始终不超过thread-3。
-
补充:若想看到有限线程池排队的现象,可在第一个executorService.submit(new myRunnable())之前打断点,之后开启debug模式,此时关注executorService的两个参数:pool size和workQueue ,最开始时二者都为0,当开始执行到第一、二、三个submit时pool size依次为1,2,3,此时workQueue 仍为0,当执行到第四、五个submit时,若线程池的三个任务还没结束,则第四、五个线程仍需等待,且workQueue依次变为1、2 。
-
1.7.4 自定义线程池
-
上文使用的newCachedThreadPool()和newFixedThreadPool(3)的底层原理也是帮我们自定义一个线程池:
-
newCachedThreadPool():
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } -
newFixedThreadPool(3):
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
-
因此,想要自定义线程池可以new一个ThreadPoolExecutor,其有七个参数:
以饭店为例:

-
其中:
-
不能为空的参数为
workQueue、threadFactory、handler(即后三者),其他参数需满足数值合法性要求。在实际使用中,若未显式指定后两者,线程池会使用默认实现,但任务队列必须明确提供。 -
临时线程数=最大线程数-核心线程数。因此最大线程数应该≥核心线程数
-
临时线程和核心线程的关系:核心线程永远不会消失,只有当线程池消失的时候才会随着线程池消失而一起消失;临时线程 在达到规定的时间就会消失(空闲时间)。
-
任务调用的顺序:先调用核心线程,当任务数超过核心线程时,多余的任务会放在阻塞队列当中;当任务数又超过 核心线程数 + 阻塞队列长度 时,多余的任务才会被临时线程执行。即临时线程是最后执行的。
-
这里创建线程池的方式指的是线程工厂Executors.defaultThreadFactory(),只有这一种方式创建线程,其底层原理其实也是new 一个线程:
public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); }DefaultThreadFactory() { @SuppressWarnings("removal") SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } } -
线程拒绝策略主要有以下四种:

-
第一种策略的示例:
public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); threadPool.submit(new myRunnable()); Thread.sleep(200); threadPool.submit(new myRunnable()); Thread.sleep(200); threadPool.submit(new myRunnable()); Thread.sleep(200); threadPool.submit(new myRunnable()); } -
结果:之后运行两个线程来执行前两个任务,说明由于线程数不足,只能将第三个线程丢弃了。
-
-
