[Java高并发系列(1)]Java中synchronized关键字详解+死锁实例

[Java高并发系列]Java 中 synchronized 关键字详解 + 死锁实例

1 概述

synchronized用于给某个对象加锁 , 其修饰的对象可以是代码块和方法(分为实例方法和静态方法).

不是说synchronized是对代码块或方法上锁, 它锁的是对象(当然也可能是类, 不过类也算对象吧hhh — 某个Object , 或者this, 或者 xxx.class)

下面来一一分析.

2 给某个对象加锁

2.1 对一个Object加锁

当某个线程中对这个object加上锁后 , 其他线程想再拿, 就得等到代码块结束后释放出这个对象, 然后再锁上该对象, 执行自己的块

e.g.1

/**
 * 有一个类T , 里面一个m方法对其私有变量count 进行减减操作
 *
 *synchronized 关键字
 * 对某个对象加锁
 * @author  lowfree
 */

public class T {
    private  int count = 10 ;

    Object o = new Object();
    
    public void m(){
        synchronized (o){       // 锁定的是整个对象
            count --;
            System.out.println(Thread.currentThread().getName() + "count " + count);
        }
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() );
        T t =new T();							//new 一个 T 对象, 对其count进行 -- 操作
        for(int i = 0 ; i < 10 ; i ++) {		//一个线程来减太慢了 ,创建10 个线程大家一起做这个事情
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t.m();						//显然 , 如果m不用synchronized包起来,则会出现多线程访问临界资源问题
                }
            }, "T_thread : ").start();
        }

    }
}

上述例子中, 一个线程中将对象实例o 拿到时, 其他线程想进入锁了o的代码块时, 得等到这个线程将这个代码块执行完成,释放了o对象才行.

上述第一个线程锁上o, 进行count-- 操作, 打印消息到屏幕这两个操作之间不会有其他线程进行争夺.

下面我们抽理出来在看看:

    Object o = new Object();
    public void m(){
        synchronized (o){       
            //  操作1
        }
        //操作2
    }

实际上synchronized关键字使得能够对o上锁, m方法中的操作1变得不可分割.

2.2 对this 对象上锁

e.g.2

    /* 相类似,  */
	public  void  m1(){
        synchronized (this) {    //任何线程要执行下面的代码, 必须先拿到this的锁
            count -- ;
            System.out.println(Thread.currentThread().getName() + "count =" + count);
        }
    }

说白了, 把自己当成一个锁的对象, 只有这个线程拿到调用m方法的对象时, 才能进入代码块.

进一步, 实际上如果写成上面那样, 是可以简写的

e.g.3

    public synchronized   void  m2(){     //相当于在方法的代码执行时要synchronized(this)
        count -- ;
        System.out.println(Thread.currentThread().getName() + "count =" + count);
    }

所以, 对一个方法使用synchronized 关键字, 就是执行该方法时要拿到this对象的锁.

2.3 对class对象上锁

那么要是这个方法是静态方法呢? 看下面这个例子:

e.g.4

public class T {
    private  static int count = 10 ;
     public  synchronized static  void m(){     //这里相当于synchronized(包名.T.class)
         count --;
         System.out.println(Thread.currentThread().getName() + "count =" + count);
     }

     public  static void mm(){
         synchronized (T.class){    //考虑一下这里写synchronize(this) 是否可以?
                                    // 不行,静态属性方法是不需要new 出对象来访问的, 因此没有 this 引用得存在
                                    //所以锁定静态方法的时候, 相当于锁定当前类的class对象
             count -- ;
         }
     }
}

2.4 小结

有些文章喜欢把2.2 中 e.g.3 叫为方法锁 , 把2.3 中称为类锁, 但实际上都是对对象上锁.

img

