33-多线程--卖票示例+线程安全(产生原因+解决方式:同步)+同步(同步代码块+同步的好处与弊端+同步的前提+同步函数+卖票示例的同步函数写法+验证同步函数的锁+验证静态同步函数的锁)

一、卖票示例

需求:四个窗口,同时售卖100张票,票号为1-100

1、没有多线程时的卖票代码

class Ticket {
    
    //100张票
    private int num = 100;

    public void sale() {
        //无限循环,没有写break
        while (true) {
            if (num > 0) {
                num--;
            }
        }
    }
}

2、四个窗口同时卖票,要用多线程。尝试通过继承Thread类创建多线程

class Ticket extends Thread {

    //100张票
    private int num = 100;

    public void sale() {
        //无限循环,没有写break
        while (true) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "......" + num--);
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        sale();
    }
}

public class Test {
    public static void main(String[] args) {
        //创建四个线程
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        Ticket t4 = new Ticket();

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

问题:

(1)每个线程都有自己的num,一共卖出了400张票

(2)会出现 num=0 或 num=负数 的票。原因:num=1时,线程0已经做完 if(num > 0) 的判断,还没执行num--,此时线程切换到了线程1。线程1执行run()方法,继续判断 if(num > 0),符合,(此处可能没执行num--,线程再次进行了切换)打印输出,接着执行num--,此时num=0。...... 等到下次执行权再切回到线程0时,线程0不需要再做判断,直接输出,然后做num--操作。此时,线程0输出的num就可能是0或者负数 -- 具体参见第二点:线程安全

思考:

(1)将num改成静态static的?

         静态实现数据共享,可以解决只卖一组票的问题

         但这样一来,Ticket和num就没有关系了(num被所有对象共享)。而且如果现在有两组100张票,2个窗口负责卖前100张票,另外2个窗口负责卖后100张票,此时,使用static是4个线程卖100张票,不能解决问题

(2)创建一个Ticket线程,开启四次?

class Ticket extends Thread {

    //100张票
    private int num = 100;

    public void sale() {
        //无限循环,没有写break
        while (true) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "......" + num--);
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        sale();
    }
}

public class Test {
    public static void main(String[] args) {
        //创建一个线程
        Ticket t1 = new Ticket();

        //开启四次
        t1.start();
        t1.start(); //报错。Exception in thread "main" java.lang.IllegalThreadStateException
        t1.start();
        t1.start();
    }
}

问题:此种做法是错误的,多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动

说明:此段代码有两个线程:主线程和t0线程。主线程调用第一个 t1.start(); 开启了一个线程t0,t0开始执行run()方法中的内容。当主线程在调用第二个 t1.start(); 时抛出异常,主线程立即结束。而t0线程会继续执行直至结束(在run()方法中发生异常才是t0线程的异常)

3、不能使用单例。用单例,内存中就只能有一个票对象,但实际是可以有多个票对象的。eg:每两个窗口卖一组100张的票

4、创建多线程,不能通过继承Thread类创建,那就只能通过实现Runnable接口创建。封装线程任务+封装资源

class Ticket implements Runnable {

    //封装资源 -- 票的数据
    private int num = 100;

