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、写在最后

这是目前总结的线程相关的知识,但是理解还比较浅显,如果有什么错误还请大家多多指教哈!