目录
一、线程安全
1.1synchronized的使用
死锁的四个必要条件:
1.2Java 标准库中的线程安全类
1.3volatile 关键字
1.4 wai和notify
二、多线程案例
2.1 单例模式(懒汉、饿汉模式)
2.2 线程安全的单例模式(!!!!安全的懒汉模式(重要))
2.3 阻塞队列(生产者-消费者模型)
2.4 定时器
三、线程池
操作系统中,由于线程的调度是随机性的(抢占式执行),由此带来了一些安全方面的问题。
线程不安全的原因:
- 1.线程之间的抢占式执行(无法确定顺序,具有随机性)
- 2.多个线程 修改 同一个变量(规避:一个线程只改一个变量)
- 3.原子性:操作只有一个步骤,多步骤互相排斥,加锁即保证了原子性(同步互斥,不能同时进行两个操作,A作业时B不能进行打断)
- 4.内存可见性(和原子性类似):一个线程对共享变量值的修改,能够及时地被其他线程看到.(一个线程频繁读,另一个频繁写数据,则会存在可见性问题)
- 指令重排序:代码执行顺序也会影响线程安全
给方法直接加synchronized关键字进行加锁,进入此方法自动加锁,离开方法,自动解锁,便可保证下面代码自增结果始终为100000
当一个线程加锁成功,其他线程尝试枷锁便会出发阻塞等待(BLOCKED),持续到占用所得线程释放锁为止。
class Counter {
public int count;
synchronized public void increase() {
count++;
}
}
public class thread {
private static Counter counter = new Counter(); //创建一个实例counter
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join(); //进行一个线程等待(俩线程执行完毕,再打印)
t2.join();
System.out.println(counter.count); //100000
}
}
执行计数器++,的步骤:
synchronized可保证原子性、可见性、有序但不能禁止指令重排;
volatile保证可见性、禁止指令重排但不保证原子性
此处的有序并不代表指令重排序
Java中每个类都是继承自Object,new出来的实例包含了自己安排的属性,也包含了“对象头”,对象的一些元数据。(加锁操作也就是给对象头里设置标志位)。两个线程针对同一个变量进行加锁才具有竞争,若是不同变量则不需竞争
监视器锁:monitor lock
synchronized public void increase() {
count++;
}
public void increase() {
synchronized(this) { //锁对象需要手动指定
count++;
}
}
public static void func() {
synchronized (Counter.class) { //针对类对象(Counter是类名)进行加锁
}
}
连续进行两次锁,则会造成死锁的状态:
synchronized public void increase() {
synchronized(this) { //锁对象需要手动指定
count++;
}
}
为了解决死锁的问题,引入了可重入锁(记录当前锁被占用的线程,记录一个加锁次数)
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
死锁的四个必要条件:
- 互斥使用:一个锁被占用后,其他线程不能占用(原子性)
- 不可抢占:一个所被占用后,其他线程不能抢走
- 请求和保持:一个线程占据多把锁之后,除非进行显式的释放锁,否则锁只能被该线程持有
- 环路等待:等待关系(实际中,要尽量避免出现循环等待,给固定的顺序,就可避免死锁)
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:
线程安全的 :
volatile 修饰的变量, 能够保证 "内存可见性".。不会引起线程阻塞。
代码在写入 volatile 修饰的变量的时候:
代码在读取 volatile 修饰的变量的时候:
直接访问工作内存(实际是 CPU 的寄存器或 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况。加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.(缓存是介于CPU和寄存器之间的,空间比寄存器大,速度比CPU快)
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
t1 读的是自己工作内存中的内容;当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序具有随机性。
wai和notify都是 Object 对象
wait 做的事情:
- 释放当前的锁
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
public class waitnotifyDemo {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) { //加入锁,object为锁对象。则不会出现IllegalMonitorStateException非法锁异常的结果
System.out.println("wait等待前");
object.wait();
System.out.println("wait等待后");
}
}
}
notify 方法是唤醒等待的线程:
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
使用notify()方法唤醒线程:
public class waitnotifyDemo {
private static Object locker = new Object(); //创建一个锁对象
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
//进行wait
synchronized (locker) {
try {
System.out.println("wait 之前");
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 之后");
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() ->{
//进行notify
synchronized (locker) {
System.out.println("notify 之前");
locker.notify();
System.out.println("notify 之后 ");
}
});
t2.start();
}
}
notify 和 notifyAll:
notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着;notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
wait 和 sleep 的对比(面试题):
wait 和 sleep 完全是没有可比性的,因为wait是用于线程之间的通信的,sleep是让线程阻
塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
- 1. wait 需要搭配 synchronized 使用. sleep 不需要.
- 2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.
单例模式:设计模式(有固定的套路,是在基础框架上进行优化)之一,可保证某个类在程序中只存在一个实例(不会创建多个实例)。这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
(1)饿汉模式:类加载的同时, 创建实例.
Java程序中,一个类对象只存在一份,故而保证了类的static 成员也只有一个
类对象 != 对象
类对象(类名.class):就是.class文件被JVM加载到内存之后表现出来的形式
类:相当于实例的模板,类中可以包含很多个对象(实例).
对象(实例):new对象
class Singleton {
// 1.使用static创建一个实例,并立即进行实例化New 操作
// instance 对应的实例是该类唯一的实例
private static Singleton instance = new Singleton(); //创建一个类成员instance(类加载的时候就创建实例:饿汉模式)
// 2.把构造方法设为私有的,防止其他操作中不小心new 到singleton(饿汉模式只能new一次)
private Singleton() {}
//提供一个方法确保外面能拿到唯一的实例(私有属性提供公开接口则外部可以进行调用)
public static Singleton getInstance() {
return instance;
}
}
public class singleTon {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
// Singleton instance2 = new Singleton(); //不能进行编译,因为static修饰的类对象 只能有唯一的一个实例
}
}
(2)懒汉模式:类加载的时候不创建实例. 第一次使用的时候才创建实例.(调用getInstance才创建实例);此模式中,getInstance只读取了变量的内容,若多个线程只读取同一变量不进行修改,则线程是安全的。
//懒汉模式:非立即实例化,需要时再实例化
class Singleton2 {
private static Singleton2 instance = null; //不用实例化
private Singleton2() {} //构造方法私有
public static Singleton2 getInstance() { //提供接口供外部调用
if (instance == null) {
instance = new Singleton2(); //需要使用实例时再创建实例
}
return instance;
}
}
public class singtonDemo2 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
真正需要解决问题的是需要实现线程安全的模式。在上述模式中,懒汉模式涉及到读写操作,存在安全问题。加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.
线程安全版本的单例模式:
- 1.饿汉模式:类加载时就创建(线程安全的,只涉及读操作)
- 2.懒汉模式:首次调用实例才创建(非安全,体现在首次调用,若多个线程同时调用则不安全,需加锁,并只在首次调用加锁)
只需给懒汉模式加锁即可保证线程安全:
线程未初始化之前,多线程调用getInstance涉及到读写操作(不安全),而初始化之后(instance != null,if 条件不成立 )getInstance操作只剩下读操作(不存在线程安全的问题,故不需要再加锁),故加锁之前应该有前提条件
同时给实例instance加入volatile保证内存可见性(避免多次读取时,数据的错误),
//懒汉模式:非立即实例化,需要时再实例化
class Singleton2 {
private static volatile Singleton2 instance = null; //不用实例化,但要加入内存可见性
private Singleton2() {}
public static Singleton2 getInstance() {
if (instance == null) { //在未初始化之前进行加锁,若初始化了,只需进行读操作(不存在安全问题)
synchronized (Singleton2.class) { //指定锁对象(类名.class)
if (instance == null) {
instance = new Singleton2(); //需要使用实例时再创建实例
}
}
}
return instance; //返回instance(也即是读操作,不进行修改)
}
}
public class singtonDemo2 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
当多线程首次调用 getInstance, 发现 instance 为 null, 于是继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作;当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
阻塞队列(线程安全)是一种特殊的队列. 遵守 "先进先出" 的原则.
阻塞队列的一个典型应用场景就是 "生产者消费者模型"(典型的开发模型).
开发中的阻塞队列不是一个简单的数据结构,而是一个/一组专门的服务器程序,它在阻塞队列的基础上提供更多的功能(数据持久化存储、支持多个数据通道、多节点容灾冗余备份、管理面板、方便配置参数...),也称为“消息队列”。
生产者消费者模型:通过一个容器(中间场所)来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.阻塞队列只是单纯的存储数据,不参与计算,因此抗压能力较强
(1)Java标准库中的阻塞队列
public static void main(String[] args) throws InterruptedException {
BlockingDeque queue = new LinkedBlockingDeque<>();
queue.put("hello");
String str = queue.take();
}
(2)生产者消费者模型代码实现(队列+线程安全+阻塞)
队列可以基于数组和链表实现,此处基于数组实现更为简单(队尾进 ,对头出)
实现循环队列??如何区分空?还是满?:
1.浪费一个空间
插入:tail+1=head:满
删除:tail=head:空
2. 创建一个变量size,记录当前元素个数(常用方法)
出:size-- ,size=0时为空
入:size++,size=array.length满
代码实现:先实现出入功能,再添加锁、最后实现阻塞(入put的阻塞是队列满了,出take的阻塞是队列为空),针对那个对象,就对那个对象wait
import java.util.Queue;
class blockQueue {
private int[] data = new int[1000];
private int size = 0; //有效元素个数
private int head = 0; //头下标
private int tail = 0; //尾下标
private Object locker = new Object(); // 创建一个专门的锁对象
public void put(int value) throws InterruptedException { //入队列
synchronized (locker) {
if(size == data.length) {
locker.wait(); //,针对那个对象,就对那个对象wait(此处的等待由take中唤醒,也即是出队列之后,就可放入元素)
// return;
}
data[tail] = value;
tail++;
if (tail >=data.length) { // 当达到数组的容量进行循环操作(从头开始)
tail = 0;
}
size++;
locker.notify(); //唤醒take中的阻塞等待
}
}
public Integer take() throws InterruptedException { //出队列
synchronized (locker) { //保证线程安全(此处锁对象用this或者locker都可以)
if (size == 0) {
locker.wait(); //针对那个对象,就对那个对象wait
// return null; //队列为空,返回一个非法值(int返回类型不能是null)
}
int ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
locker.notify(); //唤醒put中等待的对象this
return ret;
}
}
}
public class myBlockQueue {
public static void main(String[] args) throws InterruptedException {
//实现简单的生产者消费者模型
blockQueue queue = new blockQueue();
Thread producer = new Thread(() ->{
int num = 0;
while (true) {
System.out.println("生产数量:"+num);
try {
queue.put(num);
num++;
Thread.sleep(1000); //当生产者速度慢了,消费者也得跟着生产者的速度进行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(() ->{
while (true) {
int num = 0;
try {
num = queue.take();
System.out.println("消费数量:"+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
(1)标准库中的定时器
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule ( 包含两个参数. 第一个参数:指定即将要执行的任务代码, 第二个参数:指定时间 /单位:ms )
Timer 内部有专门的线程,负责执行注册的任务:
import java.util.Timer;
import java.util.TimerTask;
public class timeDemo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
System.out.println("main");
}
}
(2)定时器的构成:
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.
import java.util.concurrent.PriorityBlockingQueue;
class MyTask { //创建一个任务类MyTask(定义好任务和时间,构造函数、执行方法run、和获取时间的公开接口)
private Runnable runnable; //指代任务
private long time; //时间
public MyTask(Runnable runnable, long after) { //构造函数:快捷键alt+insert
this.runnable = runnable;
this.time = System.currentTimeMillis() + after; //设置一个绝对时间,超过此时间就可执行任务
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
}
//创建一个定时器类 ,内部包含一个带优先级的阻塞队列queue,和schedule方法,用于存放任务和时间
// 以及 类的构造函数 (创建线程,如何执行任务)
class myTimer {
//定时器内部需要存放多个任务
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay) { // schedule的主要任务是往阻塞队列中插入元素
MyTask task = new MyTask(runnable,delay); //task描述一个任务,包含任务内容和时间
queue.put(task);
}
public myTimer() { //构造函数
Thread t = new Thread(() ->{ //创建一个线程
while (true) {
try {
MyTask task = queue.take(); //1.取队首元素
long curTime = System.currentTimeMillis();
if(curTime < task.getTime()) { //时间未到.把任务塞回到阻塞队列
queue.put(task);
}else {
task.run(); // 时间到了,执行任务
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class classdemo {
public static void main(String[] args) {
myTimer mytimer = new myTimer();
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
System.out.println("hello main");
}
}
(1)问题1:以上代码中,MyTask没有指定比较规则,导致报错:
优先级队列要进行比较Comparable
改进后的代码:
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable{ //实现Comparable接口
private Runnable runnable; //指代任务
private long time; //时间
public MyTask(Runnable runnable, long after) { //构造函数:快捷键alt+insert
this.runnable = runnable;
this.time = System.currentTimeMillis() + after; //设置一个绝对时间,超过此时间就可执行任务
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) { //添加比较方法即可
return (int) (this.time - o.time); //时间小的在前
}
}
(2)问题2:循环若不加任何限制,执行会非常快:只需指定一下等待时间即可解决wait(time),time时间到达后会自动唤醒。
比如第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队
首元素几万次. 而当前距离任务执行的时间还有很久呢.(造成资源浪费,没必要)
此处指定等待时间,只能用wait(可被唤醒),sleep不能被中途唤醒;同时,修改 MyTimer 的 schedule 方法, 每次有新任务(马上执行)到来时唤醒一下 worker 线程.(synchronized代码块)
class myTimer {
//定时器内部需要存放多个任务
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay) { // schedule的主要任务是往阻塞队列中插入元素
MyTask task = new MyTask(runnable,delay); //task描述一个任务,包含任务内容和时间
queue.put(task);
//每次插入任务成功后,都唤醒扫描线程一下,检查队首的任务是否要执行了(若未到,则计算等待时间)
synchronized (locker) {
locker.notify();
}
}
private Object locker = new Object();
public myTimer() { //构造函数
Thread t = new Thread(() ->{ //创建一个线程
while (true) {
try {
MyTask task = queue.take(); //1.取队首元素
long curTime = System.currentTimeMillis();
if(curTime < task.getTime()) { //时间未到.把任务塞回到阻塞队列
queue.put(task);
synchronized (locker) { //指定一个等待时间,时间到达后自动唤醒
locker.wait(task.getTime() -curTime);
}
}else {
task.run(); // 时间到了,执行任务
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
整体代码:
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable{ //创建一个任务类MyTask(定义好任务和时间,构造函数、执行方法run、和获取时间的公开接口)
private Runnable runnable; //指代任务
private long time; //时间
public MyTask(Runnable runnable, long after) { //构造函数:快捷键alt+insert
this.runnable = runnable;
this.time = System.currentTimeMillis() + after; //设置一个绝对时间,超过此时间就可执行任务
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (this.time - o.time); //时间小的在前
}
}
//创建一个定时器类 ,内部包含一个带优先级的阻塞队列queue,和schedule方法,用于存放任务和时间
// 以及 类的构造函数 (创建线程,如何执行任务)
class myTimer {
//定时器内部需要存放多个任务
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay) { // schedule的主要任务是往阻塞队列中插入元素
MyTask task = new MyTask(runnable,delay); //task描述一个任务,包含任务内容和时间
queue.put(task);
//每次插入任务成功后,都唤醒扫描线程一下,检查队首的任务是否要执行了(若未到,则计算等待时间)
synchronized (locker) {
locker.notify();
}
}
private Object locker = new Object();
public myTimer() { //构造函数
Thread t = new Thread(() ->{ //创建一个线程
while (true) {
try {
MyTask task = queue.take(); //1.取队首元素
long curTime = System.currentTimeMillis();
if(curTime < task.getTime()) { //时间未到.把任务塞回到阻塞队列
queue.put(task);
// 此处synchronized代码块是避免盲等,造成资源浪费
synchronized (locker) { //指定一个等待时间,时间到达后自动唤醒
locker.wait(task.getTime() -curTime);
}
}else {
task.run(); // 时间到了,执行任务
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class classdemo {
public static void main(String[] args) {
myTimer mytimer = new myTimer();
mytimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello timer");
}
},3000);
System.out.println("hello main");
}
}
把线程提前创建好,放入池子(用户态实现),需要时直接取就不必再申请了,而线程用完之后不用销毁,放回池子便于下次继续使用。线程池最大的好处就是减少每次启动、销毁线程的损耗。
创建销毁涉及用户态和内核态的切换(切换到内核态,创建出对应的PCB);若只是在池里的操作,则只是在用户态完成操作(更高效)
自己写的代码,一般都是在应用程序中应用,常称为”用户态“代码。纯用户态的操作效率比内核态处理的操作效率更高。(进入和内核态则不可控,效率受到一定的影响)
(1)标准库中的线程池
关于线程池的常用参数:
线程池中线程 的个数如何设置????(应通过性能测试,找到合适的值 且CPU占用也合理的平衡点)
线程越多,整体的运行速度越快,但CPU占用也越高(反之CPU占用越低),但是平时应用过程应保持CPU占用留有冗余空间,以应对突发情况
Executors 本质上是 ThreadPoolExecutor 类的封装.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class threadpoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10); //创建一个固定线程数目的线程池,指定线程个数
// Executors.newCachedThreadPool(); //创建一个会自动扩容的线程池
// Executors.newSingleThreadExecutor(); //创建只有一个线程的线程池
// Executors.newScheduledThreadPool(); //创建一个带有定时器功能的线程池
for (int i = 0; i < 50; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello ThreadPool");
}
});
}
}
}
(2)实现线程池
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
class MyThreadPool {
//描述一个任务直接使用Runnable
// 使用一个数据结构队列queue来组织若干个任务
private BlockingDeque queue = new LinkedBlockingDeque<>();
static class worker extends Thread { //描述一个工作线程,其任务是从任务队列中取任务并执行
private BlockingDeque queue = null;
public worker (BlockingDeque queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
Runnable runnable = queue.take(); //获取任务
runnable.run(); //执行任务
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private List workers = new ArrayList<>(); //创建一个数据结构组织若干个线程
public MyThreadPool(int n) { //在构造方法中创建若干个线程,放到上述数组workers中
for (int i = 0; i < n; i++) {
worker worker = new worker(queue);
worker.start();
workers.add(worker);
}
}
public void submit(Runnable runnable) { //创建一个方法,允许程序员往线程池放任务
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class threadpoolDemo1 {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 50; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
}