线程同步以及synchronize关键字的作用

整理一下有关线程同步的知识,以及synchronize控制线程同步最基本的用法

1、线程的同步

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。(引用百度百科)
那么多线程环境下,必然会存在线程安全的问题,而线程同步就是在这种情况下,保证数据的准确性,安全性,而且还要考虑它的性能。

下面来通过一个多线程操作案例:模拟12306 多个黄牛买票来说明线程安全性的问题。

package sync;

/**
 * @author hao
 * @create 2019-06-21 ${TIM}
 */

/**
 * 模拟12306多人抢票的过程
 */
public class UnsafeThread01 implements  Runnable{
    //设置标识符号
    boolean falg =true;
    //票的数量
    int num=10;
    @Override
    public void run() {
    // 当还有票 线程就一直执行byTicket()买票操作
        while (falg){
            try {
                byTicket();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void byTicket() throws InterruptedException {
        if(num<=0){
            falg =false;
            return;
        }
        /* 模拟网路延迟 */
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"--->"+num--);
    }

    public static void main(String[] args) {

        UnsafeThread01 thread01 = new UnsafeThread01();
		// 开启三个线程 模拟多个人同时买票
        new Thread(thread01,"黄牛1").start();
        new Thread(thread01,"黄牛2").start();
        new Thread(thread01,"黄牛3").start();
    }
}

输出结果:

黄牛1--->10
黄牛2--->9
黄牛1--->8
黄牛2--->7
黄牛1--->6
黄牛2--->5
黄牛1--->4
黄牛3--->3
黄牛2--->2
黄牛1--->1
黄牛3--->0
黄牛2--->-1

看到上面的输出结果,出现了买到了负数的票,不符合逻辑,也就是出现出来线程安全的问题,数据不准确。按照我们上面代码的逻辑,似乎很正确,当买到最后一张票的时候,退出买票,看似没有任何问题,其实不然,我们来分析一下出现的原因。

我可以很清楚的理解 主要是在临界值上出了问题 也就是最后一张票的时候。
情景:只剩下最后一张票
黄牛1 黄牛2 黄牛3 同时进来抢这张票
假设黄牛1先进来 判断(num<=0)true此时因为网络延迟 黄牛1线程阻塞
同样 黄牛2 也进来 因为 黄牛1还在阻塞状态 并未买到票(num<=0)仍为 ture 因为网络延迟黄牛2阻塞
同样 黄牛3 阻塞
此时 黄牛1 获得时间片 被cpu调度 执行买票操作 票为0
黄牛2 此时也被执行到 在0的基础上执行买票操作 票为-1
黄牛3 又在票为-1的基础上 执行买票操作 票为-2

那么存在线程的安全问题,该如何解决呢,问题出在买票 也就是byTicket() 这个操作上,假如我们能控制这个操作,只允许线程一个一个,排着队依次执行,问题就解决啦。此时我们就要借助一个关键字synchronize 来控制线程的同步。

2、synchronize

synchronize是java内置的一个关键字,用来处理多线程环境下, 线程同步的问题,保证数据的准确性 。它有最基本的三种用法

1、同步普通方法,锁的是当前对象。默认就this
2、同步静态方法,锁的是当前 Class 对象。
3、同步块,锁的是 {} 中的对象。

举一个形象的例子:有很多房间,synchronized就是给房子上锁,想要访问房间的人(线程) 要先拿到钥匙(锁的对象不同 钥匙也不同),进入,然后归还钥匙之后,其他人(线程)才能接着拿着钥匙访问其他的房间。
我们类比到我们买票的操作,我们就可以给买票这个操作加一把锁,需要执行这个操作就要获得钥匙,当自己在执行的时候,别的线程就无法执行。
1)、使用synchronize 给方法加锁

 public  synchronize  void byTicket() throws InterruptedException {
        if(num<=0){
            falg =false;
            return;
        }
        /* 模拟网路延迟 */
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"--->"+num--);
    }

执行效果:

黄牛1--->10
黄牛2--->9
黄牛1--->8
黄牛2--->7
黄牛1--->6
黄牛2--->5
黄牛1--->4
黄牛1--->3
黄牛2--->2
黄牛1--->1

这样加锁之后,每次执行买票操作 都只运行一个线程进入,再次模拟一下最后一张票