3 sychronized的特点

  1. 保证原子性 , 可见性和有序性.

    ​ 可见性 : 释放锁时, 所有写入都会写回内存

    ​ 获得锁后, 都会从内存读取最新数据

  2. 可重入性

    对同个线程在获得锁后, 在调用其他需同样锁的代码可直接调用 考虑一个

    原理

    ​ a) 记录锁的持有线程 & 持有数量

    ​ b) 调用synchronized 代码 时检查对象是否已经被锁

    ​ 是 ==> 检查是否被当前线程锁定 & 若是 则计数加 1

    ​ 不是 ==> 加入等待队列

    ​ c) 释放时计数减 1 直到0 => 则释放锁

    下面是两个可重入的例子

    
    /**
     * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
     * 也就是说synchronized获得的锁是可重入的
     * 重入锁
     */
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        synchronized void m1() {
            System.out.println("m1 start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            m2();
        }
    
        synchronized void m2() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m2");
        }
    }
    
    /**
     * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
     * 也就是说synchronized获得的锁是可重入的
     * 这里是继承中有可能发生的情形,子类调用父类的同步方法
     */
    
    package base10;
    
    import java.util.concurrent.TimeUnit;
    
    public class T {
        synchronized void m(){
            System.out.println("m  start");
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m end");
        }
    
        public static void main(String[] args) {
            new TT().m();
        }
    
        }
        class  TT extends T{
            @Override
            synchronized void m() {
                System.out.println("child m start");
                super.m();
                System.out.println("child m start");
            }
        }
    
    
  3. 重量级

    底层通过一个监视器对象(monitor)完成 wait() , notify() 等方法也依赖于monitor 对象

    监视器锁(monitor) 的本质依赖于底层操作系统的互斥锁(Mutex lock) 实现

    上述切换过程较长 所有synchronized效率低 & 重量级

ps : 相关概念

img

另外: 程序在执行过程中,如果出现异常,默认情况锁 会被释放. 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。

比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据. 因此要非常小心的处理同步业务逻辑中的异常.


import java.util.concurrent.TimeUnit;
/**
 * @author  lowfree
 */
public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(count % 5 == 0) {
                int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {

            @Override
            public void run() {
                t.m();
            }

        };
        new Thread(r, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(r, "t2").start();
    }

}


4 一个用sychronized 实现死锁的例子

我们知道死锁指的是两个即以上进程/线程相互占有对方需要的资源, 且等待对方释放的一种阻塞状态

(具体来说由于系统资源竞争, 程序推进顺序非法, 死锁产生的四个必要条件: 互斥, 不可剥夺, 请求保持, 循环等待 产生的)

基于如此, 我们可以写一个包含两个线程t1,t2 ; 还有两个锁对象o1 , o2 的死锁例子

/**
 * 死锁
 * @author  lowfree
 */
public class DeadLock {
    Object oa = new Object();   //lock 1
    Object ob = new Object();   //lock 2

    public void m1(){									//m1 占据oa对象锁
        synchronized (oa) {
            System.out.println("m1 start ...");

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ob) {							//中途又想锁住 ob
                System.out.println("gotten ob ...");
            }
            System.out.println("m1 end ...");
        }
    }

    public void m2(){									//m2 先占据ob对象锁
        synchronized (ob) {
            System.out.println("m2 start ...");
            synchronized (oa) {							//中途又想  ma
                System.out.println("gotten oa ...");
            }
            System.out.println("m2 end ...");
        }
    }

    public static void main(String[] args) {
        DeadLock deadLock =new DeadLock();
        new Thread(new Runnable() {						//创建线程1并启动
            @Override
            public void run() {
                deadLock.m1();
            }
        },"thread 1").start();

        new Thread(new Runnable() {						//创建线程2并启动
            @Override
            public void run() {
                deadLock.m2();
            }
        },"thread 2").start();
    }
}

下面来解释下上述程序:

  1. 实例化了一个DeadLock对象, 其中创建了oa和ob对象,用作锁对象
  2. 创建线程 thread 1, 启动.
  3. thread 1 中deadLock调用m1方法, 占据oa锁资源, 然后线程休息2s.
  4. 这时主线程继续执行, 创建thread2 启动.
  5. thread 2 中deadLock调用m2方法. 占据ob锁资源.
  6. 紧接着thread2 中deadLock继续想占据 oa , 但是oa被thread 1 占据, 等待其释放吧.
  7. 这时候可能thread1 的2s sleep完成, 回到thread1中 ,开始想占据 ob.

这时候, thread1 占据oa 想要占据ob 继续执行, thread2 占据ob 想要占据oa继续执行 , 即成了死锁.

参考博客: https://blog.csdn.net/carson_ho/article/details/82992269

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值