Day32、33 尚硅谷JUC——多线程锁

我是大白==(●—●)==,这是我开始学习记录大白Java软件攻城狮晋升之路的第三十二、三十三天。今天学习的是【尚硅谷】大厂必备技术之JUC并发编程.

一、Synchronized的八种情况

共享变量类Phone

class Phone {

    public synchronized void sendSMS() throws InterruptedException {
        //停留4秒
//        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws InterruptedException {
        //停留4秒
//        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}

1. 标准访问,先打印短信还是邮件

public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

执行结果:image.png
原因分析:main线程sleep了100ms导致BB线程在AA线程后执行,synchronized锁的是当前对象,即this。

2. 停4秒在短信方法内,先打印短信还是邮件

    public synchronized void sendSMS() throws InterruptedException {
//        停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

执行结果:等待4秒后,先执行sendSMS在执行sendEmail方法image.png
原因分析:如果两个方法都是静态方法、或者都是非静态方法,并且都使用了synchronized修饰,而且没有在该方法内部调用了同步监视器的wait(),则其他线程不能进入其他使用synchronized方法修饰的方法。

引申:当一个线程进入一个对象的一个synchronized方法后,其他线程是否可以进入此对象的其他方法?

  1. 如果其他方法没有使用synchronized关键字修饰,则可以进入。
  2. 如果当前线程进入的synchronized方法是static方法,其他线程可以进入其他synchronized修饰的非静态方法;如果当前线程进入的synchronized方法是非static方法,其他线程可以进入其他synchronized修饰的静态方法。
  3. 如果两个方法都是静态方法、或者都是非静态方法,并且都使用了synchronized修饰,但只要在该方法内部调用了同步监视器的wait(),则其他线程依然可以进入其他使用synchronized方法修饰的方法。
  4. 如果两个方法都是静态方法、或者都是非静态方法,并且都使用了synchronized修饰,而且没有在该方法内部调用了同步监视器的wait(),则其他线程不能进入其他使用synchronized方法修饰的方法。

3. 新增普通的hello方法,是先打短信还是hello

public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            phone.getHello();
        }, "BB").start();
    }
}

运行结果:image.png
原因分析:如上一节所述

4. 现在有两部手机,先打印短信还是邮件

public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

运行结果:image.png
原因分析:两个对象,用的不是一把锁。

5. 两个静态同步方法,1部手机,先打印短信还是邮件

将共享变量Phone的方法加上static:

class Phone {
    public static synchronized void sendSMS() throws Exception {
//        停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public static synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}
public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

运行结果:image.png

6. 两个静态同步方法,2部手机,先打印短信还是邮件

public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone2.sendEmail();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

运行结果:image.png
原因分析:加上static后,锁的是Class对象,因此不同对象的锁也是相同的。

7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件

class Phone {
    public static synchronized void sendSMS() throws Exception {
//        停留4秒
        TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}

运行结果:image.png

8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件

public class Lock_8 {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone2.sendEmail();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

运行结果:image.png
原因分析:static synchronized锁的是Class类,synchronized锁的是对象,也可想象为前者是锁的整栋大楼,后者是锁的一间屋子,大楼被锁了,但是屋子没有被锁,仍然可以执行synchronized修饰的方法。

9.总结

synchronized实现同步的基础:Java中的每一个对象都可以作为锁
具体表现为以下3种形式。

  1. 对于普通同步方法,锁是当前实例对象
  2. 对于静态同步方法,锁是当前类的Class对象
  3. 对于同步方法块,锁是Synchonized括号里配置的对象

二、公平锁和非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

ReentrantLook初始化可以设是否为公平锁,其底层源码如下:

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

非公平锁:

/**
* 非公平锁直接进行操作
**/
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

公平锁:

    /**
     * 公平锁会先进行判断队列里是否有线程,有则进行排队
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

首先会通过hasQueuedPredecessors()方法判断等待队列里是否有线程排队,没有则直接执行任务,有则排队。

三、可重入锁

可重入锁也叫作递归锁,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,也就是可以重复获取相同的锁。
synchronized是隐式可重入锁无需手动加锁解锁,Lock是显式可重入锁,需要手动上锁和解锁且保证相同。例如下面代码

public class SyncLockDemo {
    public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(Thread.currentThread().getName() + " 外层");
                synchronized (o) {
                    System.out.println(Thread.currentThread().getName() + " 中层");
                    synchronized (o) {
                        System.out.println(Thread.currentThread().getName() + " 内层");
                    }
                }
            }
        }, "t1").start();
    }
}

运行结果:image.png
可以发现没有死锁,可以多次获取相同的锁。

接下来再看一个示例代码:

public class SyncLockDemo {

    public synchronized void add() {
        add();
    }

    public static void main(String[] args) {
        new SyncLockDemo().add();
    }
}

运行结果如下:
image.png
结果表明加锁的方法是可以递归。导致循环无限制的递归调用并导致堆栈空间用尽。也说明了可重入锁的特性。

接下来用Lock锁代码演示:

package com.example.demo.sync;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Administrator
 * <p>
 * 可重入锁
 */
public class SyncLockDemo {
    public static void main(String[] args) {
        Lock lock= new ReentrantLock();
        new Thread(() -> {
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "  外层");

                try{
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "  内层");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
    }
}

结果显示image.png
当删除掉一个lock.unlock()方法呢?

public class SyncLockDemo {
    public static void main(String[] args) {
        Lock lock= new ReentrantLock();
        new Thread(() -> {
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "  外层");

                try{
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "  内层");
                } finally {
                    //去掉一个解锁
//                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();
    }
}

会发现结果并没有影响image.png
这是因为可重入锁Lock锁住了外面但是仍然可以进入内部的锁。

public class SyncLockDemo {
    public static void main(String[] args) {
        Lock lock= new ReentrantLock();
        new Thread(() -> {
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + "  外层");

                try{
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "  内层");
                } finally {
//                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "t1").start();

        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "aaaa");
            lock.unlock();
        },"aa").start();
    }
}

此时会发现发生了阻塞,因为t1线程没有释放所有锁。aa线程无法获得锁,就无法进行操作。

四、死锁

1. 什么是死锁

死锁就是两个或者两个以上进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。
image.png

2. 产生死锁的原因

  1. 系统资源不足
  2. 进程运行推进顺序不合适
  3. 资源分配不当

我觉得上述说法还是相对比较笼统以及不全面,如下是我整理的产生死锁的四个必要条件:

  • 1)互斥条件:一个资源只能一个线程使用,直到该线程被释放
  • 2)请求与保持条件:当一个线程因请求获得资源时造成阻塞,该线程已获得的资源不释放。
  • 3)不剥夺条件:线程已经获得的资源,在未使用完,不可被其他进程剥夺,只有自己使用完才释放资源。
  • 4)循环等待条件:发生死锁时,所等待的线程必定会形成一个环路,造成永久阻塞。

3. 死锁代码

public class DeadLockDemo {
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (a) {
                System.out.println(Thread.currentThread().getName() + "持有锁a, 试图持有锁b");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println(Thread.currentThread().getName() + "获取锁b");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (b) {
                System.out.println(Thread.currentThread().getName() + "持有锁b, 试图持有锁a");
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (a) {
                    System.out.println(Thread.currentThread().getName() + "获取锁a");
                }
            }
        }, "B").start();

    }
}

结果如图所示
image.png

4.验证是否死锁

  1. 利用jps命令,类似Linuxps -ef,可查看进程端口。
  2. 利用jstack命令,是jvm自带的堆栈跟踪工具。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值