多线程(线程的生命周期&同步代码快&锁&等待唤醒机制)
一:线程的生命周期
所谓生命周期就是从生到死的过程中经历了哪些阶段。
先用人来举例:人刚开始是小婴儿状态(婴儿),慢慢长大变成了单身状态(单身),然后找个女朋友结婚生子(已婚),GG了。但是人生总是充满了变数,有变数就会有意外(意外状态),(离婚)。只能调节好心情继续生活,重新变成了单身状态。
首先创建线程的时候是新建状态(新建)。新建完了之后要调用start方法运行线程,此时就变成了就绪状态(就绪)。注意是在调用完了start方法之后才变成就绪状态。在就绪状态下,线程开始抢夺CPU的执行权。注意,是正在抢夺,还没有抢到。没抢到就无法执行代码,所以就绪状态是有执行资格,但没有执行权。什么意思?有执行资格就是你可以有资格去抢资格的执行权,没有执行权就是你现在还没有抢到,不能去执行代码 ,所以说是没有执行权的。所以在就绪状态下,线程干的事情就是不停的抢CPU。抢到了之后就会变成运行状态(运行),运行状态下,线程就会运行代码,所以他是有执行资格也是有执行权的。在运行的过程当中,CPU的执行权是有可能被其他线程抢走的,一旦被抢走,此时又会回到就绪状态。如果当前线程把run方法里面所有的代码都执行完了,此时线程就会死亡,变成垃圾(死亡)。如果在运行的过程中,遇到了sleep方法,此时线程就会阻塞(阻塞)。阻塞就是等着,此时什么也干不来,他不能去抢CPU的执行权,也不能执行代码,所以他是没有执行资格,也没有执行权的。当睡眠的时间到了之后,他就会变成就绪状态,开始重新抢夺CPU的执行权。
提问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
不会,因为sleep方法执行完了之后,线程会进入就绪状态,此时需要重新去抢夺CPU的执行权,只有再次抢到了才回去执行代码。。
二:线程的安全问题
(1)需求:某电影院目前正在上映国产大片,共有100张票,而他有三个售票窗口,情设计一个程序模拟该电影院卖票。
见下一点
三:同步代码块
卖票引发的安全问题:
(1)相同票出现了多次
(2)出现了超出范围的票
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (ticket < 100) {
ticket++;
System.out.println(getName() + "正在卖第" +ticket + "张票");
} else {
break;
}
}
分析一下代码就知道为什么了:
重复票:假设现在有三条线程,三条线程要操作的共享数据刚开始为0。线程开启之后,线程在while(true)的地方抢夺CPU的执行权,谁抢到了就会继续往下执行。假设线程1在一开始的时候抢到了执行权,线程1就会继续往下执行,此时是满足ticket < 100的条件的,所以线程1就会进入到if当中,进去之后立马睡100ms,睡觉的时候是不会去抢夺CPU的执行权的,CPU的执行权一定会被其他线程抢走。假设线程2抢到了,线程2就会继续往下执行,此时也是满足ticket < 100的条件,所以线程2也会进入到if里面,进入之后同样睡100ms,此时CPU的执行权就会被线程3抢走,线程3也会进去睡100ms。时间到了之后,这三条线程会陆陆续续醒来执行下面的代码。假设线程1醒来了,抢到了CPU的执行权,线程1就会继续往下执行,ticket++,由0变成了1,但是自增之后还没来得及打印,CPU的执行权又被线程2抢走了,线程2在这里也ticket++,ticket就会从1自增为2,所以我们要知道一句话:就是线程在执行代码的时候,CPU的执行权随时有可能会被其他线程抢走。假设现在又被线程3抢走了,线程3也自增,ticket就变成了3。那么在这个时候,不管是线程1还是线程2还是线程3,在打印票号的时候,打印的都是3!!这就是重复票的由来。根本原因就是线程在执行的时候,他是具有随机性的,CPU的执行权有可能随时会被其他线程抢走,这是第一个原因。
卖超出范围的票:假设现在ticket已经到99了,此时还是3条线程在抢夺CPU执行权。假设线程1抢到了,然后睡了100ms.睡的时候CPU又被线程2抢走了,线程2也进去睡100ms。线程3再进去睡100ms,睡完之后也会陆陆续续醒来。假设线程1醒来了,抢到了CPU的执行权,然ticket++,99变成了100,自增完了之后还没来得及打印,CPU又被线程2抢走了,线程2ticket++,同样还没有打印,CPU又被线程3抢走了,线程3页ticket++,这时候ticket已经等于102了。这个时候不管是线程1还是线程几,他们打印的都是102。原因也是因为线程执行的时候是具有随机性的。
问题发现了,怎么改呢?
假设能把操作共享数据的代码给锁起来,当有线程进去之后,其他的线程就算抢夺到了CPU的执行权,也得在外面等着,不让他进去。只有当线程出来了,其他的线程才能进去。说白了就是把操作共享数据的这段代码锁起来,让所有的线程在这段代码当中能轮流执行,就不会出现上面的两个问题了。
(3)同步代码块
同步代码块:把操作共享数据的代码锁起来
格式:
synchronized (锁) {
// 操作共享数据的代码
}
特点:
1、锁默认打开,有一个线程进去了锁自动关闭
2、里面的代码全部执行完毕,线程出来,锁自动打开
卖电影票的完整代码
public class ThreadDemo1 {
public static void main(String[] args) {
// 创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
// 起名字
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 开启线程
t1.start();
t2.start();
t3.start();
}
}
class MyThread extends Thread {
// 表示这个类的所有对象,都共享ticket数据
static int ticket = 0;
static Object object = new Object();
public void run() {
while (true) {
// 锁对象一定要保证是唯一的
synchronized (object) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (ticket < 100) {
ticket++;
System.out.println(getName() + "正在卖第" +ticket + "张票");
} else {
break;
}
}
}
}
}
四:同步代码块的两个小细节
(1)synchronize同步代码快不能卸载循环的外面,比如:
// 锁对象一定要保证是唯一的
synchronized (object) {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (ticket < 100) {
ticket++;
System.out.println(getName() + "正在卖第" +ticket + "张票");
} else {
break;
}
}
因为这样的话,第一个抢到CPU执行权的人进去了,要把循环执行完毕了,外面的才能进去。
(2)锁对象,一定要是唯一的
如果不是唯一的,会有什么效果??
public void run()