多线程-*同步代码块*解决两种线程创建方式的线程安全问题

线程安全问题及解决

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是多个线程中对资源有读和写的操作。就容易出现线程安全问题。

举例:

多线程-*同步代码块*解决两种线程创建方式的线程安全问题_第1张图片

同一个资源问题和线程安全问题

案例:

火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共100个(即,只能出售100张火车票)。我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。

例题: 开启三个窗口售票,总票数为100张。

分别使用两种线程创建方式实现:

理想状态:

多线程-*同步代码块*解决两种线程创建方式的线程安全问题_第2张图片

极端状态:

多线程-*同步代码块*解决两种线程创建方式的线程安全问题_第3张图片

实现Runnable接口方式:

package thread.demo03_threadsafe.notsafe;

//使用实现Runnable接口的方式,实现卖票

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

        SaleTicket s = new SaleTicket();

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

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
        //正常运行后发现每个窗口都卖了100号票
    }
}


class SaleTicket implements Runnable{
    int ticket = 100;

    @Override
    public void run() {

        while (true){
            if (ticket > 0){

                try {
                    Thread.sleep(10);//有了这一段运行后看结果竟然还出现-1号票,这就是线程的安全问题
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket);
                ticket--;
            }else {
                break;
            }
        }

    }
}

继承Thread方式

package thread.demo03_threadsafe.notsafe;

//使用继承Thread类的方式,实现卖票。

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

        SaleTicket1 s1 = new SaleTicket1("窗口1");
        SaleTicket1 s2 = new SaleTicket1("窗口2");
        SaleTicket1 s3 = new SaleTicket1("窗口3");

        s1.start();
        s2.start();
        s3.start();
        //正常运行后还是有重票三个100,甚至错票-1

    }
}


class SaleTicket1 extends Thread{

    public SaleTicket1() {
    }

    public SaleTicket1(String name) {
        super(name);
    }

    static int ticket = 100;//没有static直接300张票,每个窗口都卖1——100
    //一个对象一份的是实例变量。
    //所有对象一份的是静态变量。

    @Override
    public void run() {
        while (true){
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName() + "票号为: " + ticket);
                ticket--;
            }else {
                break;
            }

        }
    }
}

以上两种线程创建方式运行后都出现了线程安全问题!!


怎么解决?看下面

线程的安全问题与线程的同步机制

1. 多线程卖票出现的问题:出现了重票和错票


2.什么原因导致的?  线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作。


3.如何解决? 必须保证一个线程a在操作ticket的过程中,其他线程必须等待,
           直到线程a操作ticket结束以后,其它线程才可以进来继续操作ticket。(ticket是共享数据)
           比如上厕所几个坑位,坑位就是共享数据,排队进去后,上完,后面的人才能进来,不能一次进去好几个人,这样才安全。


4.Java是如何解决线程的安全问题? 使用线程的同步机制。

方式1:同步代码块

synchronized(同步监视器){
    //需要被同步的代码
}

说明:
> 需要被同步的代码,及为操作共享数据的代码
> 共享数据:即多个线程都需要操作的数据。比如:ticket
> 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
> 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
              比如一个坑位,有个门锁,你进去把门锁上别人就进不来了,上完厕所把锁打开其他人才能进来。
> 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。

注意:在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
      在继承Thread类的方式中,同步监视器要慎用this,可以考虑使用:当前类.class*下面的同步方法解决安全问题下节课看*
方式2:同步方法

说明:



5.synchronized的好处:

使用 同步代码块 解决两种线程创建方式的线程安全问题

解决实现Runnable接口方式出现的线程安全问题:

package thread.demo03_threadsafe.runnablesafe;

//使用实现Runnable接口的方式,实现卖票。---> 存在线程安全问题
//使用同步代码块解决上述卖票中的线程安全问题。

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

        SaleTicket s = new SaleTicket();

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

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
        //运行后正常,解决了线程安全问题,达到了预期结果。
    }
}


class SaleTicket implements Runnable{
    int ticket = 100;//ticket是共享数据
    Object obj = new Object();
    Dog dog = new Dog();

    @Override
    public void run() {

//        synchronized(this){ //全部用synchronized包起来运行查看结果全是窗口一,一个线程进来一直循环完才出来,不合适
        while (true){

            try {
                Thread.sleep(5);   //试了两次发现运行结果全是同一个窗口卖完了,这段代码就让进来的睡一下,其他的窗口就能获取到了。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

//            synchronized(obj){    //要想保证安全性,对象必须是唯一的。 obj是唯一的?yes
//            synchronized(dog){    //dog:是唯一的?yes
            //this:最方便的。它是唯一的?yes,this是run方法的对象,也就是SaleTicket的对象,启动类中new了一下它,就是题目中的s,是唯一的。
            synchronized(this){ //这个可以
//            synchronized (SaleTicket.class){  后面学的这个对象也是唯一的,而且靠谱,也能用。

                if (ticket > 0){

                    try {
                        Thread.sleep(10);//有了这一段运行后看结果竟然出现-1号票,这就是线程的安全问题
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "售票,票号为: " + ticket);
                    ticket--;
                }else {
                    break;
                }
            }

        }

    }
}

class Dog{

}

分析线程同步机制原理:

多线程-*同步代码块*解决两种线程创建方式的线程安全问题_第4张图片

解决继承Thread方式出现的线程安全问题:

package thread.demo03_threadsafe.threadsafe;

//使用继承Thread类的方式,实现卖票。
//使用同步代码块的方式解决线程安全问题。

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

        SaleTicket1 s1 = new SaleTicket1("窗口1");
        SaleTicket1 s2 = new SaleTicket1("窗口2");
        SaleTicket1 s3 = new SaleTicket1("窗口3");

        s1.start();
        s2.start();
        s3.start();
        //运行后正常,解决了线程安全问题,达到了预期结果。

    }
}


class SaleTicket1 extends Thread{

    public SaleTicket1() {
    }

    public SaleTicket1(String name) {
        super(name);
    }

    static int ticket = 100;//没有static直接300张票,每个窗口都卖1——100
    //一个对象一份的是实例变量。
    //所有对象一份的是静态变量。

    static Object obj = new Object();//加了static就是全局唯一的。

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

//            synchronized (this) {    //this不安全,this就是这个方法的对象,也就是SaleTicket1类的对象,被new了三次,不是唯一的,不安全。
//            synchronized (obj) {    //obj:使用static就能保证其唯一性,这个可以用,但是还有没有现成的而且还唯一的?
            synchronized (SaleTicket1.class) {    //这里应该放对象,看起来像类,这个比较超纲,后续学到反射讲解,这其实也是一个对象。
                                                  //结构:Class clz = SaleTicket1.class    SaleTicket1.class本来就是Class的对象,是唯一的。
                                                  //大写Class就相当于一个类,它的对象就是具体的某个具体的类。

                if (ticket > 0){

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

                    System.out.println(Thread.currentThread().getName() + "票号为: " + ticket);
                    ticket--;
                }else {
                    break;
                }
            }


        }
    }
}

你可能感兴趣的:(java学习,java)