一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码。
首先, “并发编程” 成为 “刚需”.
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
例1:
package thread;
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello thread!");
}
}
public class Demo1 {
public static void main(String[] args) {
// 创建一个线程
// Java 中创建线程,离不开一个关键的类,thread
// 一种比较朴素的创建线程的方式,是写一个子类,基础thread,重写其中的 run 方法
Thread t = new MyThread();
t.start();
System.out.println("hello main!");
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello thread!");
}
}
Thread t = new MyThread();
t.start(); // 线程开始运行
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello thread!");
}
}
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
t.start(); // 线程开始运行
匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
例2:
package thread;
public class Demo6 {
// 1. 单个线程,串行的,完成 20 亿次自增.
// 2. 两个线程,并行的,完成 20 亿次自增.
private static final long COUNT = 20_0000_0000;
private static void serial() {
// 需要把方法执行的时间给记录下来
// 记录当前的毫秒级时间戳
long beg = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
long end = System.currentTimeMillis();
System.out.println("单个线程消耗的时间:" + (end - beg) + "ms");
}
private static void concurrency(){
long beg = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
int a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2 = new Thread(() -> {
int a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
}catch (InterruptedException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("两个线程消耗的时间:" + (end - beg) + "ms");
}
public static void main(String[] args) {
concurrency();
serial();
}
}
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
方法 | 说明 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
创建 Thread 实例,并没有真的在操作系统内核里创建出线程;
调用 start() 方法,线程才真正独立去执行了。
注意:start() 和 run() 的区别!
例3:
package thread;
public class Demo9 {
// 用一个布尔变量表示线程是否结束
// 这个变量是一个成员变量,而不是局部变量
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isQuit) {
System.out.println("线程运行中....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束!");
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出");
isQuit = true;
}
}
例4:
package thread;
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
// [1] 立即退出
// break;
System.out.println("新线程即将退出!");
// [2] 稍后退出,此处的 sleep 可以换成任意的用来收尾工作的代码
// try {
// Thread.sleep(3000);
// } catch (InterruptedException interruptedException) {
// interruptedException.printStackTrace();
// }
// [3] 不退出!啥都不做,就相当于忽略了异常
}
}
System.out.println("新线程已退出!");
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出!");
t.interrupt();
}
}
有时候我们需要控制一个线程结束,再执行其他工作,就需要一个等待线程结束的方法:join().
例5:
package thread;
public class Demo11 {
private static Thread t1 = null;
private static Thread t2 = null;
public static void main(String[] args) throws InterruptedException {
System.out.println("main begin");
t1=new Thread(()->{
System.out.println("t1 begin");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
});
t1.start();
t2 =new Thread(()->{
System.out.println("t2 begin");
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 end");
});
t2.start();
t2.join();
System.out.println("main end");
}
}
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
join 的作用:
如果是继承 Thread 然后重新 run 方法,可以直接在 run 中使用 this 得到线程的实例;
但如果是 Runnable 或者 lambda,this 就不行了(指向 Thread 实例);
需要使用 Thread currentThread().
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
之前我们也用到过休眠线程的方法 sleep();
注意:sleep(1000):休眠时间不小于 1000 ms,实际时间大于 1000 ms,误差在 10 ms 以下。
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
枚举遍历线程的所有状态:
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
例6:sleep
package thread;
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 在 start 之前获取,获取到的是线程还未创建的状态
System.out.println(t.getState());
t.start();
// 在 start 之后 sleep 之前,线程正在 CPU 上运行
System.out.println(t.getState());
Thread.sleep(500);
// 在 sleep 之后获取,线程通过 sleep 进入阻塞
System.out.println(t.getState());
t.join();
// 在 join 之后获取,获取到的是线程已经结束后的状态
System.out.println(t.getState());
}
}
例7:yield
yield 大公无私,让出 CPU;不改变线程的状态, 但是会重新去排队;相当于 sleep(0).
package thread;
public class Demo14 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
}
}
可以明显的看到:使用 yield 时, 张三的数量远远少于李四。
例8:
package thread;
// 创建两个线程,让这两个线程同时并发的对一个变量,自增 5w 次,最终预期能够一共自增 10w次
class Counter {
// 用来保存计数的变量
public int count;
public void increase() {
count++;
}
}
public class Demo14 {
// 这个实例用来进行累加
public static Counter counter = new Counter();
public static void main(String[] args) {
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();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count: " + counter.count);
}
}
运行后发现,每次运行得到的结果都不同,是一个 5w 到 10 w之间的随机数,那么这是为什么呢?
count++ 对于操作系统来讲,CPU 会先从内存中读取 数据,然后在 CPU 中完成 运算,最后再把寄存器中的 数据 写到内存中.
一共三步,如果多个线程同时执行上述三步操作,执行顺序就会出现多种排列组合,只有当 二者互不干扰,连续完整执行上述三步操作时,才会达到我们想要的结果.
如果所有指令全部达到要求,则结果为 10w;如果所有指令均没有达到要求,则结果为 5w;
但实际上由于线程执行的不确定性,不确定有多少次指令达到要求,所以结果为 5w~10w.
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
这个也是造成线程不安全的万恶之源,对于我们程序员来说,也无能为力!
注意,一定是 多个线程 修改 同一个 变量,才会造成线程不安全!
这个比如刚才举的例子,t1 和 t2 两个线程都对 count 进行修改,就会造成线程不安全。这类问题,我们在写代码的时候就可以进行针对控制,可以通过调整程序的设计,进行规避。但不是所有的场景都可以规避!!!
我们把不可拆分的最小单位称为原子
那么其他操作可访问该操作的,就称为不具备原子性;如果给该操作加上一把锁,使其变为不可访问,那就是原子的;有时也把这个现象叫做同步互斥,表示操作是互相排斥的.
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
例如:
通过 = 来修改,= 只对应一道指令,是原子的;
通过 ++ 来修改,++ 对应三道指令,不是原子的.
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
一个线程修改,一个线程读取,就容易因为可见性,引发问题.
例如:
线程1 进行反复的读 和判断,
如果在正常情况下,线程1 在读和判断,线程2 突然写了一下,写完以后,线程1 就能立即读到内存的变换,从而让判断出现变换;
但是在程序运行过程中,可能会涉及一个操作 “优化”.比如:线程1 优化后,只判断,不读了,此时线程2 发生改变时,线程1 感受不到了.
例9:
package thread;
import java.util.Scanner;
import java.util.concurrent.Callable;
public class Demo16 {
// 写一个内部类,此时这个内部类处在 Demo16 的内部,和刚才那个 Counter 不是一个作用域了
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) {
// 执行循环,但是此处循环啥都不做
}
System.out.println("t1 finish");
});
t1.start();
Thread t2 = new Thread(() -> {
// 让用户输入一个数字,赋值给 flag
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
counter.flag = scanner.nextInt();
});
t2.start();
}
}
t1 的工作(反复执行):
- LOAD:读内存的数据到 CPU 寄存器
- TEST:检查 CPU 寄存器的值是否和预期的一样
读内存比读 CPU 寄存器慢几千倍,上万倍
即 t1 的操作注意慢在 LOAD 上.
对于 LOAD,每次进行 LOAD 的结果又没有变化,就进行了 “优化”.
“优化” 后,相当于只从进行了一次 LOAD,然后反复进行 TEST 操作
此时,t2 线程对 flag 进行修改,但是 t1 不在进行 LOAD 操作,就看不见内存中的修改.
这就是内存可见性问题。
“优化”:系统在执行正确的前提下,做出变换使性能更优.
在单线程环境下:没有问题
在多线程环境下:由于多线程环境过于复杂,编译器 / JVM / 操作系统进行 “优化” 时,可能产生误判.
针对这个问题,java 引入了 volatile 关键字,让程序员手动的禁止编译器对某个变量进行 “优化”.
指令重排序,也是 操作系统 / 编译器 / JVM 优化操作!!!
指令重排序就是 调整顺序 达到了 加快速度的效果!
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
例如:Test t = new Test();
1.创造内存空间
2.在这个内存空间上构造一个对象
3.把这个内存的引用赋给 t
其中 2 和 3 的顺序是可以调换的:
在单线程下,调换顺序,无影响;
在多线程下,另一个线程尝试读取 t 的引用,
如果按照 2,3,第二个线程读到 t 为 非空 的时候,t 一定是一个有效的对象;
如果按照 3,2,第二个线程读到 t 为 非空 的时候,t 可能是一个无效的对象;
例10:
package thread;
class Counter1 {
// 用来保存计数的变量
public int count;
public synchronized void increase() {
count++;
}
}
public class Demo15 {
public static Counter1 counter1 = new Counter1();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter1.increase();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count: " + counter1.count);
}
}
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
synchronized 的工作过程:
所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 “把自己锁死”
一个线程没有释放锁, 然后又尝试再次加锁!然后无法进行锁的操作,就会造成 死锁。这样的锁称为 不可重入锁.
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.
在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
public class SynchronizedDemo {
public synchronized void methond() {
}
}
public class SynchronizedDemo {
public synchronized static void method() {
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
注意: 两个线程竞争同一把锁, 才会产生阻塞等待.
package thread;
public class Demo15 {
public static Object Locker1 = new Object();
public static Object Locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (Locker1) {
System.out.println("t1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 finish");
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (Locker2) {
System.out.println("t2 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 finish");
}
});
t2.start();
}
}
package thread;
public class Demo15 {
public static Object Locker1 = new Object();
public static Object Locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (Locker1) {
System.out.println("t1 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 finish");
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (Locker1) {
System.out.println("t2 start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 finish");
}
});
t2.start();
}
}
volatile 修饰的变量, 能够保证 “内存可见性”.
代码在写入 volatile 修饰的变量的时候,
代码在读取 volatile 修饰的变量的时候,
对于 例9 中提到的内存可见性引发的 线程不安全问题,通过 volatile 关键字可以进行解决.
例13:
package thread;
import java.util.Scanner;
import java.util.concurrent.Callable;
public class Demo16 {
// 写一个内部类,此时这个内部类处在 Demo16 的内部,和刚才那个 Counter 不是一个作用域了
static class Counter {
volatile public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// 执行循环,但是此处循环啥都不做
// 加了sleep 以后,循环速度慢了,读内存操作就不频繁了,就不会触发 “优化”
/*try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
System.out.println("t1 finish");
});
t1.start();
Thread t2 = new Thread(() -> {
// 让用户输入一个数字,赋值给 flag
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
counter.flag = scanner.nextInt();
});
t2.start();
}
}
volatile 和 synchronized 有着本质的区别.
因为 volatile 可以防止操作系统进行 “优化”,而指令重排序的本质也就是 “优化”,所以 volatile 也可以解决指令重排序问题,这里就不展开讲了。
wait 做的事情:
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
调用 wait 的对象和 synchronized 里使用的锁对象是一个对象,还得和调用 notify 的对象是一个对象.
wait 结束等待的条件:
例14:wait
package thread;
public class Demo17 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized(object){
System.out.println("wait 之前");
object.wait();
System.out.println("wait 之后");
}
}
}
这样会出现一种情况,程序一直在等待,不能继续执行了,所以我们需要唤醒等待,就用到了 notify().
notify 方法是唤醒等待的线程.:
例15:
package thread;
import java.util.Scanner;
// 创建两个线程,一个线程调用 wait,一个线程调用 notify
public class Demo18 {
// 这个对象用来作为锁对象
public static Object Locker = new Object();
public static void main(String[] args) {
// 用来等待
Thread waitTask = new Thread(() -> {
synchronized (Locker) {
try {
System.out.println("wait 开始");
Locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
waitTask.start();
// 创建一个用来通知、唤醒的线程
Thread notifyTask = new Thread(() -> {
// 让用户来控制,用户输入个内容后,再执行通知
Scanner scanner = new Scanner(System.in);
System.out.println("输入任意内容,开始通知:");
// next 会阻塞,直到用户真正输入内容以后
scanner.next();
synchronized (Locker) {
System.out.println("notify 开始");
Locker.notify();
System.out.println("notify 结束");
}
});
notifyTask.start();
}
}
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
注意: 虽然是同时唤醒 多 个线程, 但是这 多 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
单例模式是校招中最常考的设计模式之一.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
程序启动,则立即创建实例
例16:饿汉模式
// 单列,饿汉的方式
class Singleton {
// static:静态,实际效果,和 字面含义 没有任何关联关系
// 实际的含义:类属性 / 类方法
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
// 构造方法设为私有!其他的类就不能 new Singleton()
private Singleton() {
}
}
程序启动,等到真正使用的时候,再创建实例.
例17:懒汉模式
// 懒汉模式的实现
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
// 首次调用 getinstance 才会创建实例
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
上面的懒汉模式的实现是线程不安全的.
例18:懒汉模式(多线程)
// 懒汉模式(多线程)的实现
class SingletonLazy {
// 为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
// 当线程已经安全以后,再不再加锁了
if (instance == null) {
// 加锁, 双重if,降低锁竞争的频率
synchronized (SingletonLazy.class) {
// 拿到锁以后,再进一步确认,是否要初始化
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
无锁队列:也是一种线程安全的队列,实现内部没有使用锁,更加高校,但是同时消耗更多的 CPU 资源.
消息队列:在队列中涵盖多种不同 “类型” 的元素.取元素的时候可以按照某个类型来去,做到针对该类型的 “先进先出”.
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间没有交流,而是通过阻塞队列来进行,生产者生产完毕直接给阻塞队列,消费者直接通过阻塞队列获取,两者不见面.
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
例19:生产者消费者模型
package thread;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo20 {
public static void main(String[] args) {
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(() -> {
int n = 0;
while (true) {
try {
System.out.println("生产元素:" + n);
queue.put(n);
n++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
例20:阻塞队列实现
package thread;
// 自己模拟实现一个阻塞队列.
// 基于数组的方式来实现队列.
// 提供两个核心方法:
// 1. put 入队列
// 2. take 出队列
class MylockingQueue {
// 假定最大是 1000 个元素,当然也可以设定成可配置的
private int[] items = new int[1000];
// 队首的位置
private int head = 0;
// 队尾的位置
private int tail = 0;
// 队列的元素个数
volatile private int size = 0;
// 入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length) {
// 队列已满,就等待
this.wait();
}
items[tail] = value;
tail++;
if (tail == items.length) {
// 注意如果 tail 达到数组末尾,就需要从头开始
tail = 0;
}
size++;
// 即使没有线程在等待,多调用几次 notify 也没有影响
this.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
int ret = 0;
synchronized (this) {
while (size == 0) {
// 队列为空,就等待
this.wait();
}
ret = items[head];
head++;
if (head == items.length) {
head = 0;
}
size--;
this.notify();
}
return ret;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
MylockingQueue queue = new MylockingQueue();
Thread customer = new Thread(() -> {
while (true) {
int value = 0;
try {
value = queue.take();
System.out.println("消费:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer = new Thread(() -> {
int value = 0;
while (true) {
try {
queue.put(value);
System.out.println("生产:" + value);
value++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
小知识:if 判断 和 取余% 的区别
// if判断
if (tail == items.length) {
// 注意如果 tail 达到数组末尾,就需要从头开始
tail = 0;
}
// 取余%
tail = tail % items.length;
两者的效果是一样的,只是效率上有所差距。
% 带来的问题:
类似于 “闹钟”.
例21:标准库中的定时器
package thread;
import java.util.Timer;
import java.util.TimerTask;
public class Demo22 {
public static void main(String[] args) {
// java.util 里的一个组件
Timer timer= new Timer();
// schedule 这个方法的效果就是 “安排一个任务”
// 不是立即执行,而是 3000 ms 之后再执行
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("这是要安排一个任务");
}
},3000);
}
}
想要实现定时器,首先得有一个定时器类,然后还要有要执行的任务类。
// 通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
// 任务要干什么
private Runnable command;
// 任务什么时候干
private long time;
public MyTask(Runnable command, long after) {
this.command = command;
// 此处记录的时间是一个绝对的时间戳. 不是 “多长时间之后执行”
this.time = System.currentTimeMillis() + after;
}
// 执行任务的方法,直接在内部调用 Runnable 的 run 即可.
public void run() {
command.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
// 希望时间小的在前面,时间大的在后面
// 这里谁减谁 才能达到时间小的在前
// 写代码验证一下
return (int) (this.time - o.time);
}
}
// 实现定时器类
class MyTimer {
// 使用一个优先级队列来保存若干个任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
// command:要执行的任务是什么
// after:多长时间之后执行这个任务
public void schedule(Runnable command, long after) {
MyTask myTask = new MyTask(command, after);
queue.put(myTask);
}
}
// 实现定时器类
class MyTimer {
// 省略前面代码
// ...
public MyTimer() {
// 在这里启动一个线程.
Thread t = new Thread(() -> {
while (true) {
// 循环过程中,就不断的尝试从队列中获取到队首元素.
// 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
try {
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.getTime() > curTime) {
// 时间还没到,塞回到队列中
queue.put(myTask);
} else {
// 时间到了,直接执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
// 实现定时器类
class MyTimer {
// 省略前面代码
// ...
// 用来阻塞等待的锁对象
private Object locker = new Object();
public MyTimer() {
// 在这里启动一个线程.
Thread t = new Thread(() -> {
while (true) {
// 循环过程中,就不断的尝试从队列中获取到队首元素.
// 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
try {
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.getTime() > curTime) {
// 时间还没到,塞回到队列中
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
} else {
// 时间到了,直接执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public void schedule(Runnable command, long after) {
MyTask myTask = new MyTask(command, after);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
// 实现定时器类
class MyTimer {
// 省略前面代码
// ...
public MyTimer() {
// 在这里启动一个线程.
Thread t = new Thread(() -> {
while (true) {
// 循环过程中,就不断的尝试从队列中获取到队首元素.
// 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
try {
synchronized (locker) {
while (queue.isEmpty()) {
locker.wait();
}
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.getTime() > curTime) {
// 时间还没到,塞回到队列中
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
} else {
// 时间到了,直接执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
完整代码:
例22:实现定时器
package thread;
import java.util.PriorityQueue;
import java.util.Timer;
import java.util.concurrent.PriorityBlockingQueue;
// 通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
// 任务要干什么
private Runnable command;
// 任务什么时候干
private long time;
public MyTask(Runnable command, long after) {
this.command = command;
// 此处记录的时间是一个绝对的时间戳. 不是 “多长时间之后执行”
this.time = System.currentTimeMillis() + after;
}
// 执行任务的方法,直接在内部调用 Runnable 的 run 即可.
public void run() {
command.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
// 希望时间小的在前面,时间大的在后面
// 这里谁减谁 才能达到时间小的在前
// 写代码验证一下
return (int) (this.time - o.time);
}
}
// 实现定时器类
class MyTimer {
// 用来阻塞等待的锁对象
private Object locker = new Object();
// 使用一个优先级队列来保存若干个任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
// command:要执行的任务是什么
// after:多长时间之后执行这个任务
public void schedule(Runnable command, long after) {
MyTask myTask = new MyTask(command, after);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
public MyTimer() {
// 在这里启动一个线程.
Thread t = new Thread(() -> {
while (true) {
// 循环过程中,就不断的尝试从队列中获取到队首元素.
// 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
try {
synchronized (locker) {
while (queue.isEmpty()) {
locker.wait();
}
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.getTime() > curTime) {
// 时间还没到,塞回到队列中
queue.put(myTask);
locker.wait(myTask.getTime() - curTime);
} else {
// 时间到了,直接执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo23 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1111");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2222");
}
}, 4000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3333");
}
}, 6000);
}
}
线程池,解决问题的思路:
线程池最大的好处就是减少每次启动、销毁线程的损耗.
Executors 创建线程池的几种方式:
例23:标准库中的线程池
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo24 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// Executors.newCachedThreadPool();
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
例24:实现线程池
package thread;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
// 实现线程池类
// 简单实现:固定 10 个线程的线程池
class MyThreadPool {
// 这个队列就是 “任务队列” 把当前线程池要完成的任务都放到这个队列中.
// 再由线程池内部的工作线程负责完成它们
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
// 核心方法,往线程池中插入任务
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设定线程池中有几个线程
public MyThreadPool(int n) {
// 构造方法中,就需要创建一些线程,让这些线程负责完成上述执行任务的工作!!
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
}
}
}
public class Demo25 {
public static void main(String[] args) {
MyThreadPool myThreadPool =new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}