多线程-初阶(synchronized关键字和volatile关键字waitsleep 死锁)

Posted 秃头小宝儿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程-初阶(synchronized关键字和volatile关键字waitsleep 死锁)相关的知识,希望对你有一定的参考价值。

1.Synchronized关键字

(1)synchronized的实现

  • 1.针对操作系统层面,它是依靠互斥锁mutex.
  • 2.针对JVM,使用monitor来实现。
    在这里插入图片描述锁信息 monitor:
    在这里插入图片描述
  • 3.针对Java语言来说,是将锁信息存放在对象头(标识,标识锁状态/锁的拥有者)

(2)synchronized的3种使用场景

1.使用synchronized修饰代码块(可以给任意对象进行加锁)
2.使用synchronized来修饰静态方法(对当前的类进行加锁)
3.使用synchronized来修饰普通方法(对当前类实例进行加锁)

(3)synchronized锁升级的过程(jdk 1.6优化)

重量级:用户态–>内核态(有特别大的性能开销)
在这里插入图片描述

(4)synchronized和Lock的区别

  • 1.关键字不同
  • 2.synchronized自定义进行加锁和释放锁,而Lock需要手动的加锁和释放锁。
  • 3.Lock是 Java层面的锁的实现,而synchronized是Jvm层面的实现。
  • 4.synchronized和 Lock适用范围不同,Lock只能用来修饰代码块,而synchronized即可以修饰代码块,还可以修饰静态方法和普通方法。
  • 5.synchronized锁的模式只有是非公平锁模式,而Lock既可以使用公平锁,也可以使用非公平锁的模式。
  • 6.Lock的灵活性更高(tryLock)。

(5)Lock手动锁

// 1.创建lock实例
        Lock lock = new ReentrantLock(true);//公平锁
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    // 2.加锁
                    lock.lock();
                    try {
                        // 业务操作
                        number++;
                    } finally {
                        // 3.释放锁
                        lock.unlock();
                    }
                }
            }
        });

注意事项:lock()操作一定要放在try外面。如果放在try里面可能造成两个问题:

  • 1.如果try里面抛出异常了,还没有加锁成功就执行finally里面的释放锁的操作。因为还没有加锁就释放锁。
  • 2.如果放在try里面,如果没有锁的情况下试图释放锁,这个时候产生的异常就会将业务代码(try里面的异常)给吞噬掉,增加了代码调试的难度。
  • 如果一定想把lock放在 try 里面的话,一定记得放在第一行。

(6)公平锁和非公平锁

公平锁调度:

1.一个线程释放锁。
2.(主动)唤醒”需要得到锁“的队列来得到锁。

非公平锁:当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁。
在Java语言中所有的锁的默认实现方式都是非公平锁

synchronized是非公平锁
ReentrantLock 默认是非公平锁,但也可以显示的声明为公平锁。

显示的声明公平锁:

     Lock lock=new ReentrantLock(true);

2.volatile关键字

在这里插入图片描述
volatile总结:volatile可以解决内存不可见和指令重排序的问题,但是不能解决原子性问题。

3.通信-对象的等待集wait set

(1)wait()方法

方法wait() 的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,调用wait方法前,必须先加锁synchronized(object)。
解决 wait /notify随即唤醒的问题—LockSupport park() /unpark(线程)
LockSupport虽然不会报Interrupt的异常,但依旧可以监听到线程中止的指令。

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
      System.out.println("等待中...");
      object.wait();//一定要先加锁后休眠
      System.out.println("等待已过...");
   }
     System.out.println("main方法结束...");
 }

注意事项:

当使用吧wait()方法时,当不传递任何参数时,线程进入waiting状态,当传递的参数大于 0 时,线程进入timed_waiting状态。
当调用 wait()不传递参数的时候,其底层实现也是调用了wait(0)这个方法。
在这里插入图片描述

(2)面试题:wait和sleep区别?

相同点

  • 1.wait 和 sleep都是让线程进行休眠状态。
  • 2.wait 和 sleep在执行的过程中都可以接收到终止线程执行的通知。

不同点

  • 1.wait 使用必须配合 synchronized一起使用,而sleep不用。
  • 2.wait 在执行的时候会释放锁,而使用sleep不会释放锁。
  • 3.wait是Object的方法,sleep是Thread(线程)的方法。
  • 4.默认情况下wait(不传递任何参数或者是参数为0的情况下)它进入waiting状态,而sleep会进入timed_waiting状态。
  • 5.使用wait时可以主动的唤醒线程,而使用sleep时不能主动的唤醒线程。

(3)为什么wait会释放锁?而sleep不会释放锁?

sleep必须要传递一个最大等待时间的,也说sleep是可控的(对于时间层面来说),而wait 是可以不传递参数的,从设计层面来讲如果让wait这个没有超时等待时间的机制不释放锁的话,那么线程可能会一直阻塞,而sleep不存在这个问题。

(4)为什么wait是Object的方法?而sleep是Thread的方法?

wait需要操作锁,而锁是属于对象级别(所有的锁都是放在对象头中的),它不是线程级别,一个线程中可以有多把锁,为了灵活起见,所以wait放在Object当中。

(5)sleep(0)和 wait(0)有什么区别?

1.sleep(0) 表示 0毫秒之后继续执行,而wait(0)表示一直休眠。
2.sleep(0)表示重新触发一次CPU 竞争。

(6)wait和 notify的使用:

public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        System.out.println("t1 wait 之前");
                        lock.wait();
                        System.out.println("t2 wait 之后");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        System.out.println("t2 wait 之前");
                        lock.wait();
                        System.out.println("t2 wait 之后");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t2");
        t2.start();

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        System.out.println("t3 wait 之前");
                        lock.wait();
                        System.out.println("t3 wait 之后");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t3");
        t3.start();

        Thread.sleep(500);
        System.out.println("主线程调用唤醒方法");
        // 在主线程中唤醒线程 t1
        synchronized (lock) {
            lock.notify();
//      lock.notifyAll();
        }
    }

wait注意事项

1.wait方法在执行之前必须先加锁,也就是说wait方法一定要配合synchronized一起使用。
2.wait和notify在配合synchronized使用时,一定要注意使用同一把锁。
3.wait 和 notify在配合使用时,一定要操作同一把锁。

4.死锁

定义:在两个或者两个以上线程运行中,因为资源抢占而造成线程一直等待的问题。
在这里插入图片描述

(1)造成死锁的4个条件

  • 1.互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了。(不可更改)
  • 2.请求拥有条件:当一个线程拥有了一个资源后又试图请求两一个资源。(可以解决)
  • 3.不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源。(不可更改)
  • 4.环路等待条件:两个或者两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路。(可以解决)

(2)如何解决死锁问题?

解决造成死锁的4个条件中的 请求拥有条件或者 环路等待条件。
控制加锁的顺序。

(3)精简版的死锁代码

synchronized(lockA){
           Thread.sleep(1000);
          synchronized(lockB){
          //业务代码。。。
          }
   }
synchronized(lockB){
           Thread.sleep(1000);
          synchronized(lockA){
          //业务代码。。。
          }
   }

以上是关于多线程-初阶(synchronized关键字和volatile关键字waitsleep 死锁)的主要内容,如果未能解决你的问题,请参考以下文章

synchronized关键字的使用及互斥锁的实现

Java并发机制之Volatile关键字

Java多线程:synchronized关键字和Lock

26 多线程——线程安全 synchronized

Java多线程同步 synchronized 关键字的使用

Java多线程系列---“基础篇”04之 synchronized关键字