1.进程与线程
进程是 OS(操作系统operating system,简称OS)分配资源的基本单位,线程是执行调度的基本单位。可能有些拗口,这样理解:
多进程是指操作系统能同时运行多个任务(程序)
这就是4个进程
多线程是指在同一程序中有多个顺序流在执行
例如播放电影可以看见图像的同时还可以听见声音,图像与声音就是两个线程,感觉不到它们之间的时差,是因为CPU执行顺序太快了,人感受不到。
微信在电脑运行时的线程内存简述图
Java程序启动至少会有两个线程启动
- 每当使用Java命令执行一个类时,实际上都会启动一个JVM,每个JVM实际上就是在操作系统中启动一个进程
- Java本身具备了垃圾回收机制,所以每个Java运行时至少会启动两个线程,一个main线程,一个是GC(垃圾回收机制)
2.创建Java线程三种方式
run()与start()区别
- 调用start方法方可启动线程,真正实现了多线程
而run方法只是thread的一个普通方法调用还是在当前线程里执行,并没有增加线程的数量
第一种:继承Thread类
第二种:实现Runnable接口
两种方式区别
继承Thread类
- 子类继承Thread类具备多线程的能力
- 启动线程:子类对象.statr();
- 不建议使用:避免OOP单继承局限性
- 实现Runnable接口
- 实现接口Runnable具有多线程能力
- 启动线程:传入目标对象+Thread对象.start();
- 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
练习项目
购买火车票,耳熟能详的多线程入手项目了,通过购买火车票项目来查看线程在CPU之间的频繁调度
public class BuyTrainTickets implements Runnable{
int ticketNum = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (ticketNum > 0) {
// Thread.currentThread().getName() 获取当前线程的名字
System.out.println("我在" + Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张车票");
}
}
}
// Main方法:
public static void main(String[] args) {
BuyTrainTickets buyTrainTickets = new BuyTrainTickets();
// new Thread(buyTrainTickets, "窗口1").start(); --->Thread t1 = new Thread(buyTrainTickets, "窗口1"); t1.start();
new Thread(buyTrainTickets, "黄牛那里").start();
new Thread(buyTrainTickets, "火车站那里").start();
new Thread(buyTrainTickets, "网上").start();
}
}
运行结果:
暴露问题:多个线程共享同一个资源的时候线程不安全,数据紊乱
多运行几次,会发现结果都不一样
第三种:实现Callable接口(了解)
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式,都需要有一个run方法,但是这个run方法有不足:
- 不能抛出异常
- 没有返回值
基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口,实现Callable接口好处:
- 有返回值
- 能抛出异常
缺点:线程创建比较麻烦
线程生命周期
3.线程常见方法
设置优先级
- 同优先级别的线程,采取的策略就是先到先服务,使用时间片策略
- 如果优先级别高,被CPU调度的概率就高(是概率高,不是一定)
- 级别:1-10,默认级别是5
Thread类(java.lang包下)
public class TestThread01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
class TestThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(i);
}
}
}
class TestThread {
// Main方法:
public static void main(String[] args) {
// 创建两个子线程,让这两个子线程争抢资源
TestThread01 testThread01 = new TestThread01();
testThread01.setPriority(10);
testThread01.start();
TestThread02 testThread02 = new TestThread02();
testThread02.setPriority(1);
testThread02.start();
}
}
join方法
join方法:当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。
注:必须先start(),再join()才有效
sleep方法
sleep():人为制造阻塞状态
运行后控制台显示运行完成,但是数字会在3秒后输出在控制台
sleep()实现秒表功能
public class Test01 {
// Main方法:
public static void main(String[] args) {
// 定义一个时间格式
DateFormat df = new SimpleDateFormat("HH:mm:ss");
while(true){
// 获取当前时间:
Date d = new Date();
// 按照上面定义的格式将Date类型转为指定格式的字符串:
System.out.println(df.format(d));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
常见面试题
sleep()、wait()和yield()的区别
- 区别
- sleep()和yield()是Thread类中的方法
- wait()是Object类中的方法
- 也就是说,在java中,所有的类都有wait()方法,而只有继承了Thread类的才有sleep()和yield()方法
- sleep()和wait()的区别
- 线程调用sleep()方法后,短暂暂停线程的运行,当前线程不会释放它的资源
- wait()方法用于线程间的通信,线程调用wait()方法后,当前线程会释放它的资源
- sleep()和yield()的区别
- sleep()是被动的(被写代码的,人为进入阻塞状态)
- yield()是主动的,当前线程已经执行的差不多了,告诉CPU“可以把我的资源让给比我线程优先级更高的线程或者跟我同级的线程去执行”,如果没有这样的线程,那么当前线程继续执行,如果有那么当前线程进入就绪状态
setDaemon方法
设置伴随线程:将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了
public class TestThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("子线程---" + i);
}
}
// Main方法:
public static void main(String[] args) {
// 创建并启动子线程
TestThread02 testThread02 = new TestThread02();
testThread02.setDaemon(true);
testThread02.start();
// 主线程输出1-10数字
for (int i = 0; i < 10; i++) {
System.out.println("主线程---" + i);
}
}
}
4.线程安全问题(重点)
不知道你们在做购买火车票的代码的时候,遇没遇见出现两张一样的火车票的情况,我在运行代码的时候,前三次都是出现2张第9张火车票
PS:图先欠着,当时忘截图了 (⁄ ⁄•⁄ω⁄•⁄ ⁄)
出现上述原因:重票 —->线程安全引起的问题。多个线程在争抢CPU资源的时候,导致资源共享出现问题。一个线程还没执行完,另一个进程就进来了。相当于一个人吃饭没吃完,碗被人抢走了 ( ̄□ ̄;)
解决方法:加 锁(lock) —> 加同步 —> 同步监视器(synchronized)
方式一:同步代码块
使用方式:
synchronized(XXXX.clsss){
// 需要被锁的代码块
}
其中XXXX.clsss 被称为同步监视器,也就是锁,原理是:当线程开始执行同步代码块前,必须先获得对同步代码块的锁定。并且任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
同步监视器总结
锁必须多个线程用的是同一把锁!!!不能更改
总结一:
- 必须是引用数据类型,不能是基本数据类型
- 也可以创建一个专门的同步监视器,没有任何业务含义
- 一般使用共享资源做同步监视器即可
- 在同步代码块中不能改变同步监视器对象的引用
- 尽量不要String和包装类Integer做同步监视器(尽管String类型唯一,但是它的值可以改变)
- 建议使用final修饰同步监视器
总结二:同步代码块执行过程
- 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
- 第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了CPU,但是没有开锁open
- 第二个线程获取了CPU,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
- 第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
- 第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
总结三:
多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块。多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
方式二:同步方法
public synchronized void testThread() {
//需要被同步的代码块
}
对于关键字synchronized修饰的方法,不需要再指定同步监视器,这个同步方法(非static方法)无需显式地指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象
注:synchronized可以修饰方法,修饰代码块,但是不能修饰构造器、成员变量等
同步方法与代码块区别
- 同步代码块的效率要高于同步方法
- 原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法
- 同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
方式三:Lock锁
Lock锁是JDK1.5之后引入新一代的线程同步方式
优先级顺序:Lock —->同步代码块 —-> 同步方法
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// open锁
lock.lock();
// 如果代码出现异常可能关闭不了锁,那样会耽误其它代码执行,所以用try catch finally 进行捕获
try {
if (ticketNum > 0) {
// Thread.currentThread().getName() 获取当前线程的名字
System.out.println("我在" + Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张车票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// close锁 --->即使有异常,锁也能关闭
lock.unlock();
}
}
}
Lock和synchronized的区别
- synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的,是虚拟机级别的
- Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,性能更优
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
线程同步的优缺点
- 如果代码线程安全,效率低
- 如果代码线程不安全,效率高
- 可能造成死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 解决方法: 减少同步资源的定义,避免嵌套同步
5.线程通信问题(重点)
生产者消费者问题
生产者消费者问题,也称有限缓冲问题,是一个线程同步问题的经典案例。
- 在缓冲区(仓库)为空时,消费者不能进行消费
- 在缓冲区(仓库)为满时,生产者不能进行生产
- 在一个线程进行生产或者消费时,其他线程不能进行消费或者生产操作,即保持线程间的同步
问题解决
因为需要保持线程间的同步,即一个线程消费(或生产)完,其它线程才能争抢CPU资源:生产者线程在生产之前,需要等待直至获取自己所需的信号量之后(即仓库为空),才会进行生产的操作;同样,对于消费者线程,在消费之前需要等待直到没有线程在访问共享区(缓冲区),再进行消费的操作,之后再解锁并唤醒其他可用阻塞线程。
实现代码
篇幅有限,便在网上找了篇文章。主要是因为懒 ¬o( ̄- ̄メ)
原理
在Java对象中有两种池:
锁池—————>sychronized
等待池————>wait(),notify(),notifyAll()
- 如果一个线程调用了某个对象的wait()方法,那么该线程进入到该对象的等待池中(并且已经将锁释放)
- 如果未来某一时刻,另外一个线程调用了相同对象的notify()方法或者notifyAll()方法,那么该等待池中的线程就会被唤醒,然后进入到对象的锁池里面去获得该对象的锁
- 如果获得锁成功后,那么该线程就会沿着wait()方法之后的路径继续执行。注意是沿着wait()方法之后
wait()方法和notify()方法,是必须放在同步方法或者同步代码块中才有效的.因为在同步的基础上进行线程的通信才是最有效的
线程生命完整周期图
Condition
Oralce官方文档:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Condition.html
Condition是在Java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
- 它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition
- 一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列
- 在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列
- Object中的wait(),notify(),notifyAll()方法是和”同步锁”(synchronized关键字)捆绑使用的;而Condition是需要与”互斥锁”/“共享锁“捆绑使用的
- 调用Condition的await()、signal()、signalAll()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
void await() throws InterruptedException { }
造成当前线程在接到信号或被中断之前一直处于等待状态
与此 Condition 相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一之前,当前线程将一直处于休眠状态:
- 其他某个线程调用此 Condition 的 signal() 方法,并且碰巧将当前线程选为被唤醒的线程
- 其他某个线程调用此 Condition 的 signalAll() 方法
- 其他某个线程中断当前线程,且支持中断线程的挂起
- 发生“虚假唤醒”
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证它保持此锁
void signal() {}
唤醒一个等待线程
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁
void signalAll() {}
唤醒所有等待线程
如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