琐碎

2020-01-02  本文已影响0人  今年五年级

java在每个对象上都关联了一个监视器和一个等待集合(wait sets,是一个线程集合),即有监视器这个东西

操作监视器:

synchronized作用于方法:称为同步方法,同步方法被调用时,自动执行加锁操作,只有加锁成功,方法体才会得到执行

如果被synchronized修饰的方法是实例方法,那么该实例的监视器会被锁定
如果是static静态方法,那么线程会锁住相应的Class对象的监视器,方法体执行完或者遇到异常退出后,自动执行解锁操作

面试:
1 一个类中两个synchronized static方法之间是否构成同步
构成同步
2 synchronized 作用于静态方法时是对 Class 对象加锁,作用于实例方法时是对实例加锁,他们之间不构成同步

注意:实际使用的时候可以用局部锁,即如果有4个线程,1和2线程获取到对象1,而3号线程获取到对象2,则应该在获取到具体的对象以后,1和2线程给对象1加锁,3线程给对象2加锁,而不是最外围4个线程获取对象前加锁,这样效率是低下的。

案例代码:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

@Slf4j
public class Test {

    private static ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        map.put("obj1", new Object());
        map.put("obj2", new Object());

        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
            int finalI = i;
            threads[i] = new Thread(() -> {
                oneObject(finalI);
//                twoObject(finalI);
            });

