美文网首页
【多线程进阶并发编程一】synchronized应用

【多线程进阶并发编程一】synchronized应用

作者: 5d44bc28b93d | 来源:发表于2018-03-26 20:30 被阅读71次
synchronized基本应用.png

并发问题的产生


java-memory-model-3.png

为什么会产生并发问题呢?这个得从Java内存模型(JMM)说起。
JMM将java虚拟机内部划分为堆栈,栈也称线程栈是每个线程独有的。而堆是线程共享的。

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈,即方法的本地变量是不存在线程安全问题的。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。


20160921182337904.png

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

两程序员小A与小Q的讨论:

Q:为甚么说方法中的变量不存在线程安全问题?

A:因为线程的内部变量属于线程私有,每个线程都有自己的线程栈。

Q:为什么两个线程调用同一个方法时会产生线程安全问题呢?不是说线程栈是线程私有的吗?

A:线程栈确实是线程私有,但是如果线程访问的方法中访问实例变量就会产生线程安全问题。

Q:等等,你前面不是说线程的内部变量是属于线程私有的吗?

A:是的这个说法很正确,对于线程来说调用方法就会创建一个栈帧,然后入线程栈,而方法访问的本地变量是存放在栈里面的,但是成员变量会随着对象存储在堆空间,而对象的引用同样是存储在栈空间的,方法通过引用去访问堆中的对象,又因为堆中的对象是共享的,那么如果两个线程同时对这个共享对象操作你觉得会产生什么结果.

synchronized应用

Java 同步块(synchronized block)用来标记方法或者代码块是同步的。Java同步块用来避免竞争。

背景故事:MT小镇开了一家电影院,电影院老板觉得每次都那么多人过来排队,只有一个窗口效率低下,很多顾客反映本来想看个电影,但是排队等待太久了就放弃了,希望影院能提供更好高效的售票服务。老板一想觉得现在都互联网时代了,这么落后的手工售票方式是时候淘汰了,于是打算招聘技术人员开发这套系统。参加应试的小Q、小A、小M 都想获得这个职位,于是老板就让他们各自设计一个系统

  • 共享资源争抢带来的问题

    小Q刚步入社会,想着一定要大展身手拿下这个offer,走向人生巅峰,迎娶白富美。于是设计了如下的系统:

    售票系统:

    public class TicketService {
        //总票数
        private int totalNum = 10;
    
        public void saleTicket() {
            System.out.println(Thread.currentThread().getName() + "请稍等");
            if (totalNum > 0) {
                System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + this.totalNum);
                this.totalNum--;
    
            }
    }
    

    模拟用户

    class UserThread implements Runnable {
        private TicketService ticketService;
    
        public UserThread(TicketService ticketService) {
            this.ticketService = ticketService;
        }
    
        @Override
        public void run() {
            System.out.println("我要买票我是" + Thread.currentThread().getName());
            this.ticketService.saleTicket();
        }
    }
    

    上线测试:

    public static void main(String args[]) {
        TicketService ticketService = new TicketService();
        UserThread userThread = new UserThread(ticketService);
        for (int i = 0; i < 10; i++) {
            Thread thread01 = new Thread(userThread);
            thread01.setName("User0" + i);
            thread01.start();
        }
    }
    

    测试结果非常的糟糕:

    我要买票我是User00
    User00请稍等
    User00正在为您出票 票号为 10
    我要买票我是User01
    User01请稍等
    User01正在为您出票 票号为 9
    我要买票我是User04
    User04请稍等
    User04正在为您出票 票号为 8
    我要买票我是User05
    User05请稍等
    我要买票我是User08
    User08请稍等
    User08正在为您出票 票号为 7
    User05正在为您出票 票号为 7
    我要买票我是User02
    User02请稍等
    我要买票我是User06
    我要买票我是User03
    User03请稍等
    User03正在为您出票 票号为 5
    User06请稍等
    我要买票我是User09
    User02正在为您出票 票号为 5
    User09请稍等
    User06正在为您出票 票号为 4
    我要买票我是User07
    User07请稍等
    User09正在为您出票 票号为 3
    User07正在为您出票 票号为 2
    
  • synchronized 实例方法同步

    小A一看机会来了,轮到自己一展身手了,在想上面的方案其实就是没有考虑到共享资源竞争的问题,我只需要控制好资源的竞争就行
    方案如下:

        public synchronized void saleTicket01() {
        System.out.println(Thread.currentThread().getName()+"请稍等");
        if (totalNum > 0) {
            System.out.println(Thread.currentThread().getName()+"正在为您出票 票号为 " + this.totalNum);
            this.totalNum--;
    
        }
    }
    
    

    输出:

    我要买票我是User00
    我要买票我是User01
    User00请稍等
    User00正在为您出票 票号为 10
    User01请稍等
    User01正在为您出票 票号为 9
    我要买票我是User04
    User04请稍等
    我要买票我是User02
    我要买票我是User03
    我要买票我是User05
    User04正在为您出票 票号为 8
    我要买票我是User06
    User06请稍等
    User06正在为您出票 票号为 7
    User05请稍等
    User05正在为您出票 票号为 6
    我要买票我是User09
    User03请稍等
    User03正在为您出票 票号为 5
    User02请稍等
    我要买票我是User07
    我要买票我是User08
    User02正在为您出票 票号为 4
    User08请稍等
    User08正在为您出票 票号为 3
    User07请稍等
    User07正在为您出票 票号为 2
    User09请稍等
    User09正在为您出票 票号为 1
    

    老板觉得可行,这样客户就可以自己在家买票,随时随地买票。而且不会出现一张票卖个多个人的现象。

  • synchronized 实例方法中同步代码块

    小M看了之后觉得自己想要拿到offer必须得有比小A更好的方案才行,他想到了性能提升采用同步代码块的方式,在操作共享资源的时候加锁,这样就不会使得不需要加锁的地方也锁上了,导致执行不了,执行效率低下。
    改进方案:

        public void saleTicket02() {
        System.out.println(Thread.currentThread().getName() + "请稍等");
        synchronized (this) {
            if (totalNum > 0) {
                System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + this.totalNum);
                this.totalNum--;
            }
        }
    }
    
    

