多线程(中)

143 阅读6分钟

效率好高,并且能记住很多内容,很爽。

线程的生命周期

线程的生命周期包含下面5个:

新建:当一个Thread类或其子类的对象被声明并创建时,新生的 线程对象处于新建状态

就绪:处于新建状态的线程start后,将进入线程队列等待CPU时间片,此时已经具备了运行的条件,只是没分配到CPU资源。

运行:当就绪的线程被调度并获得CPU资源时,就进入运行状态,run方法定义了线程的操作和功能

阻塞:在某种特殊情况下。被人为挂起或执行输入输出操作时,让出CPU并临时中断自己的执行,进入阻塞状态

死亡:线程完成了它的全部工作或线程被提前强制性的中止或出现异常导致结束

重点来了:哪些操作会使一个状态变成另外一个状态呢?

image.png

线程的同步

引入例子:在多线程(上)的卖票问题中,会出现重票,错票问题,称之为线程的安全问题。

下图也可以说是线程的安全问题

image.png

重票,错票:称之为线程的安全问题

重票出现的原因:当某个线程操作车票的过程中,还没有操作完另一个线程就进入了if循环,也就导致了此时两个ticket的值相同

错票出现的原因:在最后只剩1张票时,有2个或2个以上的线程在同一个if循环中,那么ticket会由1变为0甚至变为-1。

如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket,其他线程才能操作ticket。称之为“锁”。这种情况即使线程a出现了阻塞,也不会被改变

用我自己的话来说就是:3个人上一间厕所,再没有加锁时,3个人都冲了进去,也就导致了线程不安全。而加上了“锁”的话,一个人进入后另外两个人只能在外面等待。就算你在里面睡着了,其他人也进不来。

那么在Java中,我们使用同步机制来解决上述问题

方式1:同步代码块

synchronized(同步监视器){

需要被同步的代码

}

说明:

1.操作共享数据的代码,即为需要被同步的代码-->不能包多了代码,也不能包少了代码

2.共享数据:多个线程共同操作的变量。ticket就是共享数据

3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁

要求:多个线程共用同一把锁

下图即可完成线程安全(没有重票,错票)的卖票

public class Runnablepickets {
    public static void main(String[] args) {


        Mthread2 m = new Mthread2();
        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);
        Thread t3 = new Thread(m);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Mthread2 implements Runnable {
    private int ticket = 100;//这里不用加static的原因是三个线程共用这个Mthread
    Object obj = new Object();
    @Override
    public void run() {



        while (true) {
            synchronized (obj){
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖票,票号为" + ticket);
                ticket--;
            } else {
                break;
            }
        }
        }
    }
}

注意:创建的对象必须定义在run方法外,不然就相当于创建了3个对象,那么线程还是不安全的(体现了多个线程共用同一把锁的要求)

同样的,给出同步代码块解决继承的线程安全问题

如果只想下图操作,是行不通的,因为此时的“锁”(obj)并不是唯一的。所以,只能在Object前加上static

public class sold {//创建3个窗口卖100张票

    public static void main(String[] args) {
        window t1 = new window();
        window t2 = new window();
        window t3 = new window();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class window extends Thread{
    private static int ticket = 100;//设置为static,ticket才能被3个线程共用
    static Object obj = new Object();
    @Override
    public void run() {
        while (true){
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println(getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

synchronized中的同步监视器更为简便的写法。(this)即表示当前对象,此时继承的方法中只有一个对象。只能在实现中使用。在继承中使用时由于创建了3个对象,所以每个this都代表不相同的对象,也就违背了多个线程共用同一把锁的要求。

总结:

1.实现的方式中可直接用this充当“锁”。(不过也得具体情况具体分析)

2.继承的方式中可以使用“当前类.class“来充当“锁”。

同步的方式:好处:解决了线程的安全问题

局限性:操作同步代码时,只能有一个线程参加,其他线程等待,相当是一个单线程的过程,会使效率有些低

同步的方式2:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的;

下图为使用同步方法解决实现Runnable接口的线程安全问题

 * 使用同步方法解决实现Runnable接口的线程安全问题
 *
 *
 *  关于同步方法的总结:
 *  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
 *  2. 非静态的同步方法,同步监视器是:this
 *     静态的同步方法,同步监视器是:当前类本身
 *
 * @author shkstart
 * @create 2019-02-15 上午 11:35
 */


class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {

            show();
        }
    }

    private synchronized void show(){//同步监视器:this
        //synchronized (this){

            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);

                ticket--;
            }
        //}
    }
}


public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}
//使用同步方法解决继承中遇到的线程安全问题
public class WindowTest4 {//创建3个窗口卖100张票

    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window4 extends Thread {
    private static int ticket = 100;//设置为static,ticket才能被3个线程共用

    @Override
    public void run() {
        while (true) {

            show();
        }

    }
    private static synchronized  void show(){//同步监视器:t1,t2,t3,所以不加static是错误的,加了static后,同步监视器变为Window.class
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}

问题:

1.在敲代码的过程中,遇到了点问题。在实现Runnable接口并且线程安全的2种方式中卖票,显示的都只有一个窗口卖票。

2.切记:此方式没有使用break,因此需要手动关闭,不然风扇直接开转,LOL都没转这么快

解决:

1.无解,把代码复制了一遍都没用 在后面的lock也会出现类似问题,但lock可以通过填入true参数保证每个窗口都会出现,而这里好像无法解决

PS:这好像不是个问题,小概率会出现有2个或3个窗口卖票(也可能是电脑性能太好了?)

同步方法的总结:

1.同步方法仍然涉及到同步监视器,只是不需要显式地声明

2.非静态的同步方法,同步监视器是:this

静态的同步方法,同步监视器是:当前类本身