情景:加锁之后 只剩最后一张票
黄牛1 获得锁 进入byTicket()方法中
黄牛2 无法进入 因为黄牛1 没有释放 一直等待锁…
黄牛3 无法进入 因为没有锁 一直等待锁…

黄牛1 执行完买票操作 num-- 票的数量为0 释放锁
黄牛2获得锁 因为num 为0 退出操作 释放锁
黄牛3 获得锁 同样 num 为0 退出操作 释放锁

这样,就保证了数据的准确性,不会像之前那种出现负数。但是这样也存在一个问题,我们给一个方法加上了一把锁,也就是说整个方法都要同步,如果方法体中有某个操作 是不需要同步的,但是我们却锁了整体 这样影响执行效率。
就好像 我们放了珍贵的东西在房间的抽屉里, 我们不需要对整个房间上锁。可以仅仅对抽屉上一把锁,这样我们就可以在房间里面放一些杂物,我们获取杂物的时候,就不需要开锁,关锁,提高了效率。我们就可以采用synchronize 同步块来解决

2)、synchronize 同步块
更细粒度的锁,可以提高效率,用同步块,包含我们只需要同步的代码。

    public  void byTicket() throws InterruptedException {
        synchronized (this){
            if(num<=0){  //处理票数1的情况  
                falg =false;
                return;
            }
            /* 模拟网路延迟 */
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+"--->"+num--);
        }
        //非同步的其他操作
        System.out.println("不需要同步的其他操作..............");
    }

这样我们只同步了我们需要同步的方法,对于其他非同步方法,我们就可以放在同步代码块外面来执行。
对于上述代码代码,看看还有没有优化的空间。

情景:当票为0的时候
黄牛1 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁
黄牛2 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁
黄牛3 获得锁 进入byTicket()方法中 因为num0 退出买票操作 释放锁

每一个线程都排着队,进行判断是否有票,是不是判断没票这个操作没有必要同步,所有我们对我们的同步代码块进行修改:

    public  void byTicket() throws InterruptedException {
     if(num<=0){  //处理票数1的情况  
                falg =false;
                return;
            }
        synchronized (this){
            /* 模拟网路延迟 */
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+"--->"+num--);
        }
        //非同步的其他操作
        System.out.println("不需要同步的其他操作..............");
    }

执行一下输出结果:

黄牛1--->10
黄牛1--->9
黄牛1--->8
黄牛1--->7
黄牛3--->6
黄牛3--->5
黄牛3--->4
黄牛3--->3
黄牛3--->2
黄牛3--->1
黄牛2--->0
黄牛1--->-1

发现又出新的数据准确问题。看来没有锁住。分析一下:

模拟最后一张票
黄牛1 黄牛2 黄牛3 同时进入买票这个操作
黄牛1 判断 因为num 大于0 所以获得锁 执行 num-- 释放锁 票为 0
黄牛2 获得锁 执行 num-- 释放锁 票为 -1
黄牛3 获得锁 执行 num-- 释放锁 票为 -2
.。。。。。。
所以根本没有锁住 ,对此我们再次改进

  public  void byTicket2() throws InterruptedException {

        if(num<=0){  //处理票数0的情况  进行非同步处理 提高代码的执行效率(双重检测)
            falg =false;
            return;
        }
        synchronized (this){
            if(num<=0){  //处理票数1的情况  但是当票为0之后 线程没有必要执行此同步操作,可以进行非同步的判断
                falg =false;
                return;
            }
            /* 模拟网路延迟 */
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+"--->"+num--);
        }
    }

输出结果:

黄牛1--->10
黄牛1--->9
黄牛1--->8
黄牛1--->7
黄牛1--->6
黄牛1--->5
黄牛1--->4
黄牛1--->3
黄牛3--->2
黄牛3--->1

同步代码块外面这个代码(非同步判断):

  if(num<=0){  //处理票数0的情况  进行非同步处理 提高代码的执行效率(双重检测)
            falg =false;
            return;
        }

同步块里面这个判断(同步判断):

if(num<=0){  //处理票数1的情况  但是当票为0之后 线程没有必要执行此同步操作,可以进行非同步的判断
                falg =false;
                return;
            }

这样改进之后的代码,效率就会更高啦。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值