最后老板选择了小M的方案。

  • 证明synchronized 同步实例方法时锁的为当前对象

现在假设有两个用户服务窗口分别调用saleTicket01,saleTicket02最终我们发现两个窗口能够做到很好的同步。从而进一步证明加锁的为同一个对象。

    public synchronized void saleTicket01() {
        System.out.println(Thread.currentThread().getName() + "请稍等");
        if (totalNum > 0) {
            System.out.println(Thread.currentThread().getName() + "saleTicket01窗口 正在为您出票 票号为 " + this.totalNum);
            this.totalNum--;
        }
    }

    public void saleTicket02() {
        System.out.println(Thread.currentThread().getName() + "请稍等");
        synchronized (this) {
            if (totalNum > 0) {
                System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
                this.totalNum--;
            }
        }
    } 
  • synchronized 静态方法同步
   private static int totalNum = 10;
   public static synchronized void saleTicket03() {
       System.out.println(Thread.currentThread().getName() + "请稍等");
       if (totalNum > 0) {
           System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + totalNum);
           totalNum--;
       }
   } 

将次方法与saleTicket02对比就能发现他们锁的不是同一个对象,会导致一张票卖多人的问题,那么如何证明这个锁的对象是TicketService.class呢?只需要将saleTicket02改成如下方式,即可保证售票系统正确运行

    public void saleTicket02() {
       System.out.println(Thread.currentThread().getName() + "请稍等");
       synchronized (TicketService.class) {
           if (totalNum > 0) {
               System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
               this.totalNum--;
           }
       }
   }
  • 数据类型String的常量池特性

在JVM中存在String常量池缓存的功能。由于传递的参数都是LOCK,两个线程获取的是同一把锁,谁先抢到cpu资源,谁先执行,那么另外一个线程将永远处于等待中,这就是String常量池带来的问题,所以synchronized都不用String做锁,而是采用new Object()。

class PrintService{
    public  void print(String param){
        synchronized (param){
            while(true){
                System.out.println(Thread.currentThread().getName());
            }
        }
    }
    public static void main(String[] args){
        PrintService printService = new PrintService();
        ThreadA threadA = new ThreadA(printService);
        ThreadB threadB = new ThreadB(printService);
        threadA.start();
        threadB.start();
    }
}
class ThreadA extends Thread{
    private PrintService printService;
    ThreadA(PrintService printService){
        this.printService = printService;
    }

    @Override
    public void run() {
        super.run();
        this.setName("AA");
        printService.print("LOCK");
    }
}
class ThreadB extends Thread{
    private PrintService printService;
    ThreadB(PrintService printService){
        this.printService = printService;
    }

    @Override
    public void run() {
        super.run();
        this.setName("BB");
        printService.print("LOCK");
    }
}
  • 任意对象可作为监视器

任意对象都可以作为锁,这个后面的深入理解synchronized的原理章节中会说到。

    Object object = new Object();
    public  void print(){
        synchronized (object){
            while(true){
                System.out.println(Thread.currentThread().getName());
            }
        }
    }
  • synchronized锁的可重入性

当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。但是在synchronized中,如果一个线程获得一个锁后再次获取同一把锁是可以获取的,这就是锁的可重入性。

优点:避免了重复获取同一把锁造成的死锁

  • 出现异常自动释放锁

相关文章

网友评论

      本文标题:【多线程进阶并发编程一】synchronized应用

      本文链接:https://www.haomeiwen.com/subject/gkoqfftx.html