一、进程与线程
1.1 概念
程序是机器上安装的软件,是一个静止的内容。
当程序被启动,就会产生至少一个进程。一般情况下,一个程序产生一个进程,但有些特殊用途的程序进行时会产生多个进程。
在一个进程中,可以创建多个任务同时进行,这些任务称为线程,是一种轻量级进程。当这些线程同时执行(交替执行),称为多线程。
1.2 理解
线程是同时执行还是交替执行?
线程是利用cpu空闲时间交替执行,由于交替执行时间短,看起来像同时执行。
在现在的电脑上是同时执行还是交替执行?
现在电脑并非单核,单核cpu执行多线程都是交替执行,但多核意味着多个cpu,也就可做到同时执行。
1.3 线程和进程的区别
- 一个程序至少一个进程。
- 一个进程至少一个线程,可包含多个线程。
- 进程是系统分配资源的基本单位,而线程是cpu调度的单位。
- 进程之间一般不能共享数据,但线程之间可以共享数据。
二、线程的创建
2.1 线程的组成
- cpu的时间片:每个线程在执行时都需要cpu分配时间;
- 运行数据:
- 堆空间数据。共享数据
- 栈空间数据。一般是临时变量,线程中有独立空间未保存。
- 逻辑代码
2.2 线程的创建
三种方法创建
- 继承Thread类
- 需要重写run方法
- 然后创建该类的对象
- 执行start()开始执行线程
- 实现Runnable(任务)接口
- 需重写run方法
- 创建该类对象
- 再创建Thread类对象
- 执行start()开始执行线程
- Callable和FutureTask(可以得到线程的返回值)
- 得到任务对象
- 定义实现Collable接口,重写call方法,封装其要做的事情
- 用FutureTask把Callable对象封装成线程任务对象
- 把线程任务对象交给Thread处理
- 调用Thread的start方法启动线程,执行任务
- 线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果
Runnable创建线程较麻烦,那么Thread和Runnable的区别在哪?
- 继承Thread类的使用简单,而实现接口后还是要创建Thread类对象,使用相对复杂;
- 继承类后不能再继承其他类,而实现接口后还可以继承其他类,使用相对灵活;
- 继承类后,中间代码可能复用,而实现接口后,逻辑代码还可以复用;
- 两种创建线程的如果有执行结果,是不能直接返回
正常情况,当我们没有创建线程时,下面代码执行顺序为先打印100次A然后再打印100次B,顺序执行
public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(i + "...A"); } for (int i = 0; i < 100; i++) { System.out.println(i + "...B"); } }
当我们想要使其交替执行,则将业务代码创建为线程,使其交替执行,交替执行的执行时间是随机的,可能A抢占10次时间片打印10次,然后B抢占5次时间片,打印5次,然后又是A等等,抢占时间随机,抢占顺序随机(无规律)。
//使用继承Thread类的方式创建 public static void main(String[] args) { //创建线程 Thread1 th1 = new Thread1(); Thread2 th2 = new Thread2(); //启动线程 th1.start(); th2.start(); } public class Thread1 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + "...A"); } } } public class Thread2 extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + "...B"); } } }
//实现Runnable接口创建线程任务 public static void main(String[] args) { //创建线程任务 Runnable1 run1 = new Runnable1(); Runnable2 run2 = new Runnable2(); //创建Thread对象(传入线程任务) Thread th1 = new Thread(run1); Thread th2 = new Thread(run2); //启动线程 th1.start(); th2.start(); } public class Runnable1 implements Runnable { //实现接口也需要实现其run方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + "...A"); } } } public class Runnable2 implements Runnable { //实现接口也需要实现其run方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(i + "...B"); } } }
public class thread {
public static void main(String[] args) {
//创建任务对象,求1-100结果
Callable<String> call = new MyCallable(100);
//将任务对象封装成线程任务对象
//此处使用FutureTask的原因1:FutureTask是Runnable的子类,可以将任务对象封装成线程任务对象
//此处使用FutureTask的原因2:可以调用FutureTask的get方法去获取线程的返回值,get方法可以等待线程执行结束后再获取结果
FutureTask<String> ft = new FutureTask(call);
//创建Thread线程对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
Callable<String> call2 = new MyCallable(200);
FutureTask<String> ft2 = new FutureTask(call2);
Thread t2 = new Thread(ft2);
t2.start();
try {
//使用get方法来获取结果,当主程序运行到这里时,get方法去获取对象,如果线程还没运行结束,那么就会等待线程执行结束
String s = ft.get();
System.out.println("求和结果为:" + s);//4950
} catch (Exception e) {
e.printStackTrace();
}
try {
String s = ft2.get();
System.out.println("求和结果为:" + s);//19900
} catch (Exception e) {
e.printStackTrace();
}
}
}
//类要实现Callable接口,且要重写call方法
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
//线程用来求1-n的和,并且返回结果
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return "求和结果为:" + sum;
}
}
2.3 经典面试题
start()和run()的区别?
- 直接调用run()是直接将线程类中的业务逻辑代码执行,等同于一个类,创建该类对象,使用其方法。根本没有使用线程相关内容,没有创建多的线程。
- 当调用start方法时,自身进入就绪状态,等待抢占cpu的执行时间,进而执行run()中的业务内容。
当程序启动时,会自动创建一个进程,该进程中有默认的一个进线程,此线程名称为main(主线程)。
三、线程的状态
3.1 基本状态
- 新建:创建Thread对象,与普通创建对象没有区别;
- 就绪:调用start方法进入就绪状态,等待系统分配时间片;
- 运行:抢占到时间片后,运行run方法中的业务代码,如果业务代码没有执行完毕,但是时间片到了,就会进入就绪状态,等待下一次分配时间片。
- 终止:业务代码执行结束或main执行结束
线程饿死:一个线程一直没有抢占到时间片,无法执行。
3.2 常见方法
3.2.1 sleep 休眠
sleep指让当前进程主动进入休眠,退出抢占时间片,直到休眠结束再抢占时间片,单位为毫秒。
一旦进入休眠状态,其他进程会优先抢占时间片。
public static void main(String[] args) {
//创建
Thread1 th1 = new Thread1();
Thread2 th2 = new Thread2();
//启动
th1.start();
th2.start();
}
public class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...A");
//当A打印到20时,休眠5000毫秒后再打印,也就是五秒,需要处理异常
if (i == 20){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
public class Thread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...B");
}
}
}
3.2.2 yield 放弃
yield指让当前进程放弃这一次时间片抢夺,直接进入就绪状态,竞争下一次时间片。
注意:yield只是放弃这一次抢夺,并不能保证下一次抢夺不会优先。例如:当A放弃 本轮抢夺没有打印后进入下一次竞争,但是下一次竞争A又抢到了,然后打印,所以看似没有放弃,实则放弃一次后又抢到了打印的。
public static void main(String[] args) {
//创建
Thread1 th1 = new Thread1();
Thread2 th2 = new Thread2();
//启动
th1.start();
th2.start();
}
public class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...A");
//当A打印到20时,放弃本次争抢时间片,直接进行下次争抢
if (i == 20){
Thread.yield();
}
}
}
}
public class Thread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...B");
}
}
}
3.2.3 join 合并
join允许其他线程加入当前线程中,加入后需要将加入的线程执行完毕后才会继续执行当前线程。
public static void main(String[] args) {
Runnable run1 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...A");
}
}
};
//创建
Thread th1 = new Thread(run1);
Runnable run2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "...B");
//当B打印到20时就将th1线程合并进来,需要处理异常
if (i == 20){
try {
th1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
};
//创建
Thread th2 = new Thread(run2);
//启动
th1.start();
th2.start();
3.2.4 获取线程名
//哪个线程运行就是获取哪个线程名称
Thread.currentThread().getName()
3.3 线程等待
等待:线程进入等待状态,等待结束后进入就绪状态。
- 当线程中使用sleep后,进入限时等待,时间结束进入就绪状态。
- 当线程中使用join后,进入不限时等待,直到进来的线程执行完毕后才进入就绪状态。
- 当线程中使用wait后,进入了等待,直到被唤醒或等待超时,才进入就绪状态。
3.4 线程安全
当多线程同时访问共享资源时,如果破坏了原子操作,可能会出现线程不安全问题。
条件:
- 多线程访问
- 修改同一资源
例如:当我们有10套房子且在不分房子的情况下让三个人卖房子,可能会导致卖时比规定的10套要多出,因为房子只有交付出去了其他人才会知道,若此时有俩人同时卖了同一套房子,那么这套房子该给谁呢?这就出现了线程不安全。
//可能会多卖房子,并不是一定会出现问题 public static void main(String[] args) { //创建三个人(多线程) Persion zs = new Persion("张三"); Persion ls = new Persion("李四"); Persion ww = new Persion("王五"); //启动多线程一起卖房子 zs.start(); ls.start(); ww.start(); } //创建Persion类并且继承Thread类 public class Persion extends Thread { //共同房源,十套 private static Integer house = 10; //定义name属性 private String name; //使用有参构造赋值 public Persion(String name) { this.name = name; } //重写run方法 @Override public void run() { while (house > 0) { //卖房子 house--; System.out.println(name + "卖出一套房子,还有" + house + "套房子"); } } }
3.4.1 解决方法
线程同步(加锁)解决
锁对象的规范要求:
- 建议使用共享资源作为锁对象
- 对于实例方法建议使用this作为锁对象
- 对于静态方法建议使用字节码(类名.class)对象作为锁对象
语法:使用synchronize关键字
- synchronized同步代码块
- synchronized同步方法
//synchronized同步代码块解决方法 //创建Persion类并且继承Thread类 public class Persion extends Thread { //同上省略 //重写run方法 @Override public void run() { while (house > 0) { //synchronized(填写公共属性,例如此处填写三个人一起卖的房子house) //相当于加锁,当有一个人在卖房子的时候,其他人都不能进入该代码块卖房子 synchronized (house){ //加入判断,防止线程在循环的house>0判断完成后,进入代码块后时间片用完重新竞争时间片执行,但此时house已经卖完 //加入判断再次确认house是否卖完,然后让其无法卖 if (house>0){ //卖房子 house--; System.out.println(name + "卖出一套房子,还有" + house + "套房子"); } } } } }
3.5 线程阻塞
阻塞:当线程运行过程中,遇到加锁的代码,需要去获取锁,在没有获取时进入阻塞状态,需等待持有锁的线程将加锁代码执行结束后才能继续执行。
四、死锁
当一个线程持有锁A,等待锁B,另一个线程持有锁B,等待锁A,两个线程都不会释放锁,此时也无法获取到另一把锁,产生死锁。
死锁的根本成因:获取锁的顺序不一致导致。
简单的顺序锁解决方法:
- 让每个线程获取锁的顺序都是一样的,都去先获取A钥匙,再获取B钥匙,那么就能解决死锁的问题了。
- 设置等待锁的时间,当超过这个时间后就释放当前锁让其他线程能够使用,然后再重新获取锁。
//例如boy和girl想要打开同一扇门,此门只能进入一个人,但是这扇门有两把钥匙,此时boy和girl一人一把,两人都想进入这扇门,所以两人都不想让步给对方钥匙,那么此时就进入了死锁。
public static void main(String[] args) {
Thread boy = new Thread(new Runnable() {
@Override
public void run() {
synchronized (chops.A){
System.out.println("boy抢到了A钥匙");
System.out.println("等待B钥匙");
synchronized (chops.B){
System.out.println("boy又抢到了B钥匙");
System.out.println("抢到了两把钥匙,打开门");
}
}
}
});
Thread girl = new Thread(new Runnable() {
@Override
public void run() {
synchronized (chops.B){
System.out.println("girl抢到了B钥匙");
System.out.println("等待A钥匙");
synchronized (chops.A){
System.out.println("girl又抢到了A钥匙");
System.out.println("抢到了两把钥匙,打开门");
}
}
}
});
boy.start();
girl.start();
}
//创建一个接口定义两个公共的静态对象
public interface chops {
Object A = new Object();
Object B = new Object();
}
五、线程通信
线程通信就像生活中:家里父亲和母亲包饺子,父亲擀面皮,母亲包饺子,因为父亲擀面皮速度快,不一会将桌子摆满了,父亲就说我先wait会,待会包完了notify我,我再来接着擀面皮。
使用wait方法让线程进入等待状态,wait只能在synchronized中使用,且synchronized和wait的对象应该一致。
使用notify或者notifyAll唤醒线程,进入就绪状态。
格式:当用什么对象wait后,就用什么对象去notify,wait还可以设置时间,表示当等待多长时间后放弃等待。
使用wait等待是无限期等待,需要唤醒或者超时。
- 唤醒:指使用notify或者notifAll
- 超时:指在指定时间内,如果没有唤醒,那么就放弃等待。
//利用wait解决男女生开门死锁问题,当Boy抢到了B钥匙后主动把钥匙交出来让Girl抢,然后等Girl打开门后钥匙空出来,再叫Boy抢A钥匙。这样就不会发生死锁问题
public class Demo1 {
public static void main(String[] args) {
//创建线程
Boy boy = new Boy();
Girl girl = new Girl();
//运行线程
boy.start();
girl.start();
}
}
public class Girl extends Thread {
@Override
public void run() {
synchronized (A.a){
System.out.println("Girl抢到了A钥匙");
System.out.println("准备抢B钥匙");
synchronized (A.b){
System.out.println("Girl抢到了B钥匙");
System.out.println("Girl打开门");
A.b.notify();
}
}
}
}
public class Boy extends Thread{
@Override
public void run() {
synchronized (A.b){
System.out.println("Boy抢到了B钥匙");
System.out.println("准备抢A钥匙");
try {
A.b.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A.a){
System.out.println("Boy抢到了A钥匙");
System.out.println("Boy打开门");
}
}
}
}
public interface A {
Object a = new Object();
Object b = new Object();
}
注意:如果有多次线程同时使用某个锁对象进行wait状态,那么一次notify方法调用只会随机唤醒一个,需要多次调用notify方法,此时,可以使用notifyAll一次唤醒所有的进入wait状态的线程。
经典面试题:
sleep与wait的区别:
sleep需要指定时间,时间到了会自动醒来,而wait如果没有指定时间,会无限期等待,直到被唤醒为止。
sleep是一个静态方法,而且不需要在同步时调用,而wait是一个对象方法,需要在同步时使用。
- **
sleep在休眠时不会释放锁,而wait会释放锁。**
六、生产消费模式
6.1 设计模式
设计模式是指前人经验的总结,且经过长期的时间验证行之有效的方案,将在项目中可能遇到的问题进行分类总结,并找到其对应的解决方案。
二十三中设计模式:常见的二十三种问题的解决方案。
分为三大类:
- 创建型模式:创建对象的不同方式。(5种)单例模式,工厂模式、原型模式等。
- 结构型模式:多个对象形成一种新的结构。(7种)桥接模式、适配器模式、装饰模式。
- 行为型模式:多个对象之间相互作用。(11种)命令模式、监听者模式、调停者模式、迭代模式。
注意:软件发展到现在,遇到的问题不止23种,所以现在有很多新的设计模式出现,不在23种设计模式中,例如:MVC模式
6.2 生产消费者模式
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品存入缓冲区,消费者从缓冲区取走产品去消费,显然生产者和消费者之间必须保持同步,即不用需消费者到空的缓冲区取产品,生产者不允许向满的缓冲区存放产品。
面试题:
synchronized代码块和synchronized方法的区别?
- synchronized方法表示方法中的所有代码块都被同步;
- synchronized方法加锁对象是调用该方法的对象,所以静态方法所得对象是类名.class,非静态方法所得对象是this
- synchronized代码块可以指定加锁哪部分代码,性能优一点;
- synchronized代码块要指定加锁对象。
例如:4S店(消费者) 、仓库(缓冲区)、 产车工厂(生产者)
public class Demo1 {
public static void main(String[] args) {
//4S店和工厂都在同一个仓库in和out,所以用final修饰不可变
final WareHouse wareHouse = new WareHouse();
//生产消费者模式 4S店(消费者) 仓库(产品缓冲区) 工厂(消费者)
Factor f1 = new Factor(wareHouse);
Factor f2 = new Factor(wareHouse);
fourS cs1 = new fourS(wareHouse);
fourS cs2 = new fourS(wareHouse);
fourS cs3 = new fourS(wareHouse);
//启动线程
f1.start();
f2.start();
cs1.start();
cs2.start();
cs3.start();
}
}
//小车对象
public class Car {
//定义id和name,便于打印观察个数
private int id;
private String name;
public Car(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Car{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
//仓库
public class WareHouse {
//定义仓库大小,以及仓库已经存放的个数
private static Car[] Car = new Car[6];
private static int count = 0;
//工厂生产Car放入仓库,使用synchronized修饰方法,
public synchronized void in(Car car) {
//当仓库中的Car个数>=6时,表示仓库放满了,当前工厂线程进入wait等待
while (count >= 6){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当wait被唤醒后往后执行,将生产的车存入仓库数组
Car[count++] = car;
System.out.println("生产了一辆车" + car);
//当生产车后就唤醒4S店去卖车
this.notifyAll();
}
//4S店在仓库消费Car,使用synchronized修饰方法,
public synchronized Car out() {
//当仓库中的Car个数<=0时,表示仓库没有Car了,当前4S店线程进入wait等待
while (count <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当wait被唤醒后往后执行,将索引靠后的车卖出
Car car = Car[--count];
//将卖出车的位置置为null
Car[count] = null;
System.out.println("消费了一辆车" + car);
//当卖出车后就唤醒工厂进行造车
this.notifyAll();
return car;
}
}
//4S店,因为工厂和4S是同时进行的,所以要继承Thread多线程
public class fourS extends Thread{
private WareHouse wareHouse;
//有参构造,传入要放入的仓库
public fourS(WareHouse wareHouse) {
this.wareHouse = wareHouse;
}
//重写run方法,限制每个4S店只能卖10辆车
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//每调用一次out,表示仓库卖出一辆车
wareHouse.out();
}
}
}
//工厂,因为工厂和4S是同时进行的,所以要继承Thread多线程
public class Factor extends Thread{
private WareHouse wareHouse;
//有参构造,传入要放入的仓库
public Factor(WareHouse wareHouse){
this.wareHouse = wareHouse;
}
//重写run方法,限制每个工厂只能生产15辆车
@Override
public void run() {
for (int i = 0; i < 15; i++) {
//每次生产车需要创建Car对象传入仓库,表示仓库存入一辆车
Car car = new Car(i,Thread.currentThread().getName());
wareHouse.in(car);
}
}
}
七 、线程终止
让正在执行的线程停止
一般有三种方法
7.1 使用stop
使用stop方法,此方法已经被弃用,stop是强行停止线程,相当于电脑运行时断电,会导致一些隐患。
//Runnable创建线程简写
//理论上girl打印1000次会比boy后执行完
Thread girl = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("girl-" + i);
}
});
Thread boy = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("boy---" + i);
//当boy执行到i=90时,会将girl强制停止
if (i == 90){
girl.stop();
}
}
});
boy.start();
girl.start();
7.2 自定义标识
在线程运行过程中,定义一个线程运行时需要满足的标识,需要线程停止时,修改标识值,线程就会执行完毕。
//标识符为静态,一般用volatile修饰,后面会学习该修饰符
public static volatile boolean flag = false;
public static void main(String[] args) {
//Runnable创建线程简写
//理论上girl打印1000次会比boy后执行完
Thread girl = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("girl-" + i);
//判断标识的值是否被修改,当被修改为true,就会执行return来结束线程
if (flag){
return;
}
}
});
Thread boy = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("boy---" + i);
//当boy执行到i=90时,会将标识的值修改
if (i == 90){
flag = true;
}
}
});
boy.start();
girl.start();
}
7.3 使用interrupt
系统对线程中定义的标识,通过改变该标识值来停止线程。
格式:线程对象.interrupt()
被停止对象需要判断Thread.interrupted()
注意:如果线程正在休眠,通过interrupted去终止线程,会出现异常。
解决方法:需要在sleep抛出异常中,将抛出的异常修改为终止命令return即可终止线程。
//Runnable创建线程简写
//理论上girl打印1000次会比boy后执行完
Thread girl = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("girl-" + i);
//对系统标识进行判断,当被修改就会执行return
if (Thread.interrupted()){
return;
}
//若线程在休眠时,通过interrupted去终止线程,会出现异常
//此时只需要将抛出的异常改为return就能正常结束线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// e.printStackTrace();
return;
}
}
});
Thread boy = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("boy---" + i);
//当boy执行到i=90时,会修改系统标识
if (i == 90){
girl.interrupt();
}
}
});
boy.start();
girl.start();
八、线程的优先级
优先级越高,被CPU分配时间片的概率越高。
优先级最高位10,最低为1,默认为5,可以通过常量设置。
格式:线程对象.setPriority(值);
九、守护线程
守护线程是一个特殊的线程,如果没有其他的线程在运行,守护线程会自动停止。JVM是一个守护线程,当程序执行结束时,JVM会自动停止。
格式:线程名.setDaemon(true);
//Runnable创建线程简写
Thread girl = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("girl-" + i);
}
});
Thread boy = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("boy---" + i);
}
});
//将girl设置为守护线程,若此时没有其他线程正在运行,那么girl也会自动停止
girl.setDaemon(true);
boy.start();
girl.start();
十、volatile关键字的作用
synchronized:
- 可见性:执行到同步代码块时,如果要访问临界资源,会去获取最新的值。
- 互斥性:执行到同步代码块时,会持有锁,其他的线程如果要执行同步代码块,会等待前一个线程执行完毕后才去执行同步代码块。
volatile修饰属性时,表示该属性具备有可见性,注意:该关键字并不能解决线程安全问题。因为volatile只符合可见性特点,并没有互斥性特点。
当没有使用volatile关键字,也没有在循环中打印或者休眠时,会发现即使在外的线程中修改了变量的值,该线程也不会停止,那是因为循环中的判断并没有去读取最新的值,一直是用缓存中的值去判断,所以无法停止。
但是加了volatile关键字后,每次使用变量都会去获取最新的值,所以线程能够正常停止。
但是即使没有使用volatile关键字,如果在循环中休眠或者打印后,一样可以实现线程停止效果。注意:因为循环是CPU相对较忙,没有空闲时间进行变量值的更新,但是系统会尽量完成值的更新,所以一旦休眠或打印,对于CPU来说,会比较闲,也就说有足够时间进行变量值的更新。但不推荐这样写,应该使用volatile关键字。
十一 、Lock锁
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个全新的锁对象Lock,更加灵活,方便。
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
- 建议锁对象为final修饰且用static修饰,那么该类就只有这一把锁
构造方法 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
方法名称 | 说明 |
void lock() | 获得锁 |
void unlock() | 释放锁 |
11.1 公平锁
公平锁创建
//多态写法,如果创建锁使用有参构造,传入true,则创建一个公平锁
private static final Lock reentrantLock = new ReentrantLock(true);
11.2 读写锁
private static final ReadWriteLock reentrantLock = new ReentrantReadWriteLock();
//具体使用可以查看api文档
十二、线程池(重点)
12.1 概述
线程池就是一个可以复用线程的技术。
- 如果不使用线程池,那么每当用户发起一个请求,后台就会new一个新线程来处理,下次新任务来了又new一个新线程,而创建新线程开销很大,会影响系统性能
- 如果使用线程池,那么我们可以规定几个线程来处理用户请求(任务队列),当用户多了那么他们就会等待线程空闲了再来处理,相当于线程复用。
任务接口有:Runnable和Callable
12.2 API
12.2.1 构造方法
JDK5.0开始提供线程池接口:ExecutorService,因为接口无法被实例化,所以我们要使用它的实现类TreadPoolExcutor
如何得到线程池对象
- 方式一:使用ExecutorService的实现类TreadPoolExecutor来创建一个线程池对象。
- 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。(其实底层还是方式一,只不过帮我们封装了)
TreadPoolExecutor构造器的参数说明:
//ThreadPoolExecutor构造器 public ThreadPoolExecutor(int corePoolSize,//线程池的线程数量(核心线程,不会死掉)不能小于0 int maximumPoolSize,//线程池的最大线程数。>=核心线程数量 long keepAliveTime,//临时线程存活时间,不能小于0 TimeUnit unit,//存活时间的单位 BlockingQueue<Runnable> workQueue,//任务队列,不能为null ThreadFactory threadFactory,//生产临时线程工厂,不能为null RejectedExecutionHandler handler)//当核心线程和临时线程都在忙,且任务队列满后,再有新任务的处理方式,不能为null
指定线程池的线程数量(核心线程):corePoolSize 不能小于0 指定线程池可支持的最大线程数:maximumPoolSize 最大数量>=核心线程数量 指定临时线程的最大存活时间:keepAliveTime 不能小于0 指定存活时间的单位(秒分时天):unit 时间单位 指定任务队列:workQueue 不能为null 指定哪个线程工厂创建线程:ThreadFactory 不能为null 指定线程忙,任务满时,新任务来了怎么办:handler 不能为null
举例各参数:比如KTV中,招了三个正式工(相当于核心线程数3个),并且老板说当忙时最多只能再招7个临时工(相当于设置最大的线程数为10,那么临时线程就是7个),此时KTV来了三个客户,三个核心线程开始忙,此时又来了五个客户,因为没有服务员来招待客人,所以客户们都坐在KTV外面的板凳上等,板凳只有五个(相当于任务队列设置为5),此时老板没有招临时工,认为前面三个服务员马上就会忙完来招待剩下的客人,没想到此时又来了一个客人,且三个正式工还在忙,板凳也没有坐的了,那么老板就找人力资源(相当于线程工厂创建线程)又招了一个临时工来接待客人,如果又来的客人没地方坐那么又会招一个。(相当于临时线程创建),那么当没有客人了,此时服务员都闲了起来,过了两天都没有来客人,那么老板就开除了临时工。(相当于设置了临时线程存活时间为2天)
//创建一个线程池,核心线程数3个,最多4个线程,临时线程存活时间5秒,任务队列为1个,线程工厂,拒绝策略为抛出异常并拒绝(默认策略)
ExecutorService pools = new ThreadPoolExecutor(3, 4, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
};
//核心线程处理
pools.execute(runnable);
pools.execute(runnable);
pools.execute(runnable);
//任务队列
pools.execute(runnable);
//创建临时线程(因为核心线程在忙,且任务队列满了,会创建两个临时线程)
pools.execute(runnable);
//核心线程和临时线程都在忙,且任务队列满了,就开始拒绝并抛异常
pools.execute(runnable);
12.2.2 常用方法
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行Runnable任务 |
---|---|
Future submit(Callable task) | 执行任务,返回未来任务对象获取线程结束,一般拿来执行Callable任务 |
void shutdown() | 等任务执行完毕后关闭线程 |
List shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
12.2.3 新任务拒绝策略
最后一个参数
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出异常,默认策略 |
---|---|
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但不抛出异常,不推荐 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待醉酒的任务,把新任务加入队列 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务run()方法从而绕过线程池直接执行 |
12.3 常见面试题
临时线程什么时候创建?
- 新任务来得时候发现核心线程都在忙,且任务队列满了,并且可以创建临时线程是,才会创建临时线程,并不是一次性创建很多临时线程,根据新任务的个数来定,且临时线程也有线程个数根最大线程数挂钩。
什么时候会开始拒绝任务?
- 当核心线程和临时线程都在忙,任务队列也满了时,才会开始拒绝新任务。
12.4 线程池处理Runnable任务
使用executr方法
public class ThreadPool {
public static void main(String[] args) {
ExecutorService pools = new ThreadPoolExecutor(3, 10, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//创建任务
MyRunnable runnable = new MyRunnable();
//核心线程处理,使用execute方法
pools.execute(runnable);
pools.execute(runnable);
pools.execute(runnable);
}
}
//实现Runnable接口
class MyRunnable implements Runnable{
//重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "---->" + i);
}
}
}
12.5 线程池处理Collable任务
使用summit方法
public class ThreadPool {
public static void main(String[] args) throws Exception {
ExecutorService pools = new ThreadPoolExecutor(3, 10, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//创建任务使用线程池执行,使用summit方法
//Future是FutureTask的祖父类,所以得到返回值需要调用get()
Future submit1 = pools.submit(new MyCallable(100));
Future submit2 = pools.submit(new MyCallable(200));
Future submit3 = pools.submit(new MyCallable(300));
Future submit4 = pools.submit(new MyCallable(300));
//获得线程计算的,这里使用get方法后,他会等待线程执行结束后在返回结果,否则就会一直等待
System.out.println(submit1.get());
System.out.println(submit2.get());
System.out.println(submit3.get());
System.out.println(submit4.get());
}
}
//实现Callable接口
class MyCallable implements Callable {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return Thread.currentThread().getName() + " 1-" + n + "的和为:" + sum;
}
}
12.6 Executors工具类实现线程
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
注意:其实Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的
方法名称 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间,会被回收掉 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程代替他 |
public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池就会补充一个新的线程 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务 |
12.7 Executors弊端
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险
- 因此建议使用ThreadPoolExector来指定线程池参数,这样可以明确线程池的运行规则,避免资源耗尽风险
方法名称 | 存在问题 |
---|---|
public static ExcutorService newFixedThreadPool(int nThreads) | 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误 |
public static ExecutorService newSingleThreadExecutor() | 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误 |
public static ExecutorService newCachedThreadPool() | 创建的线程数量最大上限是Integer.MAX_VALUE,线程数量可能会随着任务1:1增长,也可能出现OOM错误 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建的线程数量最大上限是Integer.MAX_VALUE,线程数量可能会随着任务1:1增长,也可能出现OOM错误 |
十三、定时器
13.1概念
- 定时器就是一个控制任务延时调用,或者周期调用的技术
- 作用:闹钟、定时邮件发送
定时器实现方式
- 一、Timer
- 二、ScheduledExecutorService
13.2 Timer
Timer定时器的特点和存在的问题
- Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
- 可能因为其中某个任务异常使Timer线程死掉,从而影响后续任务执行
构造器 | 说明 |
---|---|
public Timer() | 创建Timer定时器对象 |
方法 | 说明 |
public void schedule(TimerTask task,long delay,long period) | 开启一个定时器,按照计划处理TimerTask任务 |
//创建Timer定时器对象
Timer timer = new Timer();
//每隔两秒执行一次打印,0表示立即开始,2000表示隔2秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "打印-->AAA 时间" + new Date());
// 如果此处执行的很慢,那么也会影响其他任务的效率(因为Timer是单线程,是顺序执行)
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//如果此处出现异常,那么其他定时任务也不会执行(因为Timer是单线程,是顺序执行)
// System.out.println(10 / 0);
}
}, 0, 2000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "打印-->BBB 时间" + new Date());
}
}, 0, 2000);
13.3 ScheduledExecutorService定时器
ScheduledExecutorService是jdk1.5引入的开发包,用来弥补Timer的缺陷,ScheduledExecutorService内部为线程池。
- ScheduledExcutorService优点:
- 基于线程池,某个任务的执行情况并不会影响其他定时任务的执行
Executors的方法 | 说明 |
---|---|
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 得到线程池对象 |
ScheduledExecutorService的方法 | 说明 |
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) | 周期调度方法 |
//线程池中创建3个线程,得到线程池对象
ScheduledExecutorService pools = Executors.newScheduledThreadPool(3);
//线程池周期调度scheduleAtFixedRate,此处设置立即开始,且每隔2秒执行一次
pools.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->AAA" + new Date());
//当该任务执行效率低时,会开启其他线程去执行其他定时器
// try {
// Thread.sleep(20000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//当该任务导致线程死掉,并不会影响其他定时器,因为还有其他线程执行
// System.out.println(10 / 0);
}
}, 0, 2, TimeUnit.SECONDS);
pools.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->BBB" + new Date());
}
}, 0, 2, TimeUnit.SECONDS);
十四、并发与并行,同步与异步
- 正在运行的程序(软件)就是一个独立的进程,线程属于进程,多个线程其实是并发和并行同时进行的。
什么是并发?
- CPU同时处理线程的数量有限
- CPU轮询为系统的每个线程服务,由于CPU切换速度快,给我们感觉这些线程在同时执行,这就是并发
什么是并行?
- 在同一个时刻上,同时有多个线程在被CPU处理并执行
个人理解:当计算机为单核时,因为cpu切换速度很快,一个核处理一个进程后,快速再去处理另一个进程,我们感觉它是同时执行,这个叫并发;当计算机多核时,例如4核,那么他可以同时执行4个进程,那么这个叫做并行。
同步和异步
- 同步是指一个线程中顺序执行,需要等待第一个执行完成后继续执行第二个;
- 异步指多个线程同时执行
十五、线程的状态
线程的状态
- 就是线程从生到死的过程,中间经历的各种状态及状态转换
- 理解线程的状态有利于提升并发编程的理解能力
Java线程的状态
- Java总共定义了6种状态
- 6中状态都定义在Thread类的内部枚举类中
线程状态 | 描述 |
---|---|
NEW(新建) | 线程刚被新建,但是并未启动 |
Runnable(可运行) | 线程已经调用了start()等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候并没有竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法又超时参数,调用他们将进入Timed Waiting状态,带有超时参数的常用方法有Thread.sleep、Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
十六、线程安全的集合
16.1 Collections类中提供的线程安全的集合获得方法
Collections类中提供了一些线程安全的集合获得方法(这些方法都是以synchronized开头),但是这些方法都是jdk1.2时提供,线程安全,但是性能不高,不推荐使用。
16.2 CopyOnWriteArrayList
写有锁,读无锁
写时会复制一份,写入后替换掉原来的地址
优点:线程安全;缺点:耗费内存,因为每次写需要复制一份。
与ArrayList用法一样。
16.3 CopyOnWriteArraySet
底层使用CopyOnWriterList实现
区别在于添加时应该使用addIfAbsent()方法实现,该方法在添加时会遍历集合,如果发现有相同元素,则放弃添加。
16.4 ConcurrentHashMap
使用方式与HashMap相同。
JDK1.7:
- 使用分段锁segment;
- 初始时采用16段,只有当添加到同一个段时,才需要互斥等待,如果不是同一个段,不需要等待;
- 最理想状态分别添加到16个段,理论可以16个线程同时添加。
JDK1.8:
- 采用CAS(compare and swap比较转换算法)机制
- 有三个核心变量,V、E、N,V是要更新的变量,E是预期值,当V==E时,认为没有其他线程修改过,则进行修改V=N,否则就认为有别的线程修改过,则取消操作。
悲观锁和乐观锁:
悲观锁就是一种互斥锁,要求只能有一个线程在操作,其他的线程需等待。
乐观锁一般需要一个状态(版本),每次操作时先下载版本号,再进行修改,修改后提交时先比较版本号,如果版本号一致,则没有人在修改时进行了修改,就可以正常提交修改,并将版本号一同修改。
synchronized就是悲观锁;后面所学的Git分布式管理工具的流程就如同乐观锁;
16.5 Queue接口
队列,遵循FIFO(First In First Out先进先出)原则
16.6 ConcurrentLinkedQueue
线程安全,高并发是性能最好的队列。
采用CAS原则
16.7 BlockingQueue
线程阻塞的队列,是Queue的子接口,增加两个无限期等待的方法。
put(E e):向队列中添加元素,如果队列中没有空间,则等待。
E take():在队列中获取元素,如果队列中没有元素,则等待。
可生产消费者模式。
16.7.1 ArrayBlockingQueue
使用数组实现的有界队列,需要固定大小。
16.7.2 LinkedBlockingQueue
使用链表实现的无界队列,默认大小Integer.MAX_VALUE。
public class Test1 {
//仓库,设置最多只能放6辆car
private static ArrayBlockingQueue queue = new ArrayBlockingQueue(6);
//线程池5个,2个工厂,3个4s店
private static ExecutorService pool = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
//工厂
Runnable c = new Runnable() {
@Override
public void run() {
//工厂生产car 最多15辆
String name = Thread.currentThread().getName();
for (int i = 0; i < 15; i++) {
Car car = new Car(i, name);
//add方法可以将car放入队列,放满后就等待,不会多生产
queue.add(car);
System.out.println(name + "生产了一辆汽车--->" + car);
}
}
};
//4s店
Runnable s = new Runnable() {
@Override
public void run() {
//4s店消费car 最多10辆
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
try {
//task方法会消费队列中的car,队列中没有可消费的就等待,不会多消费
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "消费了"+ i +"辆汽车--->");
}
}
};
//执行任务,2个工厂生产,3个4s店消费
//查看打印是否多消费或者多生产
pool.submit(c);
pool.submit(c);
pool.submit(s);
pool.submit(s);
pool.submit(s);
pool.shutdown();
}
}
class Car {
private String name;
private int id;
public Car() {
}
public Car(int id, String name) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}