zoukankan      html  css  js  c++  java
  • Java高并发学习笔记(二):线程安全与ThreadGroup

    1 来源

    • 来源:《Java高并发编程详解 多线程与架构设计》,汪文君著
    • 章节:第四、六章

    本文是两章的笔记整理。

    2 概述

    本文主要讲述了synchronized以及ThreadGroup的基本用法。

    3 synchronized

    3.1 简介

    synchronized可以防止线程干扰和内存一致性错误,具体表现如下:

    • synchronized提供了一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致的问题
    • synchronized包括monitor entermonitor exit两个JVM指令,能保证在任何时候任何线程执行到monitor enter成功之前都必须从主存获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存而不是仅仅在缓存中
    • synchronized指令严格遵循Happens-Beofre规则,一个monitor exit指令之前必定要有一个monitor enter

    3.2 基本用法

    synchronized的基本用法可以用于对代码块或方法进行修饰,比如:

    private final Object MUTEX = new Object();
        
    public void sync1(){
        synchronized (MUTEX){
        }
    }
    
    public synchronized void sync2(){
    }
    

    3.3 字节码简单分析

    一个简单的例子如下:

    public class Main {
        private static final Object MUTEX = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            final Main m = new Main();
            for (int i = 0; i < 5; i++) {
                new Thread(m::access).start();
            }
        }
    
        public void access(){
            synchronized (MUTEX){
                try{
                    TimeUnit.SECONDS.sleep(20);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
    

    编译后查看字节码:

    javap -v -c -s -l Main.class
    

    access()字节码截取如下:

    stack=3, locals=4, args_size=1
     0: getstatic     #9                  // Field MUTEX:Ljava/lang/Object;  获取MUTEX
     3: dup
     4: astore_1
     5: monitorenter					  // 执行monitor enter指令
     6: getstatic     #10                 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
     9: ldc2_w        #11                 // long 20l
    12: invokevirtual #13                 // Method java/util/concurrent/TimeUnit.sleep:(J)V
    15: goto          23				  // 正常退出,跳转到字节码偏移量23的地方
    18: astore_2
    19: aload_2
    20: invokevirtual #15                 // Method java/lang/InterruptedException.printStackTrace:()V
    23: aload_1
    24: monitorexit						  // monitor exit指令
    25: goto          33
    28: astore_3
    29: aload_1
    30: monitorexit
    31: aload_3
    32: athrow
    33: return
    

    关于monitorentermonitorexit说明如下:

    • monitorenter:每一个对象与一个monitor相对应,一个线程尝试获取与对象关联的monitor的时候,如果monitor的计数器为0,会获得之后立即对计数器加1,如果一个已经拥有monitor所有权的线程重入,将导致计数器再次累加,而如果其他线程尝试获取时,会一直阻塞直到monitor的计数器变为0,才能再次尝试获取对monitor的所有权
    • monitorexit:释放对monitor的所有权,将monitor的计数器减1,如果计数器为0,意味着该线程不再拥有对monitor的所有权

    3.4 注意事项

    3.4.1 非空对象

    monitor关联的对象不能为空:

    private Object MUTEX = null;
    private void sync(){
        synchronized (MUTEX){
    
        }
    }
    

    会直接抛出空指针异常。

    3.4.2 作用域不当

    由于synchronized关键字存在排它性,作用域越大,往往意味着效率越低,甚至丧失并发优势,比如:

    private synchronized void sync(){
        method1();
        syncMethod();
        method2();
    }
    

    其中只有第二个方法是并发操作,那么可以修改为

    private Object MUTEX = new Object();
    private void sync(){
        method1();
        synchronized (MUTEX){
            syncMethod();
        }
        method2();
    }
    

    3.4.3 使用不同的对象

    因为一个对象与一个monitor相关联,如果使用不同的对象,这样就失去了同步的意义,例子如下:

    public class Main {
        public static class Task implements Runnable{
            private final Object MUTEX = new Object();
    
            @Override
            public void run(){
                synchronized (MUTEX){
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 20; i++) {
                new Thread(new Task()).start();
            }
        }
    }
    

    每一个线程争夺的monitor都是互相独立的,这样就失去了同步的意义,起不到互斥的作用。

    3.5 死锁

    另外,使用synchronized还需要注意的是有可能造成死锁的问题,先来看一下造成死锁可能的原因。

    3.5.1 死锁成因

    • 交叉锁导致程序死锁:比如线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁
    • 内存不足:比如两个线程T1和T2,T1已获取10MB内存,T2获取了15MB内存,T1和T2都需要获取30MB内存才能工作,但是剩余可用的内存为10MB,这样两个线程都在等待彼此释放内存资源
    • 一问一答式的数据交换:服务器开启某个端口,等待客户端访问,客户端发送请求后,服务器因某些原因错过了客户端请求,导致客户端等待服务器回应,而服务器等待客户端发送请求
    • 死循环引起的死锁:比较常见,使用jstack等工具看不到死锁,但是程序不工作,CPU占有率高,这种死锁也叫系统假死,难以排查和重现

    3.5.2 例子

    public class Main {
        private final Object MUTEX_READ = new Object();
        private final Object MUTEX_WRITE = new Object();
    
        public void read(){
            synchronized (MUTEX_READ){
                synchronized (MUTEX_WRITE){
                }
            }
        }
    
        public void write(){
            synchronized (MUTEX_WRITE){
                synchronized (MUTEX_READ){
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Main m = new Main();
            new Thread(()->{
                while (true){
                    m.read();
                }
            }).start();
            new Thread(()->{
                while (true){
                    m.write();
                }
            }).start();
        }
    }
    

    两个线程分别占有MUTEX_READ/MUTEX_WRITE,同时等待另一个线程释放MUTEX_WRITE/MUTEX_READ,这就是交叉锁造成的死锁。

    3.5.3 排查

    使用jps找到进程后,通过jstack查看:

    在这里插入图片描述

    可以看到明确的提示找到了1个死锁,Thread-0等待被Thread-1占有的monitor,而Thread-1等待被Thread-0占有的monitor

    3.6 两个特殊的monitor

    这里介绍两个特殊的monitor

    • this monitor
    • class monitor

    3.6.1 this monitor

    先上一段代码:

    public class Main {
        public synchronized void method1(){
            System.out.println(Thread.currentThread().getName()+" method1");
            try{
                TimeUnit.MINUTES.sleep(5);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    
        public synchronized void method2(){
            System.out.println(Thread.currentThread().getName()+" method2");
            try{
                TimeUnit.MINUTES.sleep(5);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Main m = new Main();
            new Thread(m::method1).start();
            new Thread(m::method2).start();
        }
    }
    

    运行之后可以发现,只有一行输出,也就是说,只是运行了其中一个方法,另一个方法根本没有执行,使用jstack可以发现:

    在这里插入图片描述

    一个线程处于休眠中,而另一个线程处于阻塞中。而如果将method2()修改如下:

    public void method2(){
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " method2");
            try {
                TimeUnit.MINUTES.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    效果是一样的。也就是说,在方法上使用synchronized,等价于synchronized(this)

    3.6.2 class monitor

    把上面的代码中的方法修改为静态方法:

    public class Main {
        public static synchronized void method1() {
            System.out.println(Thread.currentThread().getName() + " method1");
            try {
                TimeUnit.MINUTES.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static synchronized void method2() {
            System.out.println(Thread.currentThread().getName() + " method2");
            try {
                TimeUnit.MINUTES.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(Main::method1).start();
            new Thread(Main::method2).start();
        }
    }
    

    运行之后可以发现输出还是只有一行,也就是说只运行了其中一个方法,jstack分析也类似:

    在这里插入图片描述

    而如果将method2()修改如下:

    public static void method2() {
        synchronized (Main.class) {
            System.out.println(Thread.currentThread().getName() + " method2");
            try {
                TimeUnit.MINUTES.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    可以发现输出还是一致,也就是说,在静态方法上的synchronized,等价于synchronized(XXX.class)

    3.6.3 总结

    • this monitor:在成员方法上的synchronized,就是this monitor,等价于在方法中使用synchronized(this)
    • class monitor:在静态方法上的synchronized,就是class monitor,等价于在静态方法中使用synchronized(XXX.class)

    4 ThreadGroup

    4.1 简介

    无论什么情况下,一个新创建的线程都会加入某个ThreadGroup中:

    • 如果新建线程没有指定ThreadGroup,默认就是main线程所在的ThreadGroup
    • 如果指定了ThreadGroup,那么就加入该ThreadGroup

    ThreadGroup中存在父子关系,一个ThreadGroup可以存在子ThreadGroup

    4.2 创建

    创建ThreadGroup可以直接通过构造方法创建,构造方法有两个,一个是直接指定名字(ThreadGroupmain线程的ThreadGroup),一个是带有父ThreadGroup与名字的构造方法:

    ThreadGroup group1 = new ThreadGroup("name");
    ThreadGroup group2 = new ThreadGroup(group1,"name2");
    

    完整例子:

    public static void main(String[] args) throws InterruptedException {
        ThreadGroup group1 = new ThreadGroup("name");
        ThreadGroup group2 = new ThreadGroup(group1,"name2");
        System.out.println(group2.getParent() == group1);
        System.out.println(group1.getParent().getName());
    }
    

    输出结果:

    true
    main
    

    4.3 enumerate()

    enumerate()可用于ThreadThreadGroup的复制,因为一个ThreadGroup可以加入若干个Thread以及若干个子ThreadGroup,使用该方法可以方便地进行复制。方法描述如下:

    • public int enumerate(Thread [] list)
    • public int enumerate(Thread [] list, boolean recurse)
    • public int enumerate(ThreadGroup [] list)
    • public int enumerate(ThreadGroup [] list, boolean recurse)

    上述方法会将ThreadGroup中的活跃线程/ThreadGroup复制到Thread/ThreadGroup数组中,布尔参数表示是否开启递归复制。

    例子如下:

    public static void main(String[] args) throws InterruptedException {
        ThreadGroup myGroup = new ThreadGroup("MyGroup");
        Thread thread = new Thread(myGroup,()->{
            while (true){
                try{
                    TimeUnit.SECONDS.sleep(1);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        },"MyThread");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(1);
        ThreadGroup mainGroup = currentThread().getThreadGroup();
        Thread[] list = new Thread[mainGroup.activeCount()];
        int recurseSize = mainGroup.enumerate(list);
        System.out.println(recurseSize);
        recurseSize = mainGroup.enumerate(list,false);
        System.out.println(recurseSize);
    }
    

    后一个输出比前一个少1,因为不包含myGroup中的线程(递归设置为false)。需要注意的是,enumerate()获取的线程仅仅是一个预估值,并不能百分百地保证当前group的活跃线程,比如调用复制之后,某个线程结束了生命周期或者新的线程加入进来,都会导致数据不准确。另外,返回的int值相较起Thread[]的长度更为真实,因为enumerate仅仅将当前活跃的线程分别放进数组中,而返回值int代表的是真实的数量而不是数组的长度。

    4.4 其他API

    • activeCount():获取group中活跃的线程,估计值
    • activeGroupCount():获取group中活跃的子group,也是一个近似值,会递归获取所有的子group
    • getMaxPriority():用于获取group的优先级,默认情况下,group的优先级为10,且所有线程的优先级不得大于线程所在group的优先级
    • getName():获取group名字
    • getParent():获取父group,如果不存在返回null
    • list():一个输出方法,递归输出所有活跃线程信息到控制台
    • parentOf(ThreadGroup g):判断当前group是不是给定group的父group,如果给定的group是自己本身,也会返回true
    • setMaxPriority(int pri):指定group的最大优先级,设定后也会改变所有子group的最大优先级,另外,修改优先级后会出现线程优先级大于group优先级的情况,比如线程优先级为10,设置group优先级为5后,线程优先级就大于group优先级,但是新加入的线程优先级必须不能大于group优先级
    • interrupt():导致所有的活跃线程被中断,递归调用线程的interrupt()
    • destroy():如果没有任何活跃线程,调用后在父group中将自己移除
    • setDaemon(boolean daemon):设置为守护ThreadGroup后,如果该ThreadGroup没有任何活跃线程,自动被销毁
  • 相关阅读:
    bzoj3064: Tyvj 1518 CPU监控
    bzoj3272: Zgg吃东西&&3267: KC采花
    bzoj2759: 一个动态树好题
    bzoj4594: [Shoi2015]零件组装机
    bzoj4873: [Shoi2017]寿司餐厅
    bzoj4593: [Shoi2015]聚变反应炉
    codeforces 739E
    bzoj2034: [2009国家集训队]最大收益
    mybatis-generator使用心得
    Linux 各种软件的安装-Jenkins和svn结合
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/14752781.html
Copyright © 2011-2022 走看看