① 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;
}
❾ 同步化是有代价的。
关于死锁产生的过程 —— 简单来说就是出现了“交叉”:
线程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中的所有线程释放注:
其实就是一个容纳多个线程元素的容器。
优点不言而喻:
代码实现
// 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:
总的来说分两种情况:
再补充说明一下:
Question02:sleep与wait的区别?
Answer: