▊ 线程
① Thread是个表示线程的类。ta有启动线程、连接线程、闲置线程等方法
② Java中每个线程都有独立的执行空间(在栈上独立。而堆是公共空间)
③ 如何启动自定义的新线程?
❶ 写一个实现Runnable的类(Thread()需要一个任务,这个任务是一个Runnable对象)
❷ 重写run()方法(Runnable是一个接口,且只有一个run()方法,run()就是那个具体的任务;而且run()是抽象方法,必须被重写)
❸ 启动Thread()
④ 直接看一个典型的demo,立马就能明白上面的文字 ↓
class MyRunnable implements Runnable{ // 实现Runnable接口(根据is-A测试,就可以说这是一个Runnable类)
public void run() { // 必须重写覆盖的抽象方法:run()
go();
}
public void go() {
doMore();
}
public void doMore() {
System.out.println("Loli saikou!!!");
}
}
public class test {
public static void main(String[] args) {
Runnable threadJob = new MyRunnable(); // Runnable类作为"任务"
Thread myThread = new Thread(threadJob); // 创建Thread对象时,传入"任务"
myThread.start(); // 启动新线程
System.out.println("Loli suki!!!"); // 这是主线程中的语句
}
}
图解更清晰 :
注意:
❶ 输出结果是随机的:我们不能确定是"Loli saikou!!!"还是"Loli suki!!!"会先输出
因为新线程启动后,主线程和新线程便开始反复梗跳
❷ 这是由调度器(scheduler)控制的;但调度器不能保证执行的时间和顺序,也没有任何API可以调用调度器;
甚至,调度器在同一个JVM中执行同一个程序也会有不同的做法
⑤ Thread对象可以重复使用吗?再start()一次?
不行。一旦线程的run()方法完成后,该线程就不能再重新启动。因为该线程结束一次后,作为一个线程,它彻底死了。
Thread对象可能还呆在堆上,如同活着的对象一般还能接受某些方法的调用,但已经永远失去了线程的执行性,只剩下对象本身。
▊ 并发与同步化
目前大致可以有两种方法解决并发性问题:
- 同步方法(这个模块所讲的)
- 同步代码块(通常配合一个锁,以及[等待唤醒机制])
它们都要使用到
synchronized
关键字
❶ 并发性(并行性)问题是多线程的典型问题。并发性问题会引发竞争状态,竞争状态会引发数据的损毁
❷ 两个经典的并发性问题:
❸ 怎样解决上面的问题呢?
他们需要对账户存取上一道锁(术语为monitor,即监视器)
❹ 因此引入了同步化(Synchronized)
使用Synchronized关键词修饰符可以防止两个线程同时进入同一对象的同一方法;基本原理如下:
同步化锁住的是方法而不是数据,当一个线程进入该方法后,取得钥匙并将该方法锁上;
另一个线程企图进入该方法时,会因为没有钥匙而一直处于等待状态;
❺ 锁有对象锁和类锁;上面说的是对象锁
注意,对象锁就是方法锁,不要理解为“这个对象被锁了”,被锁的是对象内部的同步方法(★)
❻ 方法锁让这个方法具有了原子性;事实上,当一个对象有多个同步化方法时,一个线程在访问其中一个时,另一个线程也无法访问这个对象的其他同步化方法(但可以被访问非同步方法)
也就说,在这个层面上,这多个同步化方法也获得了原子性(实质上锁多个同步方法,只用了一个锁)
因此,锁一个同步化方法,则这个对象的所有同步化方法都被锁,这就是“对象锁”名称的由来;
但不要从对象的层面去理解,要看清本质:锁的是方法,不是对象
❼ 类锁是锁住了多个实例对象
仔细想想,如果同步化的是一个静态方法,这个锁不就是类层面的了吗?
和对象锁一样,在层面上锁的是类层面,实质上锁的还是方法
❽ 非静态方法和静态方法的同步化是两个层面的(对象层面、类层面),且它们是彼此独立的。
也就说,在两个线程中,对同一个对象,我们是可以调用一个同步化的非静态普通方法和一个同步化的静态方法的
❾ 可以看出,当同步化一个方法后,不仅这个方法的内容成了原子,这个对象的所有被同步化的方法也成了原子
这种双重含义有时我们并不需要,有时候同步化代码块是个更好的选择:
synchronized(object){
int i = count;
count = i + 1;
}
❾ 同步化是有代价的。
- 同步化的方法会有额外的成本。比如查询“钥匙”对性能的损耗
- 同步化的方法会让你的程序因为要同步并行,而慢下来。话句话说,同步化会强制线程排队等待
- 最可怕的是死锁(deadlock)现象
关于死锁产生的过程 —— 简单来说就是出现了“交叉”:
线程A进入foo对象的同步化方法
↓
调度到线程B,线程B进入bar对象的同步化方法
↓
bar对象的方法需要调用foo对象的同步化方法,但线程A把foo的对象锁钥匙拿走了,线程B只能等待
↓
调度到线程A,foo对象的方法也需要调用线程B的方法,也没钥匙,也只能等待
↓
这样,不管此时调度器决定执行哪个线程,A或B都只能等待、僵持…
▇ 线程状态
▇ 线程通信
/*
线程之间的通信案例:【消费者与生产者模型】
需求:
顾客告知老板包子的种类和数量,之后放弃cpu的执行,进入waiting状态(无限等待状态);
老板花5s做包子,做好包子后唤醒顾客
注意:
1.顾客与老板线程必须用同步代码块包裹起来,保证只有一个在执行
2.同步使用的锁必须保证唯一
*/
public class Hello {
public static void main(String[] args) {
// 创建锁对象
Object lock = new Object();
// 创建顾客线程
new Thread(){
@Override
public void run(){
synchronized (lock){
System.out.println("告知老板包子的种类和数量");
try {
lock.wait(); // 放弃CPU执行,进入无线等待状态
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("被唤醒,开吃!");
}
}
}.start();
// 创建老板线程
new Thread(){
@Override
public void run(){
try {
Thread.sleep(5000); // 花5s做包子
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock){
System.out.println("做好包子啦!");
lock.notify(); // 唤醒顾客(注意使用同一个对象lock)
}
}
}.start();
}
}
▇ 等待唤醒机制
等待唤醒机制就是用于解决线程间的通信问题的,使用到了如下的方法:
wait
:线程放弃CPU资源,不再参与调度,进入waiting状态。它在等待其他线程的"通知(notify)",使这个对象监视器(锁)上等待的线程从wait set中释放出来,重新进入调度队列(ready queue)中notify
:所通知对象的wait set中的一个线程释放notifyAll
:所通知对象的wait set中的所有线程释放
注:
- 等待(wait)的线程被通知(notify)后,也不是立即执行的。当初它被中断的地方是在一个同步块内,此时这个同步块需要再次去尝试获取锁——这很有可能面临其他线程的竞争。成功获取锁后,才能从当初wait中断的地方接着执行。
- 显然,wait方法和notify方法需要使用同一个锁对象
- wait方法和notify方法一定要在同步代码块或者同步方法中使用——因为锁对象必须同步
▇ 线程池
其实就是一个容纳多个线程元素的容器。
优点不言而喻:
- 降低资源消耗。减少了创建和销毁线程的次数,每个线程可以重复使用。
- 提高响应速度。任务到达时,直接从线程池获取一个线程,而不用等待线程创建。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中线程元素的数目。
代码实现
// 1.使用线程池的工厂类Executors提供的静态方法newFixedThreadPool生产出一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
// 2.给线程池submit一个任务,则线程开启,run方法start
es.submit(new MyJob());
es.submit(new MyJob());
es.submit(new MyJob());
es.submit(new MyJob());
// 3.销毁线程池(一般不使用)
es.shutdown();
▇ 经典常见考试/面试题
比较基础,但很有利于对线程的理解与学习
Question01:当一个线程进入了对象的一个方法后,其他线程可否进入该对象的其他方法?
Answer:
总的来说分两种情况:
- 如果进入的这个方法被同步了(synchronized),则其他线程可以进入该对象的非同步的普通方法,不可以进入该对象的其他同步化方法;
- 如果进入的这个方法未被同步,在其他线程可以进入
再补充说明一下:
- 就算是其他方法是加了关键词的同步方法,若内部有wait,则其他线程可以进入;
- 静态方法和非静态方法的同步化是彼此独立的。上面的回答都建立在方法均为非静态普通方法的基础上。
Question02:sleep与wait的区别?
Answer:
- sleep():Thread类的static方法,不释放锁,只是让出CPU
其作用是:正在执行的线程主动让出CPU给其他线程,指定时间结束后才回到这个线程上继续执行。然而,如果当前线程进入了同步锁,sleep方法不释放锁。也就是说,即使当前线程使用sleep让出CPU,其他线程依旧会被同步锁挡住,无法执行。 - wait():Object类的final方法,释放锁;需要notify()/notifyAll()通知唤醒它,或自己醒来
其作用是:在一个已经获得同步锁的线程中,暂时让出自己的同步锁给其他线程;只有当其他线程针对此对象调用notify/notifyAll(或者自己醒来)才能解除waiting状态。再次获取锁后才能接着wait处继续运行 - notify()/notifyAll()方法:Object类的final方法,不释放锁
就和上面刚刚提到的那样,notify/notifyAll并不释放锁,只是"唤醒某个wait的线程,通知它可以去参与锁的竞争了"。还是那句话,那个刚刚被唤醒的线程,并不会马上获得锁,起码需要执行完notify/notifyAll后面的代码(因为notify/notifyAll在一个synchronized里)。
另外值得一提的是,notify()方法唤醒一个waiting状态的线程,但不能指定唤醒哪个线程(当只有一个线程在等待时才能确定),也不是按优先级,而是由JVM决定;notifyAll()方法唤醒的多个线程,包括正在执行的线程,也都是"平等"的去竞争