sleep、wait、yield、join(卖票卖票)
1、首先是经典的双线程卖票
public class Test extends Thread{
private int num = 1000;
@Override
public void run() {
while (this.num>0){
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num--;
}
}
public static void main(String[] args) {
Test test1 = new Test();
Thread thread1 = new Thread(test1, "一窗口");
Thread thread2 = new Thread(test1, "二窗口");
thread1.start();
thread2.start();
}
}
当然以上代码也就产生了经典的一票多卖的问题,产生的原因是因为线程在修改一个变量的值时,会从主存将此变量的值拷贝一个副本,每次是先修改的副本,然后再更新主存中的值,由此就造成了其它线程获取主存中值时获取到的不是最新的值,也就造成了卖票重复。
如果有的同学电脑运行速度飞快,不会出现这样问题,可以参照3.1的代码,模拟此问题。
2、synchronized
可以保证同一时刻被synchronized修饰的代码块只被一个线程所访问,即对线程建立了同步锁。如果作用于静态方法,则锁的是这个方法所在的类;作用于其它,则锁的是实例出的对象。
2.1、run方法修改如下
public void run() {
while (this.num>0){
synchronized (this){
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
}
}
注意,synchronized不要设置在方法上或者循环外边啊,因为设置在循环之外,一窗口获取到同步锁之后,由于循环没有执行完,会一直占据着同步锁,因此票都需要一窗口来卖,可恶,996出现了!
2.2、run方法优化
以为这样就可以高枕无忧了嘛,当然不是,你会发现有时某个窗口会卖出0号票。这是为什么呢?因为可能会出现线程进入while时,票还有剩余,但是由于没获取到同步锁,只能在同步锁代码外等待。当它获取到同步锁后,票已经卖完了,因此就卖出了0号票。如果线程足够多,甚至可能卖出-1号票,-2号票哦。那么加个判断吧
public void run() {
while (this.num>0){
synchronized (this){
if (num>0){
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
}
}
}
3、sleep方法
3.1、sleep方法是在Thread类中定义的方法,可以让线程暂时执行。
在执行1中的代码时,可能有些同学的电脑运行飞快,100张票瞬间就被窗口一卖光了,因此不会出现重复售票的问题。这可不行,工作中一定要学会摸鱼,因此我们让每个线程卖票的时候都休息20ms吧。
@Override
public void run() {
while (this.num>0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
}
3.2、sleep执行后,只会阻塞,不会释放已经获得的锁。
结论如上,sleep方法是不会释放锁的,接下来我们用代码证明下
@Override
public void run() {
while (this.num>0){
synchronized (this){
if (num>0){
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
try {
if (Thread.currentThread().getName().equals("1号窗口")){
Thread.currentThread().sleep(2000);
System.out.println("一号窗口好累,所以睡了会觉");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
如上,一号窗口在睡觉的时候,二号窗口并没有卖票,因为一号窗口占据了锁。而且一号窗口睡了不止一次觉,说明线程执行sleep方法后可以自动唤醒,再次执行。
因此,结论很明显了,sleep方法执行后,线程进入阻塞状态,但是不会释放锁。注意阻塞状态的线程等到sleep时间结束就可以进入到可运行状态,然后等待获得cpu资源调度后,就可继续运行。
4、wait
4.1、wait参数为0
此方法调用后,会使线程阻塞,释放出当前锁。如果wait未设置参数,则默认为0,将会一直等待被唤醒;如果设置了参数,则会在超过时间后自动唤醒。如下,来个未设置参数即默认为0的
public void run() {
while (this.num>0){
synchronized (this){
if (num>0){
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
try {
if (Thread.currentThread().getName().equals("1号窗口")){
System.out.println("一号窗口好累,所以睡了会觉");
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.2、notifyAll唤醒
查看打印结果发现,一号窗口卖了一次票之后就不再卖票了,一号窗口,卒!!!而且会发现票卖完后,程序一直未停止,因为它还在等待被唤醒。如果要唤醒,可以执行下面方法,可以看到窗口一一会睡觉一会卖票了。
public class Test extends Thread{
private int num = 10000;
@Override
public void run() {
while (this.num>0){
synchronized (this){
if (num>0){
if (num<=9000){
this.notifyAll();
}
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
try {
if (Thread.currentThread().getName().equals("1号窗口")){
this.wait();
System.out.println("一号窗口好累,所以睡了会觉");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Test test1 = new Test();
Thread thread1 = new Thread(test1, "1号窗口");
Thread thread2 = new Thread(test1, "2号窗口");
thread1.start();
thread2.start();
}
}
结论:wait方法不携带参数则默认为0,此时线程将会一直处于等待队列,直到可以被notify或notifyAll唤醒。如果方法中参数不为0,则会在超时之后自己进入可运行状态。而且参照4.1中的运行实例可以看到线程处于等待队列时会让出同步锁。
5、yield
yield方法和sleep方法一样都是暂停正在执行的thread对象,不会释放锁,但是不同的是yield方法在执行后并不是让线程进入了阻塞队列,而是让线程进入就绪状态,因此线程有可能立即进入可执行状态,再次执行。
private int num = 100;
@Override
public void run() {
while (this.num>0){
synchronized (this){
if (num>0){
System.out.println(Thread.currentThread().getName()+"马上开始卖票");
Thread.yield();
System.out.println(Thread.currentThread().getName()+"出售了"+this.num+"号票");
num = num -1;
}
}
}
}
执行上面的代码后发现两个现象:
(1)开始卖票和出售票的窗口永远是对应的,不会出现“1号窗口马上开始卖票”接着“2号窗口马上开始卖票”这样的顺序。这说明执行了yield的线程并不会让出同步锁。
(2)可能一个窗口连续卖出票。说明线程并未阻塞,而是和另一个线程一样处于就绪状态。
6、join
多个线程并非是顺序执行的,join方法可以让此线程结束后再执行后续的线程。
比如,我们想在两个线程卖完票之后,在main主线程中打印票买完了,在第5步的代码基础上,修改main方法的代码如下
public static void main(String[] args) {
Test test1 = new Test();
Thread thread1 = new Thread(test1, "1号窗口");
Thread thread2 = new Thread(test1, "2号窗口");
thread1.start();
thread2.start();
System.out.println("票终于卖完了");
}
结果发现,明明还没有卖完票,main线程却打印出票卖完了,可恶,竟然谎报军情。怎么解决这种情况呢,引入join方法就好了。
public static void main(String[] args) throws InterruptedException {
Test test1 = new Test();
Thread thread1 = new Thread(test1, "1号窗口");
Thread thread2 = new Thread(test1, "2号窗口");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("票终于卖完了");
}
以上main线程就只能在两个线程执行完毕后,再执行了。
7、线程状态转化图
在网上找到了一个线程状态的转化图,非常形象具体。
8、写在最后
这是目前总结的线程相关的知识,但是理解还比较浅显,如果有什么错误还请大家多多指教哈!