    public void sale() {
        //无限循环,没有写break
        while (true) {
            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "......" + num--);
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        sale();
    }
}

public class Test {
    public static void main(String[] args) {
        //创建一个线程任务对象,内存中只有一个Ticket对象
        Ticket t = new Ticket();
        
        //创建线程。线程一初始化得有任务,向其构造函数中传参
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        
        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

说明:内存中只有一个Ticket对象,四个线程运算的num都是Ticket对象的num。因为把票Ticket传给了每个线程

5、如果想让两种票一起卖,两个窗口卖普通票,另外两个窗口卖动车票

    public static void main(String[] args) {
        //卖几组票,就创建几个Ticket对象
        Ticket t = new Ticket();
        Ticket tt = new Ticket();

        //将不同的Ticket对象传给不同的线程,线程就有不同的资源
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        Thread t3 = new Thread(tt);
        Thread t4 = new Thread(tt);

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

二、线程安全

1、四个窗口,同时售卖100张票,用Runnable接口实现,存在的另一个问题:num可能为0或负数

class Ticket implements Runnable {

    //封装资源 -- 票的数据
    private int num = 100;

    public void sale() throws InterruptedException {
        //无限循环,没有写break
        while (true) {
            if (num > 0) {
                //线程阻塞。调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName() + "......" + num--);
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        //run()方法是覆盖Runnable接口中的run()
        //Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
        try {
            sale();
        } catch (InterruptedException e) {  //抛什么就catch什么
            e.printStackTrace();
        }
        //不存在异常的代码要放在try/catch外
        //......
    }
}

public class Test {
    public static void main(String[] args) {
        //创建一个线程任务对象,内存中只有一个Ticket对象
        Ticket t = new Ticket();

        //创建线程。线程一初始化得有任务,向其构造函数中传参
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

2、在写多线程时,必须要注意线程的安全问题

3、线程安全问题产生的原因/前提:

(1)多个线程在操作共享的数据

(2)操作共享数据的线程代码有多条

即 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生

(必须是多线程;得有共享数据;线程的代码当中是否有多条语句在操作同一个共享数据)

4、线程安全问题的解决思路:将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算

5、线程安全问题的解决方式:同步。包括同步代码块和同步函数

三、同步

1、同步代码块

(1)同步代码块的格式

        synchronized (对象) {
            //需要被同步的代码
            //......
        }

(2)synchronized(对象){...}:此处的对象就是同步锁/对象锁。放入一个对象(锁),是因为后期要对同步中的线程进行监视操作,而监视的方法都在锁上,所以,要加一个锁才行

注:每个线程执行前,都要先判断锁。一个线程得到CPU的执行权后,会先去判断锁。拿到锁后,开始执行。如果该线程被sleep(time),释放执行权的同时释放执行资格,但锁没有释放。另一个线程得到CPU的执行权后,也会先去判断锁,但获取不到锁,只能等待。直到被sleep(time)的线程重新获取CPU的执行权,并执行完其线程任务后,锁才会被释放。此时,其他线程才有机会获取锁并执行对应的线程任务

(3)修改后的代码

class Ticket implements Runnable {

    //封装资源 -- 票的数据
    // 可调大num,或减少sleep(time)时间,并让线程多运行几次,就可以在运行结果中看到所有线程
    private int num = 300;

    //用作 同步代码块 synchronized(对象){...} 中的对象,也可以自己再new其他对象
    Object obj = new Object();

    public void sale() throws InterruptedException {
        //无限循环,没有写break
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (num > 0) {
                    //调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "......" + num--);
                }
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        //run()方法是覆盖Runnable接口中的run()
        //Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
        try {
            sale();
        } catch (InterruptedException e) {  //抛什么就catch什么
            e.printStackTrace();
        }
        //不存在异常的代码要放在try/catch外
        //......
    }
}

public class Test {
    public static void main(String[] args) {
        //创建一个线程任务对象,内存中只有一个Ticket对象
        Ticket t = new Ticket();

        //创建线程。线程一初始化得有任务,向其构造函数中传参
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

2、同步的好处与弊端

(1)好处:解决了线程的安全问题

(2)弊端:相对降低了效率,因为同步外的线程都会判断同步锁

3、同步的前提:

      同步中,必须有多个线程,并使用同一个锁

错误示例

class Ticket implements Runnable {

    //封装资源 -- 票的数据
    // 可调大num,或减少sleep(time)时间,并让线程多运行几次,就可以在运行结果中看到所有线程
    private int num = 300;

    public void sale() throws InterruptedException {
        //同步锁
        //线程执行run()方法调用sale()时,都会创建一个Object锁对象,导致多个线程使用的不是同一个锁而出错
        //error!!!
        //要将创建锁对象的代码放在run()方法外
        //synchronized(new Object()){...}的写法和此种错误原理相同,都是创建多个锁对象
        Object obj = new Object();

        //无限循环,没有写break
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (num > 0) {
                    //调用sleep(time)方法是为了让线程切换执行权,以便看出问题所在
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "......" + num--);
                }
            }
        }
    }

    /**
     * 封装线程任务
     * 此处在run()方法中调用sale()方法,是为了保持Ticket类的原样 -- 正确写法
     * 建议最好不要将sale()的方法名直接改为run()
     */
    @Override
    public void run() {
        //run()方法是覆盖Runnable接口中的run()
        //Runnable接口中的run()没有声明异常,覆盖时也不能声明异常,只能try/catch
        try {
            sale();
        } catch (InterruptedException e) {  //抛什么就catch什么
            e.printStackTrace();
        }
        //不存在异常的代码要放在try/catch外
        //......
    }
}

public class Test {
    public static void main(String[] args) {
        //创建一个线程任务对象,内存中只有一个Ticket对象
        Ticket t = new Ticket();

        //创建线程。线程一初始化得有任务,向其构造函数中传参
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

错误原因:每个线程开启后,都有自己的run()方法。而每个run()中都 new Object(),意味着每个线程都有自己的锁。多个线程使用的不是同一个锁,出错。synchronized(new Object()){...}的写法和此种写法的错误原理相同(需要把创建锁对象的代码放在run()方法外)

注:在开发中,写了同步还没有解决问题时,需要考虑同步的前提:多个线程是否使用同一个锁

4、同步函数

需求:有两个储户,到同一家银行存钱,每人每次存100元,共存三次

class Bank {
    //银行的小金库,不能对外暴露,用private
    private int sum;

    public void add(int num) throws InterruptedException {
        sum += num;
        //为了发现问题,此处调用sleep(time)方法切换CPU执行权
        Thread.sleep(10);
        System.out.println("sum=" + sum);
    }
}

class Custom implements Runnable {
    //两人去同一家银行,Bank是共享数据,new Bank()不能写在run()中
    private Bank bank = new Bank();

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            //此处的run()方法是覆盖Runnable接口的run(),而Runnable接口的run()没有声明异常
            //所以,此处只能 try/catch
            try {
                bank.add(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Custom custom = new Custom();

        Thread t1 = new Thread(custom);
        Thread t2 = new Thread(custom);

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

问:上述代码是否存在安全隐患?

分析:多线程代码是否存在安全隐患,得看在线程运行的代码中,是否有共享数据。run()方法中,bank是共享数据,但操作共享数据的代码只有一条 bank.add(100); ,看似没问题。但在run()方法中调用了add()方法,此时,add()方法也属于线程的代码。而add()方法中,sum是共享数据,且操作sum的代码不止一条。所以,上述代码存在安全隐患

解决:解决线程安全问题的方法:同步。包括:同步代码块和同步函数

(1)同步代码块

class Bank {
    //银行的小金库,不能对外暴露,用private
    private int sum;
    //同步锁
    private Object obj = new Object();

    public void add(int num) throws InterruptedException {
        //同步代码块
        synchronized (obj) {
            sum += num;
            //为了发现问题,此处调用sleep(time)方法切换CPU执行权
            Thread.sleep(10);
            System.out.println("sum=" + sum);
        }
    }
}

(2)同步函数

class Bank {
    //银行的小金库,不能对外暴露,用private
    private int sum;
    //使用同步函数解决线程安全问题时,不用自己创建同步锁
    //同步函数的锁是this
//    private Object obj = new Object();

    //同步函数:将同步关键词synchronized作为函数的修饰符即可
    public synchronized void add(int num) throws InterruptedException {
        sum += num;
        //为了发现问题,此处调用sleep(time)方法切换CPU执行权
        Thread.sleep(10);
        System.out.println("sum=" + sum);
        /*
        //同步代码块
        synchronized (obj) {
            sum += num;
            //为了发现问题,此处调用sleep(time)方法切换CPU执行权
            Thread.sleep(10);
            System.out.println("sum=" + sum);
        }
        */
    }
}

5、卖票示例的同步函数写法

(1)使用同步代码块的写法

class Ticket implements Runnable {
    //票
    private int num = 100;
    //同步锁
    Object obj = new Object();

    @Override
    public void run() {
        //无限循环,没有写break
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //不存在异常的代码要放在 try/catch 外
                    System.out.println(Thread.currentThread().getName() + "......" + num--);
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

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

(2)使用同步函数的写法

错误示例:

class Ticket implements Runnable {
    //增加票数,为了更能凸显问题
    private int num = 400;
    //同步锁
//    Object obj = new Object();

    //同步函数的错误写法
    @Override
    public synchronized void run() {
        //无限循环,没有写break
        while (true) {
            //同步代码块
//            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //不存在异常的代码要放在 try/catch 外
                    System.out.println(Thread.currentThread().getName() + "......" + num--);
                }
//            }
        }
    }
}

运行结果:线程0把400张票全都卖完了,票号是400-1,没有出现0号票。但这些票全是线程0卖的,其他三个线程没有参与

分析:run()方法是同步函数,线程0得到锁,又得到CPU执行权,进入run()运行。之后线程0执行sleep(time),释放执行权的同时释放执行资格,但没有释放锁。其他线程获取到CPU执行权,但是得不到锁,只能等待(线程0不释放锁,其他线程都得不到锁,不能执行run()方法)...... 等到线程0重新获取到CPU执行权后,把票卖光,依旧无法释放锁:while(true){...},无限循环

        即 while(true){...}这句代码不需要被同步

解决:用另一个函数封装需要被同步的代码,再被run()调用即可

正确代码:

class Ticket implements Runnable {
    //票
    private int num = 100;
    //同步锁
//    private Object obj = new Object();

    @Override
    public void run() {
        //无限循环,没有写break
        while (true) {
            //同步代码块
//            synchronized (obj) {
                //在run()中调用同步函数
                show();
//            }
        }
    }

    /**
     * 同步函数的正确写法:
     * 定义一个同步函数,封装需要被同步的代码,再被run()方法调用即可
     * 注:num是共享数据
     */
    public synchronized void show() {
        if (num > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //不存在异常的代码要放在 try/catch 外
            System.out.println(Thread.currentThread().getName() + "......" + num--);
        }
    }
}

6、验证同步函数的锁

(1)同步函数的格式

    public synchronized void xxx() { ... }

(2)验证同步函数的锁

思路:两个线程,一个在同步代码块中卖票,一个在同步函数中卖票。如果这两个同步用的是同一个锁,就不会出现安全隐患(出现重复的票、0或负号票)

做法:run()方法中既能运行同步代码块,又能调用到同步函数,需要做线程切换动作,要定义标志位flag

class Ticket implements Runnable {
    //票
    private int num = 400;
    //同步锁
//    private Object obj = new Object();
    //标志位
    boolean flag = true;

    @Override
    public void run() {

        //同步函数的锁
        //this所指向的地址与Ticket t所指向的地址是一致的
        System.out.println("this:" + this);    //this:test.Ticket@58ceff1

        //如果flag=true,运行同步代码块
        if (flag) {
            //无限循环,没有写break
            while (true) {
                //同步代码块
//            synchronized (obj) {
                //为了验证同步函数持有的锁是this,将同步代码块的锁也改为this
                synchronized (this) {
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //不存在异常的代码要放在 try/catch 外
                        System.out.println(Thread.currentThread().getName() + "...同步代码块..." + num--);
                    }
                }
            }
        } else {
            //如果flag=false,运行同步函数
            while (true) {
                //其实是this.show(),this被省略
                show();
            }
        }
    }

    /**
     * 同步函数的正确写法:
     * 定义一个同步函数,封装需要被同步的代码,再被run()方法调用即可
     * 注:num是共享数据
     */
    public synchronized void show() {
        //需要被同步的代码
        if (num > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //不存在异常的代码要放在 try/catch 外
            System.out.println(Thread.currentThread().getName() + "...同步函数..." + num--);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        
        //Ticket t所指向的地址与this所指向的地址是一致的
        System.out.println("t:" + t);  //t:test.Ticket@58ceff1

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        //能处理的异常,用try/catch
        try {
            //此处调用sleep(time)是为了切换CPU的执行权去执行t1线程
            //否则,开启t1线程后,主线程还持有CPU的执行权,它会继续向下,把下面的代码全部执行完,flag瞬间被改为false
            //此时,t1和t2线程执行的都是flag=false的代码,不能体现切换标志位的操作(flag=true执行同步代码块,flag=false执行同步函数)
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //修改标志位
        t.flag = false;
        t2.start();
    }
}

分析:同步函数具有同步性,但同步本身不带锁,应该是函数带锁。函数可以操作对象中的数据,是因为持有this引用,即每个函数都有自己所属的this。同步函数show()被run()方法调用,而run()方法属于 Ticket t(Ticket t 是调用run()方法的对象),所以,show()方法用this代表 Ticket t 对象

说明:run()方法是封装线程任务的,将线程任务所属对象t传给new Thread(t),所以,Ticket t 是调用run()方法的对象

总结:同步函数使用的锁是this

(3)同步函数和同步代码块的区别

         同步函数的锁是固定的this,同步代码块的锁是任意的对象。建议使用同步代码块

注:同步函数可以作为同步代码块的简写形式。同步函数的锁唯一,就是this。只有同步代码块中用的锁是this时,才可以简写成同步函数

7、验证静态同步函数的锁

(1)静态同步函数的格式

    public static synchronized void xxx() { ... }

(2)验证静态同步函数的锁

思路:静态同步函数的锁一定不是this(static方法中没有this)。静态函数随着类的加载而加载,而类进内存时,还没有通过new来创建对象。但java有个特点:字节码文件进内存先封装对象。所以,类一进内存,就有一个对象,即当前的.class文件所属的对象,而这个对象就是静态同步函数所使用的锁

注:所有对象建立,都有自己所属的字节码文件对象,可以用getClass()方法获取

class Ticket implements Runnable {
    //票
    private static int num = 400;
    //同步锁
//    private Object obj = new Object();
    //标志位
    boolean flag = true;

    @Override
    public void run() {

        //静态同步函数的锁
        System.out.println("this:" + this.getClass());    //this:class test.Ticket

        //如果flag=true,运行同步代码块
        if (flag) {
            //无限循环,没有写break
            while (true) {
                //同步代码块
//            synchronized (obj) {
                //静态同步函数持有的锁是该函数所属的字节码文件对象。以下两种写法都可以
//                synchronized (this.getClass()) {
                synchronized (Ticket.class) {
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //不存在异常的代码要放在 try/catch 外
                        System.out.println(Thread.currentThread().getName() + "...同步代码块..." + num--);
                    }
                }
            }
        } else {
            //如果flag=false,运行静态同步函数
            while (true) {
                show();
            }
        }
    }

    /**
     * 静态同步函数
     */
    public static synchronized void show() {
        //需要被同步的代码
        if (num > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //不存在异常的代码要放在 try/catch 外
            System.out.println(Thread.currentThread().getName() + "...静态同步函数..." + num--);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();
        
        //无论new多少个Ticket对象,t.getClass()都是同一个
        System.out.println("t:" + t.getClass());  //t:class test.Ticket

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        //能处理的异常,就try/catch
        try {
            //此处调用sleep(time)是为了切换CPU的执行权去执行t1线程
            //否则,开启t1线程后,主线程还持有CPU的执行权,它会继续向下,把下面的代码全部执行完,flag瞬间被改为false
            //此时,t1和t2线程执行的都是flag=false的代码,不能体现切换标志位的操作(flag=true执行同步代码块,flag=false执行静态同步函数)
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //修改标志位
        t.flag = false;
        t2.start();
    }
}

静态同步函数的锁的表示形式:

a)this.getClass()

b)(Ticket t = new Ticket();) t.getClass()

c)Ticket.class

说明:a)是非静态的,b)要先创建完对象,才能调用方法,c)直接使用 类名.class 属性

注:任何类,它们都有一个静态属性:类名.class 属性。这个属性可以直接获取该字节码文件的字节码文件对象

总结:静态同步函数使用的锁是:该函数所属的字节码文件对象。该对象可以通过 对象.getClass() 方法获取(非静态),也可以用 当前类名.class属性(静态)表示

(3)同步代码块 synchronized(对象){...} 中的同步锁,可以是:obj、this、类名.class等。对象是任意的,只要能保证多个线程用的是同一个锁即可

(4)如何选择使用哪个同步锁

a)如果同步函数和同步代码块同时出现,且需要共用同一个锁,同步代码块的锁用 this

b)如果静态同步函数和同步代码块同时出现,且需要共用同一个锁,同步代码块的锁用 当前类名.class

猜你喜欢

转载自blog.csdn.net/ruyu00/article/details/83411346