Java高并发学习笔记(二):线程安全与ThreadGroup

1 来源

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

本文是两章的笔记整理。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 字节码简单分析

一个简单的例子以下:bash

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){

    }
}

会直接抛出空指针异常。app

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没有任何活跃线程,自动被销毁
相关文章
相关标签/搜索