            threads[i].start();
        }

        Stream.of(threads).forEach(x -> {
            try {
                x.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("end");

    }


    private static void twoObject(int i) {

        Object test;
        if (i == 0)
            test = map.get("obj1");
        else
            test = map.get("obj2");

        synchronized (test) {
            log.info(Thread.currentThread().getName() + "获取锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info(Thread.currentThread().getName() + "释放锁 " + test);
        }
    }

    private static void oneObject(int i) {
        Object test;
        if (i == 0)
            test = map.get("obj1");
        else
            test = map.get("obj1");

        synchronized (test) {
            log.info(Thread.currentThread().getName() + "获取锁");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info(Thread.currentThread().getName() + "释放锁 " + test);
        }
    }


}

执行oneObject()结果:

执行twoObject()结果:

可以从上面清晰的看到2者的区别

我们以前在实际使用的时候可以采用如下加锁方式
优化前代码:

      synchronized(obj){  //或者直接方法锁
          GroupKey groupKey = new GroupKey();
          BatchHolder holder = getOrCreateBatchHolder(groupKey);
          return appendToHolder(groupKey);
      }

但是这种优化前代码鸡肋且效率低下,我们用上面学到的理论来优化这段代码:

优化后代码:

      GroupKey groupKey = new GroupKey();
      BatchHolder holder = getOrCreateBatchHolder(groupKey);
      synchronized (holder) {
        return appendToHolder(groupKey);
      }

优化后使得只有获得相同holder的线程的操作才加锁,获得不同holder的并发执行,提高了执行效率

等待集合:

对集合进行操纵:Object.wait,Object.notify,Object.notifyAll
sleep(),join()可以感知到线程的wait和notify

调用wait方法后,线程释放锁,然后重新进入等待集合,被其他线程唤醒(notify)后,从等待集合移出来,
但是无法马上往下执行,该线程还需要重新获取锁才行

wait有可能被假唤醒

每个线程在一系列(可能导致它从等待集合中移除出去)的事件中,必须决定一个顺序,必须表现为他是按照那个顺序发生的

如果线程 t 被中断,此时中断状态为 true,则 wait 方法将抛出 InterruptedException 异常,并将中断状态重新设置为 false。

Notify:

唤醒的时候,线程t随机唤醒某个等待集合线程m,唤醒后,m线程加锁不会成功,直到线程t完全释放锁,因为调用notify不会释放锁
wait会阻塞,notify不会阻塞

Interrupt:

中断
线程自己也可以在自己执行代码中调用Thread.interrupt()
设置中断状态为true的时候,如下几个方法会感知:wait(),join(),sleep(),这些方法方法声明上都有throws InterruptedException,
这个就是用来响应中断状态修改的

线程阻塞在上面几个方法中,当线程感知到中断状态设置为true后(次线程的interrupt()方法被调用),会将中断状态重新设置为false,
然后执行相应的操作(通常是跳到catch异常处)

LockSupport的park()方法也能自动感知到线程被中断,但是他不会重置中断状态为false,只有上面的wait,join,sleep会在感知到
中断后先重置中断状态为false,再继续执行

注意 keyPoint
如果有一个对象 m,而且线程 t1此时在 m 的等待集合中
线程t2设置线程t1的中断状态->t1线程恢复->t1获取对象m的监视器锁->获取锁之后,抛出interrutpedException
线程t1被中断,wait方法返回,并不会立即抛出interruptedException异常,而是在重新获取监视器锁之后才会抛出异常

interrupt:仅仅是设置线程的中断标志位为true

thread.isInterrupted()可以知道线程的中断状态,不做其他操作
thread.interrupted()可以返回当前线程的中断状态, 同时将中断状态设置为false

notify和中断的影响

@Slf4j
public class WaitNotify {

    volatile int a = 0;

    public static void main(String[] args) throws InterruptedException {

        Object object = new Object();

        WaitNotify waitNotify = new WaitNotify();

        Thread thread1 = new Thread(() -> {

            synchronized (object) {
                log.info("线程1 获取到监视器锁");
                try {
                    object.wait();  //释放锁,进入等待队列
                    log.info("线程1 正常恢复啦。中断状态是"+Thread.currentThread().isInterrupted());
                } catch (InterruptedException e) {
                    log.info("线程1 wait方法抛出了InterruptedException异常");
                }
            }
        }, "线程1");
        thread1.start();

        Thread thread2 = new Thread(() -> {

            synchronized (object) {
                log.info("线程2 获取到监视器锁");
                try {
                    object.wait();  //释放锁,进入等待队列
                    log.info("线程2 正常恢复啦。");
                } catch (InterruptedException e) {
                    log.info("线程2 wait方法抛出了InterruptedException异常");
                }
            }
        }, "线程2");
        thread2.start();

        // 这里让 thread1 和 thread2 先起来,然后再起后面的 thread3
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        Thread t3=new Thread(() -> {
            synchronized (object) {
                log.info("线程3 拿到了监视器锁。");
                log.info("线程3 设置线程1中断");
                thread1.interrupt(); // 1
                waitNotify.a = 1; // 这行是为了禁止上下的两行中断和notify代码重排序
                log.info("线程3 调用notify");
                object.notify(); //2        //这里notify是随机唤醒,如果是唤醒线程2,则线程1因为中断最后获取到锁
                log.info("线程3 调用完notify后,休息一会");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                }
                log.info("线程3 休息够了,结束同步代码块");
            }
        }, "线程3");

        t3.start();

        thread1.join();
        thread2.join();
        t3.join();

        System.out.println("主线程退出");

    }
}

一般情况下,上面代码会出现如下结论:

然而,同样存在如下情况:

有可能发生 线程1 是正常恢复的,虽然发生了中断,它的中断状态也确实是 true,但是它没有抛出 InterruptedException,而是正常返回。此时,thread2 将得不到唤醒,一直 wait。

如果一个线程在等待期间,同时发生了通知和中断,它将可能发生如下两种情况:

  1. 从 wait 方法中正常返回,同时不改变中断状态(也就是说,调用 Thread.isInterrupted 方法将会返回 true)
  2. 由于抛出了 InterruptedException 异常而从 wait 方法中返回,中断状态设置为 false

wait有可能是假唤醒,即有顺序关系,线程可能因为被notify唤醒,也可能因为中断唤醒,如果它没有因为中断唤醒,则不会抛出interruptedException。并且不会重置中断标志位为false,即你打印 isInterrupted 仍然是其他线程给他设置的true

可重入锁和不可重入锁的区别

可重入锁:如果我们现在有个方法A,方法A中调用了方法B,而且这两个方法都要加锁,A加的是1号锁,B同时也想加这个1号锁,如果是可重入的,如果是可重入的,A方法一进来加了锁了那A方法执行了,A方法要调用里面的B方法,B方法相当于也要跟A加同一把锁,B一看A已经加了这把锁了,那B方法就直接拿来用,相当于B方法可以直接执行,执行完以后A释放锁即可

如果设计为不可重入锁那就糟糕了:如果A要加1号锁,A里面调用B,B也想加1号锁。A把1号锁持有了,B相当于要等待A释放1号锁它才能抢到1号锁,这就是不可重入锁的设计。这样很明显是有问题的, 这就是一个死锁,B永远等不到A去来释放,因为A还想等着B执行完了才释放

所以一句话:所有的锁都应该设计为可重入锁,避免死锁问题

上一篇下一篇

猜你喜欢

热点阅读