关于java:Java高并发学习笔记二线程安全与ThreadGroup

23次阅读

共计 7801 个字符,预计需要花费 20 分钟才能阅读完成。

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 没有任何沉闷线程,主动被销毁

正文完
 0