琐碎

作者: 今年五年级 | 来源:发表于2020-01-02 14:55 被阅读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执行完了才释放

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

    相关文章

      网友评论

          本文标题:琐碎

          本文链接:https://www.haomeiwen.com/subject/kafhictx.html