目录
一. 线程 & 进程
进程: 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程: 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少 有一个线程 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程
二. 线程调度
分时调度: 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度: 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的是抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻, 只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时 刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使 用率更高。
三. 同步 & 异步, 并发 & 并行
同步: 排队执行 , 效率低但是安全.
异步: 同时执行 , 效率高但是数据不安全.
举个例子: 有一桌子菜, 大家排队吃饭就是同步, 一起同时吃饭就是异步。
排队吃饭效率较低, 但是你看到你喜欢的菜就没人跟你抢,很安全.
相反,一桌人同时吃饭效率较高, 但是当你看到一块肉并伸出筷子时, 可能已经被别人夹走了, 数据不安全。
并发: 两个或多个事件在同一个时间段内发生。
并行: 两个或多个事件在同一时刻发生(同时发生)。
四. 如何实现多线程?
1. 继承Thread类
第一步: 定义一个类继承Thread类
第二步: 重写run方法
public class MyThread extends Thread {
//run方法就是线程要执行的任务方法
@Override
public void run() {
//这里的代码就是一条新的执行路径
//这个执行路径是触发方式,不是调用run方法,而是通过thread对象的start方法来启动任务
for (int i = 0; i < 10; i++) {
System.out.println("MyThread"+i);
}
}
}
第三步: 创建MyThread类的对象
第四步: 调用start方法, 启动线程
public class Demo1 {
public static void main(String[] args) {
MyThread m = new MyThread();
m.start();
for (int i = 0; i < 10; i++) {
System.out.println("Main"+i);
}
}
}
2. 实现Runnable接口
第一步: 定义一个类实现Runnable类
第二步: 重写run方法
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程的任务
for (int i = 0; i < 10; i++) {
System.out.println("MyRunnable"+i);
}
}
}
第三步: 创建一个任务对象
第四步: 创建一个线程并给他一个任务
第五步: 调用start方法, 启动线程
public class Demo1 {
public static void main(String[] args) {
//1 创建一个任务对象
MyRunnable r = new MyRunnable();
//创建一个线程并给他一个任务
Thread t = new Thread(r);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("Main"+i);
}
}
}
3. 为什么更推荐实现Runnable而不是继承Thread
实现Runnable与继承Thread相比有如下优势:
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2.可以避免单继承所带来的局限性
3.任务与线程是分离的,提高了程序的健壮性
4.后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
4. 匿名内部类
继承Thread类
public class MyThread0 {
public static void main(String[] args) {
new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Thread" + i);
}
}
}.start();
for (int i = 0; i < 10; i++) {
System.out.println("Main" + i);
}
}
}
或者实现Runnable接口
public class MyThread0 {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("Runnable" + i);
}
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("Main" + i);
}
}
}
温馨提示: main方法其实也是一个线程, 也称为主线程。在java中所以的线程都是同时启动的,至于哪个线程先执行,这得看哪个线程先抢到CPU的资源。
五.sleep方法
修饰和类型 | 方法 | 描述 |
---|---|---|
static void | sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
static void | sleep(long millis, int nanos) | 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。 |
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
//线程的休眠
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000); //1000毫秒
}
}
}
}
输出结果是从0-9的9个整数, 只不过没打印一个数都会停顿一秒
六.wait & notify方法
修饰和类型 | 方法 | 描述 |
---|---|---|
void | wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。 |
void | wait(long timeout) | 导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。 |
void | wait(long timeout, int nanos) | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间。 |
void | notify() | 唤醒正在等待对象监视器的单个线程。 |
void | notifyAll() | 唤醒正在等待对象监视器的所有线程。 |
这里我们通过一个生产者与消费者的例子来快速熟悉这两个方法
假设现在有三个类, 分别是厨师类, 服务员类, 和食物类.
大致的服务流程:
厨师每做好一道菜都会唤醒服务员并进入无限期等待状态, 当服务员把菜送上餐桌后便会将厨师唤醒让其开始做下一道菜, 自己则再一次进入等待状态, 循环100次.
我们先演示不加wait和notify的结果
厨师类
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndSaste("土耳其冰淇凌","巧克力味");
}else{
f.setNameAndSaste("麻婆豆腐","香辣味");
}
}
}
}
服务员类
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
食物类
static class Food{
private String name;
private String taste;
public void setNameAndSaste(String name,String taste){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
}
public void get(){
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
}
}
}
Main方法
public class Demo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
运行结果:
问题很明显, 巧克力味的麻婆豆腐和香辣味的冰淇凌? 这就离谱!!!
解决方案: 在Food类中加一个标记flag
当flag为true时, 厨师可以开始做菜, 在做完菜后, 设置flag为false, 并唤醒服务员(让他上菜), 再让厨师线程进入等待状态
当flag为false时,服务员可以上菜,上菜后再设置flag变为true(厨师在flag为true时才能做菜), 再把厨师线程线程唤醒, 最后让自己(服务员)进入等待状态
改进的Food类
static class Food{
private String name;
private String taste;
//true 表示可以生产
private boolean flag = true;
public synchronized void setNameAndSaste(String name,String taste){
if(flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag) {
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
七. 线程生命周期
-
一旦线程对象被new出来后,线程就进入了新建状态;
-
当该对象调用start()方法,就进入就绪状态;
-
进入就绪状态后,当该对象抢到CPU时间片就会进入运行状态;
-
进入运行状态后
-
4.1. 若run()方法或main()方法结束后,线程就进入终止状态;
4.2. 当线程调用了自身的sleep()方法或其他线程的join()方法, 线程就会进入阻塞状态(注意: 调用sleep ()函数后,线程不会释放"锁")。当sleep()结束或join()结束后,该线程回到可运行状态,继续争抢CPU时间片。
4.3. 当线程调用了yield()方法,该线程放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态。
4.5. 当线程调用了suspend() 和 resume() 方法,suspend()使得线程进入阻塞状态,并且不会自动恢复,只有resume() 方法被调用,才能使得线程重新进入可运行状态。一般suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
4.6、wait() 和 notify() 方法:当线程调用wait()方法后会进入等待状态,进入这个状态后,只能依靠其他线程调用notify()或notifyAll()方法才能被唤醒(一般都用notifyAll()方法,唤醒有所线程)当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用
注意:wait() 和 notify() 与suspend() 和 resume() 这两对方法看起来很相似,但是它们是有区别的。suspend()方法在线程阻塞时都不会释放占用的"锁"(如果占用了的话),而wait()方法的调用不会让线程释放掉占有的"锁"。
八. 线程安全
多线程为什么会出现安全问题呢?我们不妨先举个例子
三个售票窗口同时卖10张火车票(这里我们创建3个线程来模拟该情景)
public class Demo7 {
public static void main(String[] args) {
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (count>0){
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:"+count);
}
}
}
}
运行结果如下
结果发现出现了余票"增加"以及余票负数等问题
这是因为多线程在进行同一卖票任务时,当余票只剩一张时, 三个线程都已经进入到while(count>0)的语句里, 所以最后会出现负数的情况。
以下是三种解决方式
1. 同步代码块(隐式锁)
把synchronized关键字加上一个锁对象.
语法: synchronized(锁对象){}
public class Demo8 {
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
}
}
}
}
}
2. 同步方法(隐式锁)
把synchronized关键字修饰在方法Sale()中
语法: public synchronized boolean sale(){}
public class Demo9 {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案2 同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;
}
}
}
3. Lock(显示锁)
语法:
1. 创建一把锁: Lock l = new ReentrantLock()
2. 加锁: l.lock()
3. 解锁: l.unlock()
public class Demo10 {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案1 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Lock l = new ReentrantLock();
@Override
public void run() {
while (true) {
l.lock();
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}
l.unlock();
}
}
}
}
九. 线程池
线程池, 顾名思义就是一个装线程的容器. 通过重用已存在的线程,降低线程创建和销毁造成的消耗. 线程若是无限制的创建,可能会导致内存占用过多,并且会造成cpu过度切换.
1. 缓存线程池
特点: 没有长度限制
流程:
1. 判断线程池是否存在空闲线程
2. 存在则使用
3. 不存在则创建线程并使用
public class Demo13 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2. 定长线程池
特点: 长度固定
流程:
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
public class Demo14 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
结果如下
由于线程池指定长度为2, 在执行第三个任务时并没有创建第3个线程, 而是由空闲下来的线程来执行
3. 单线程线程池
特点: 线程池里只有一个线程
流程:
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
public class Demo15 {
/*单线程线程池
执行流程
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
**/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
运行结果如下
三个任务全部由一个线程来执行
4. 周期定长线程池
流程:
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
定时执行一次
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//定时执行一次
//参数1:定时执行的任务
//参数2:时长数字
//参数3:参数2的时间单位 Timeunit的常量指定
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5, TimeUnit.SECONDS); //5秒钟后执行*/
周期性执行任务
public class Demo16 {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行上面时间以后)
参数3:周期时长数字(没隔多久执行一次)
参数4:时长数字的单位
* **/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1,TimeUnit.SECONDS);
}
}