一、多线程
1、并发与并行
并发: 两个或多个事件在同一时间段内发生(交替执行);
并行: 两个或多个事件在同一时间段内发生(同时运行);
它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念:
并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象;
并行的"同时"是同一时刻可以多个进程在运行(处于running)。
2、进程与线程
进程: 是正在进行资源分配和调用的独立单位;
是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。
线程: 是进程中的单个顺序控制流,是一条执行路径;
- 单线程: 一个进程如果只有一条执行路径,则称为单线程程序;
- 多线程: 一个进程如果有多条执行路径,则称为多线程程序。
二、多线程的实现
java.lang.Thread类是程序中执行的线程,JVM允许应用程序同时执行多个线程
1、创建多线程方式一:定义一个子类继承Thread类,这个子类应该重写Thread类的run方法
Thread类中的 start( ) 方法,才可以使线程开始执行,使JVM调用此线程的run方法。
直接调用run方法,没有用
// MyThread类继承Thread类:
public class MyThread extends Thread {
@Override // 重写run方法
public void run() {
// 打印1~99
for(int i=0; i<100; i++){
System.out.println(i);
}
}
}
// 创建MyThread对象
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// 启动线程,调用start方法
my1.start();
my2.start();
}
}
2、设置和获取线程名称
在Thread类中,有一个静态方法,static Thread currentThread()———返回对当前正在执行的线程对象的引用。
main方法是在一个叫做main的线程中执行的。
3、线程调度
4、线程控制
- 线程控制用到的方法
- 演示
sleep()方法
join()方法
setDaemon()方法
5、线程的生命周期
6、创建多线程方式二:声明一个实现Runnable接口的类
相比继承Thread类,实现Runnable接口创建多线程的好处:
- 避免了Java单继承的局限性(实现类将来还可以有一个直接父类);
- 增强了程序的扩展性,降低了程序的耦合性(解耦)
适合多个相同的程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。
7、匿名内部类方式实现线程创建
三、线程安全
1、线程安全问题的出现
代码实现:3个窗口,卖100张票。
// SellTicket实现类
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
// 通过sleep()方法来模拟出票时间
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--;
}
}
}
}
// 测试类
public class SellTicketDemo {
public static void main(String[] args){
SellTicket st = new SellTicket();
// 创建三个Thread类对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
上述代码,会产生线程安全问题
2、判断多线程程序是否有数据安全问题的标准
- 是否是多线程环境(单线程不会出现线程安全问题);
- 是否共享数据(如果是多线程,但是没有共享数据,不会出现线程安全问题);
- 是否有多条语句操作共享数据(如果只有一条语句来控制共享数据,不会有线程安全问题)
上面的代码,三个条件都满足了,存在线程安全问题。
3、解决线程安全问题的三种方法
- 把多条语句操作共享数据的代码锁起来,让任意时刻,只能有一个线程执行
- Java提供同步代码块的方式来解决
同步代码块
运行测试类,控制台输出没出现线程安全的毛病。
同步方法
- 同步方法
就是把synchronized关键字加到方法上
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){
可能会产生线程安全的代码
}
同步方法的【锁对象】:this(this是创建对象之后产生的,静态方法优先于对象,所以静态方法就不能用this)
- 同步静态方法(比较特殊)
静态与对象无关,只与类有关;静态只能调用静态的东西
格式:
修饰符 static synchronized 返回值类型 方法名(方法参数){
可能会产生线程安全的代码
}
同步静态方法中的【锁对象】是:本类名称.class(class文件对象,反射)
Lock锁
java.util.concurrent.locks.Lock
是一个接口,该接口提供了比synchronized代码块和synchronized方法更广泛的锁定操作
Lock锁也称为同步锁,加锁与释放锁方法化了,Lock接口中的方法:
- public void lock() : 加同步锁。
- public void unlock() : 释放同步锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口
四、线程状态
回到目录
线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State
这个枚举中给出了六种线程状态:
1、Timed Waiting(计时等待)
Timed Waiting状态,在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。就相当于是延时,之前重写run方法中的 sleep(1000);
表示延时(或者说是睡眠)1s,也就是Timed Waiting(计时等待)状态。
- 进入Timed Waiting状态的一种常见情形是调用sleep方法,单独的线程也可以调用;
- 将Thread.sleep()方法的调用放在线程run()方法里面,可以保证该线程执行过程中会睡眠;
- sleep()与锁无关,线程睡眠到期会自动苏醒,并返回Runnable(可运行)状态。
Thread Waiting(计时等待)线程状态图:
2、Blocked(锁阻塞)
Blocked(锁阻塞)状态,在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
解释:线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
Blocked 线程状态图:
3、Waiting(无限等待)
Waiting(无限等待)状态,在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
- 在对象上的线程调用了Object.wait()会进入WAITING状态,直到另一个线程在这个对象上调用了Object.notify()或Object.notifyAll()方法才能恢复;
- 一个调用了Thread.join()的线程会进入WAITING状态直到一个特定的线程来结束
五、生产者消费者模式—等待唤醒机制
1、线程间通信
线程间通信:指多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
2、生产者消费者模型
3、实现生产者消费者模型
思路:
实现:
Box类:
public class Box {
// 定义成员变量,表示第x瓶奶
private int milk;
// 定义一个成员变量,表示奶箱有无牛奶的状态
private boolean state = false;
// 提供存储牛奶和获取牛奶的操作
public synchronized void put(int milk) {
// 如果有牛奶(state = true),等待消费
if (state) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有牛奶,就生产牛奶
this.milk = milk;
System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
//生产完毕之后,修改奶箱的状态
state = true;
// 还要唤醒其他等待的线程
notifyAll();
}
public synchronized void get() {
// 如果没有牛奶,就生产牛奶
if(!state){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果有牛奶,就消费牛奶
System.out.println("用户拿到第" + this.milk + "瓶奶");
System.out.println("==============================");
// 消费完之后,修改牛奶状态
state = false;
// 唤醒其他等待的线程
notifyAll();
}
}
结果:
六、线程池
之前,我们使用线程的时候就去创建一个线程,虽然非常简单,但是会存在着问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率(频繁创建线程、销毁线程都要时间)。
1、线程池概述
线程池: 线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。
在JDK1.5之后,JDK就内置了线程池,可以直接使用,底层原理是一个队列。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2、线程池的使用
Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。java并发编程:Executor、Executors、ExecutorService
线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool,生产一个指定线程数量的线程池;
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务;
3.调用ExecutorService中的submit方法,传递线程任务(实现类),开启线程,执行run方法;
4.调用ExecutorService中的shutdown方法,销毁线程池(不建议执行)
代码实现: