线程安全同步锁之对象锁

线程的相关理解就不多说了,直接进入主题:通过同步锁实现线程安全

在多线程,为了避免多个线程同时操作某个共享变量导致数据错乱,采用了同步锁机制,保证了操作的原子性,数据的一致性及线程安全。可以结合数据库的事务操作理解。

同步锁又可以分为对象锁,实例锁,类锁。

对象锁,锁住某个对象;实例锁,锁住某个实例,类锁,锁住某个类。

哈哈哈,佛了,有点抽象,不知道怎么解释,很尴尬。不急,可以根据下面的应用场景及代码来理解。

假定一个应用场景,就用经典的火车票售票系统为例:

假设有10张票,编号为1~10,有四个售票窗口(1~3)在卖,即3个窗口共享这10张票,总共卖出的票不能超出10张,也不能有相同号的票被卖出。

未加锁,线程不安全,代码如下:

/**
 * 对象锁
 */
public class SyncObject {

    static Object lock = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable(),"窗口1");
        Thread t2 = new Thread(new MyRunnable(),"窗口2");
        Thread t3 = new Thread(new MyRunnable(),"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
    private static class  MyRunnable implements  Runnable{
        static int a = 10;  //类变量,多个实例共享,数据存在线程安全问题
        @Override
        public void run() {
            sale0();
        }
        //无锁,线程不安全
        public static void sale0(){
            while (a>0){    //有票
                try{
                    Thread.sleep(500);  //模拟网络延迟,更能看出问题,多个线程在此处等待开始竞争
                    System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
                }catch (Exception e){
                }
            }
        }
    }
}

运行结果:

窗口3卖出10号票
窗口2卖出9号票
窗口1卖出8号票
窗口3卖出7号票
窗口2卖出7号票
窗口1卖出6号票
窗口3卖出5号票
窗口2卖出5号票
窗口1卖出4号票
窗口3卖出3号票
窗口2卖出2号票
窗口1卖出1号票
窗口3卖出-1号票
窗口2卖出0号票

可见7号票,5号票卖了两次,只有10张票,3个窗口总共卖了14张票,数据错乱,线程不安全。

原因分析:

当a=1时,t1,t2,t3 都跑通了while(a>0),经过0.5s的sleep后,t1率先执行了

System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");

此时,a=0,然后t2接着执行,a=-1,最后t3执行,即出现了超卖现象;

同理,当a=7时,线程t3率先执行了

System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");

输出卖了7号票,还没来得及将a--赋值给a,这时,线程t2来了,也执行了:

System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");

即,窗口2和3同时卖出了7号票。

这两种情况都是不允许发生的,于是对程序进行了改进,加入同步锁,保证线程安全:

/**
 * 对象锁
 */
public class SyncObject {

    static Object lock = new Object();

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


    private static class  MyRunnable implements  Runnable{

        static int a = 10;  //共享变量,数据存在线程安全问题
        @Override
        public void run() {
           sale1();
        }
        //线程安全,但不合理
        //当某线程进入sale方法,获取到锁后,运行synchronized中代码块,while()循环,直到a为0,才运行完才释放锁
        //这期间其他售票窗口无法进入,无法卖票,当a=0时,其他窗口才获取到该锁,才能执行synchronized代码块,
        // 而此时已没票
        public static void sale1(){
            synchronized (lock){   //同步代码块,某一时刻只允许一个线程进来,运行完释放锁
                while (a>0){
                    try{
                        Thread.sleep(500); //模拟买票网络延迟
                        System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
                    }catch (Exception e){
                    }
                }
                System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
            }
        }
    }
}

运行结果如下:

窗口1卖出10号票
窗口1卖出9号票
窗口1卖出8号票
窗口1卖出7号票
窗口1卖出6号票
窗口1卖出5号票
窗口1卖出4号票
窗口1卖出3号票
窗口1卖出2号票
窗口1卖出1号票
窗口1票已卖完。。。
窗口3票已卖完。。。
窗口2票已卖完。。。

多次运行后发现,虽然没有出现同号票,或多出的票,但是10张票都是只由一个窗口(假设1)卖完,其他窗口无票可卖。

导致的情况是,若窗口1票卖不完,而其他窗口想买买不到,造成了资源不能合理充分利用。

虽然加了sync同步锁,某个时刻只能有一个线程执行sync中的代码块,保证了变量a操作的原子性,数据的一致性。

但是由于while()循环,只要a>0有票,持有锁的线程就会一直执行,不释放锁,则其他线程只能等待,无法进行售票。

直到该线程执行完sync中代码块,才释放锁,其他线程才能抢到锁,但a已经为0,已无票可售。虽然保证了线程安全,

但是不能满足现实业务需求,我们需要的是在有票的情况下,各个窗口都有机会抢到票卖,既不能出现重复票,也不能

超卖。改进如下:

package com.knowsight.test.Thread.synclock;

/**
 * 对象锁
 */
public class SyncObject {

    static Object lock = new Object();

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


    private static class  MyRunnable implements  Runnable{

        static int a = 10;  //共享变量,数据存在线程安全问题
        @Override
        public void run() {
           //sale1();
            sale2();
            //sale0();
        }

        //无锁,线程不安全
        public static void sale0(){
            while (a>0){
                try{
                    Thread.sleep(500);  //模拟网络延迟,更能看出问题,多个线程在此处等待开始竞争
                    System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
                }catch (Exception e){
                }
            }
        }

        //线程安全,但不合理
        //当某线程进入sale方法,获取到锁后,运行synchronized中代码块,while()循环,直到a为0,才运行完才释放锁
        //这期间其他售票窗口无法进入,无法卖票,当a=0时,其他窗口才获取到该锁,才能执行synchronized代码块,
        // 而此时已没票
        public static void sale1(){
            synchronized (lock){   //同步代码块,某一时刻只允许一个线程进来,运行完释放锁
                while (a>0){
                    try{
                        Thread.sleep(500); //模拟买票网络延迟
                        System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
                    }catch (Exception e){
                    }
                }
                System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
            }
        }

        //线程安全,较为合理
        public static void sale2(){
            while (a>0){   //有票,无锁,多个线程(售票窗口)可以同时进入
                synchronized (lock){   //同步代码块,只能一个线程进入,运行完释放锁
                    if(a>0){       //因为当a=1时,可能会有多个线程跑通了a>0,在sync外等待,但只能给其中一个,需要再次判断是否有票
                        try{
                            Thread.sleep(500); //模拟买票网络延迟
                            System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"号票");
                        }catch (Exception e){
                        }
                    }else {
                        System.out.println(Thread.currentThread().getName()+"票已卖完。。。");
                    }
                }
            }
        }
    }
}

代码较简单,也有注释,就不多唠叨。

关于多线程,个人感觉比较抽象,每个人都有各自的理解。以上是个人以火车票售票系统作为应用场景,及代码的具体实现,来理解多线程,及线程安全,同步锁等,如有不妥之处,望多多指教!

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章