1. 通过继承Thread类的方式创建新的线程时为什么要覆盖父类的run()方法呢?
记住三个关键字:线程 & 任务 & 启动
创建线程是为了创建一条新的执行路径,并在这个新路径去执行某任务(也就是在这个新路径去执行某代码),来达到和其他线程(比如主线程)中的任务同时运行的目的。
Question : 那个这个新路径的具体任务在哪儿体现呢?
Answer : 在run()方法中来体现的。run()方法就是封装自定义线程任务的函数。run()方法中定义的就是线程要运行的任务代码。
线程创建并覆盖run()方法之后,调用线程的start()方法来启动线程,线程启动了那是不是就要执行任务了呢,那任务在哪儿呢,在覆写的run()方法里呢。
start()方法的作用:让该线程开始执行;Java虚拟机调用该线程的run()方法,调用run()方法就是开始执行任务了。
2. 创建线程有两种方式,可不可以只提供继承Thread类这一种方式?
先直接说结论:不可以,因为Java语言不支持多继承。
回顾一下创建线程的两种方式:
- 创建一个Thread类的子类,并覆写父类中的run方法。
- 创建一个实现Runnable接口的类的实例对象,这个类主要就是实现Runnable接口中的run方法。然后使用Thread(Runnable runnable)构造方法创建线程,这里的runnable参数就是那个实现Runnable接口的类的实例对象。
假如我创建的一个类已经继承了一个Thread类之外的父类了,那这个类就不能在继承Thread类了,因为Java语言不支持多继承。那这样的话就没法创建线程了。
鉴于以上原因,Java还需要提供另外一种不是通过继承Thread类创建线程的方式来创建线程。
所以,第二种方式是必不可少的。
实现Runnable接口的方式创建线程的好处:
- 将线程的任务从线程的子类中分类出来,进行了单独的封装。或者说书,安装面向对象的方式将线程需要执行的任务进行了单独的封装。这是一种思想。
- 避免了Java语言单继承的局限性。
3 线程安全问题的现象
if (num > 0)
{
//| | |
//| | |
//线程1进来了,但是还没执行num--,CPU将执行权分给QQ音乐的线程了
//| | 线程2进来了,但是还没执行num--,CPU将执行权分给线程3了
//| | |线程3进来了,但是还没执行num--,CPU将执行权分给eclipse线程了
//| | |
//| | |
num--;
System.out.println(num);
}
如上图,每个线程在进入if代码块的时候,num都是大于0的。
线程1进来了,但是还没执行num--,CPU将执行权分给QQ音乐的线程了;
线程2进来了,但是还没执行num--,CPU将执行权分给线程3了;
线程3进来了,但是还没执行num--,CPU将执行权分给eclipse线程了。
最终这3个线程都把num--和输出语句都执行了一遍,可能导致num的输出值变成负数了。
4. 线程安全问题产生的原因
产生前提:
- 多个线程在操作共享的数据
- 操作共享数据的线程代码有多行
一句话说明产生原因: 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算, 就会导致线程安全问题的产生。
解决思路: 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候, 其他线程时不可以参与运算的。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
比如用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象)
{
需要被同步的代码;
}
5. 同步的好处,同步的弊端,同步的前提
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁,这样就增大了资源开销。
同步的前提:同步中必须有多个线程并使用同一个锁。
6. 写一个死锁示例
说明下:下面的死锁使用的是同步嵌套的方式。
package javapractise;
public class DeadLock {
public static void main(String[] args) {
Task task0 = new Task();
Task task1 = new Task();
Thread t0 = new Thread(task0);
Thread t1 = new Thread(task1);
t0.start();
task1.flag = false; //
t1.start();
}
}
class MyLock {
public static final Object LOCK_A = new Object();
public static final Object LOCK_B = new Object();
}
class Task implements Runnable {
public boolean flag = true;
@Override
public void run() {
if (flag) {
// 嵌套锁
synchronized (MyLock.LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...if分支....第1行输出");
synchronized (MyLock.LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...if分支....第2行输出");
}
}
} else {
// 嵌套锁
synchronized (MyLock.LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...else分支....第1行输出");
synchronized (MyLock.LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...else分支....第2行输出");
}
}
}
}
}
- 运行结果1(死锁情况)
if分支和else分支的第二行输出语句都没有打印,程序锁死了。 - 运行结果2(正常情况)
if分支和else分支的代码都运行完毕了
再写一个形式简单一点的
package javapractise;
public class DeadLock {
// 创建两个对象,充当锁
public static final Object LOCK_A = new Object();
public static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 创建任务run0
Runnable run0 = new Runnable() {
@Override
public void run() {
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...01");
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...02");
}
}
}
};
// 创建任务run1
Runnable run1 = new Runnable() {
@Override
public void run() {
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + "...03");
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + "...04");
}
}
}
};
// 创建2个线程,并启动线程
Thread th0 = new Thread(run0);
Thread th1 = new Thread(run1);
th0.start();
th1.start();
}
}
- 运行结果1(死锁情况)
- 运行结果2(正常情况)
7. 非静态的同步函数使用的锁是什么? 静态的同步函数使用的锁又是什么?同步代码块的锁又是什么呢?
先说答案:
非静态同步函数使用的锁是this.
静态同步函数使用的锁是该函数所属的类的字节码文件对象(类名.class对象).
同步代码块使用的锁可以是任何对象.
非静态函数都有自己所持有this引用,而static函数不持有this。
同步函数仅仅是函数带了同步性,同步本身不带锁吧,那同步函数应该是函数带的锁吧,非静态函数都有自己所持有this引用,那就用this作为非静态函数的锁。而static函数不持有this,那就用函数所属的类的字节码文件对象作为锁吧。
8. 我们一般啊,使用同步代码块比使用同步函数要好。
9. 线程间通讯示例--wait/notify/notifyAll示例
线程间通讯,通过等待/唤醒机制实现
涉及的方法:
- wait,让线程冻结,没有了执行权,也没有了执行资格
- notify,唤醒线程池中的一个线程(任意),有了执行资格,等待CPU分配执行权
- notifyAll,唤醒线程池中所有的线程,有了执行资格,等待CPU分配执行权
注意:
上面的这次方法都必须在同步中使用。因为这些方法都是操作线程状态的方法,操作线程状态的时候必须要明确到底操作的是哪个锁上的线程。如下,LOCK_B.notify()是无法唤醒在等待LOCK_A锁的线程的。
LOCK_A.wait();
LOCK_B.wait(); LOCK_B.notify();
/*
* 写两个线程,要求线程A给公共资源赋值一次,线程B就获取一次公共资源。也就是线程A赋值-->线程B获取-->线程A赋值-->线程B获取......
* 思路 : 线程间的通讯使用wait和notify方法
*/
public class ThreadWait {
public static void main(String[] args) {
// 创建资源
Resource resource = new Resource();
// 创建任务
TaskA taskA = new TaskA(resource);
TaskB taskB = new TaskB(resource);
// 创建线程
Thread threadA = new Thread(taskA);
Thread threadB = new Thread(taskB);
// 线程启动
threadA.start();
threadB.start();
}
}
class Resource {
private String name;
private String sex;
private boolean flag = false;
public void setNameAndSex(String name, String sex) {
synchronized (this) {
if (flag) { // 这里使用while更好
try {
this.wait(); // 冻结目前持有this锁的线程(这里指的是给name和sex赋值的线程)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name; // 给公共资源赋值
this.sex = sex; // 给公共资源赋值
this.flag = true; // 给name和sex赋值已经将完成了,将flag置为true,防止不停的赋值
this.notify(); // 唤醒在等待this锁的某个线程(这里指的是读取name和sex的线程)
}
}
public void getNameAndSex() {
synchronized (this) {
if (!flag) { // 这里使用while更好
try {
this.wait(); // 冻结目前持有this锁的线程(这里指的是读取name和sex的线程)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name + "......" + sex); // 获取公共资源
this.flag = false; // 获取name和sex已经完成了,将flag置为false,防止不停的获取值
this.notify(); // 唤醒在的等待this锁的线程(这里指的是给name和sex赋值的线程)
}
}
}
class TaskA implements Runnable {
Resource res;
boolean flag = false;
TaskA(Resource res) {
this.res = res; // 任务A持有公共资源Resource
}
@Override
public void run() {
int x = 0;
for (int i = 0; i < 50; i++) {
if (x == 0) {
res.setNameAndSex("Jack", "man");
} else {
res.setNameAndSex("丽丽", "女");
}
x = (x + 1) % 2; // 为了让交替着赋值为Jack和丽丽
}
}
}
class TaskB implements Runnable {
Resource res;
TaskB(Resource res) { // 任务B也持有公共资源Resource
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
res.getNameAndSex();
}
}
}
程序运行结果:
10. 为什么操作线程状态的wait/notify/notifyAll方法要定义在Object类中,而不定义在Thread类中?
我们知道这三个方法都是被锁对象调用的,比如LOCK.wait() / LOCK.notifyAll()。而任何对象都可以作为锁,任意对现象都可以调用的方法那得定义在Object类中。
11. 等待唤醒机制——生产者消费者模型01(wait/notifyAll)
这里介绍多生产者,多消费者的问题。
本例的代码有两个生产者,两个消费者,生产者生产一个,消费者消费一个。
问题1 : 标记判断为什么要使用while替换上一个例子中的if ?
if判断标记,只判断一次,会导致不该运行的线程运行了,会出现数据错误的情况。
什么时候会出现错误呢?比如某个时刻flag标记是true,那么生产者1线程进入wait等待,过了一会儿,别人把他唤醒了,他就直接往下执行代码了,但是由于是多线程,生产者2号线程在生产者1号线程被唤醒但还没来得及执行代码的时候又将flag改成了true,此时生产者1线程直接去执行下面的生产代码就有问题了。
while判断标记,沉睡的线程被唤醒后要在进行一次flag标记判断,解决了线程获取执行权后,是否真的应该运行!
问题2 : 为什么要使用notifyAll替换上一个例子中的notify
notify: 简单说,while判断标记 + notify会导致死锁。可以把下面的代码中notifyAll改成notify在电脑跑一下,肯定会下出现死锁的。
notifyAll解决了本方线程一定会唤醒对方线程的问题,因为他会把所有线程都唤醒。
代码演示
package p1.thread;
class Resource {
private String name;
private int count = 0;
private boolean flag = false;
public synchronized void produce(String name) {
while (flag) // 标记判断为什么要使用while替换上一个例子中的if
try {
this.wait();
} catch (InterruptedException e) {
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产..." + this.name);
flag = true;
notifyAll(); // 为什么要使用notifyAll替换上一个例子中的notify
}
public synchronized void consume() {
while (!flag)
try {
this.wait();
} catch (InterruptedException e) {
}
System.out.println("......" + Thread.currentThread().getName() + "...消费..." + this.name);
flag = false;
notifyAll();
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.produce("手机");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.consume();
}
}
}
class ProducerConsumerDemo {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
12. 等待唤醒机制——生产者消费者模型02(await/signal)
创建一个锁,再通过这个锁获取两组监视器,一组监视生产者,一组监视消费者
jdk1.5以后将锁封装成了对象。
Lock接口: 它的出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成显式锁操作。 同时操作更为灵活。可以一个锁上加上多组监视器。
-
lock() : 获取锁。
-
unlock() : 释放锁,通常需要定义finally代码块中。
Condition接口:它的出现替代了Object中的wait notify notifyAll方法。
将这些监视器方法单独进行了封装,变成Condition监视器对象。可以和任意锁进行组合。
- await() --> wait()
- signal() --> signal()
- signalAll() --> signalAll()
核心代码
// 创建一个锁对象
Lock lock = new ReentrantLock();
// 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
// code...
producer_con.signal();
// code...
consumer_con.signal();
代码演示
package p2.thread;
import java.util.concurrent.locks.*;
public class ProducerConsumerDemo2 {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t0 = new Thread(pro);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
class Resource {
private String name;
private int count = 1;
private boolean flag = false;
// 创建一个锁对象
Lock lock = new ReentrantLock();
// 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void produce(String name) {
lock.lock(); // 【获取锁】
try {
while (flag)
try {
producer_con.await();
} catch (InterruptedException e) {
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName() + "...生产..." + this.name);
flag = true;
consumer_con.signal(); // 生产完一个商品后通知消费者进行消费
} finally {
lock.unlock(); // 【释放锁】,在finally释放,防止try语句中的代码发生异常时候没有进行释放锁
}
}
public void consume() {
lock.lock(); 【获取锁】
try {
while (!flag)
try {
consumer_con.await();
} catch (InterruptedException e) {
}
System.out.println("........" + Thread.currentThread().getName() + "...消费..." + this.name);
flag = false;
producer_con.signal(); // 消费完一个商品后通知生产者进行生产
} finally {
lock.unlock(); // 【释放锁】,在finally释放,防止try语句中的代码发生异常时候没有进行释放锁
}
}
}
class Producer implements Runnable {
private Resource r;
Producer(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.produce("烤鸭");
}
}
}
class Consumer implements Runnable {
private Resource r;
Consumer(Resource r) {
this.r = r;
}
public void run() {
while (true) {
r.consume();
}
}
}
13. 上面两个例子分析一下
使用while判断标记 + notifyAll的形式可以实现线程之间通讯,而且不会死锁。那为什么还要搞一套Lock + condition的方式来实现同样的功能呢?
- 可以选择性的是实现唤醒对方线程的效果。(notifyAll他会唤醒本方线程还会唤醒对方线程,而唤醒本方线程是没有意义的,因为flag标志不满足。
- 对锁的造作更加灵活。
- 同步的隐式锁操作变成显式锁操作。
14. 造成死锁的常见方式:
- 锁嵌套
- while + notify(解决办法notify --> notifyAll)
15. sleep和wait的区别
- wait可指定时间,也可以不指定时间,而sleep必须执行时间
- 在同步时,对CPU的执行权和锁的处理方式不同。
- wait 释放执行权,且释放锁(必须释放锁,如果不释放锁,怎么让别人来唤醒它呀)
- sleep 释放执行权,但是不释放锁(它不需要被别人唤醒)
16. 线程结束方式
- stop()方法 已经废弃的方法,因为它不安全
- run方法的方法体执行完毕了就结束了
那么怎么来控制任务结束呢?
定义标记:控制循环通常用定义标记来完成。
但它也有一些解决不了的问题或场景,比如线程处于冻结状态,就无法读取标记,就一直被冻结中,线程任务就无法结束了。
针对以上情况,可以使用如下方法解决
- interrupt()方法
它会将线程从冻结状态强制性恢复到运行状态中,让线程强制具有CPU执行资格,但是由于是强制性的,会发生InterruptException异常。记得要处理这个异常。
就像,催眠师把你催眠了,然后他接了个电话出国了,那你就没办法被接触催眠了,这个时候使用interrupt()方法把你给唤醒了,相当于给你泼了一盆冷水,你就醒了,但是由于不是正常的叫醒的,你起来的把脑袋磕了一下,头一直疼。然后你还得赶紧赶紧揉一揉,处理这个异常。
- 守护线程 setDaemon()方法 将这个线程设为后台线程,前台线程结束了,后台线程就结束了。
setDaemon()用法:
线程1.setDaemon(); // 在线程启动之前就得设置
线程1.start();
- 临时加入线程 join()
线程a的方法体中的某一行有一句线程B.join(),那个线程a执行到这一行的时候就停止了,一直等线程B执行完自己才能继续执行。
可以比喻成插队。