本文是两章的笔记整理。java
本文主要讲述了synchronized
以及ThreadGroup
的基本用法。编程
synchronized
synchronized
能够防止线程干扰和内存一致性错误,具体表现以下:数组
synchronized
提供了一种锁机制,可以确保共享变量的互斥访问,从而防止数据不一致的问题synchronized
包括monitor enter
和monitor exit
两个JVM
指令,能保证在任什么时候候任何线程执行到monitor enter
成功以前都必须从主存获取数据,而不是从缓存中,在monitor exit
运行成功以后,共享变量被更新后的值必须刷入主内存而不是仅仅在缓存中synchronized
指令严格遵循Happens-Beofre
规则,一个monitor exit
指令以前一定要有一个monitor enter
synchronized
的基本用法能够用于对代码块或方法进行修饰,好比:缓存
private final Object MUTEX = new Object(); public void sync1(){ synchronized (MUTEX){ } } public synchronized void sync2(){ }
一个简单的例子以下: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
关于monitorenter
与monitorexit
说明以下:架构
monitorenter
:每个对象与一个monitor
相对应,一个线程尝试获取与对象关联的monitor
的时候,若是monitor
的计数器为0,会得到以后当即对计数器加1,若是一个已经拥有monitor
全部权的线程重入,将致使计数器再次累加,而若是其余线程尝试获取时,会一直阻塞直到monitor
的计数器变为0,才能再次尝试获取对monitor
的全部权monitorexit
:释放对monitor
的全部权,将monitor
的计数器减1,若是计数器为0,意味着该线程再也不拥有对monitor
的全部权与monitor
关联的对象不能为空:并发
private Object MUTEX = null; private void sync(){ synchronized (MUTEX){ } }
会直接抛出空指针异常。app
因为synchronized
关键字存在排它性,做用域越大,每每意味着效率越低,甚至丧失并发优点,好比:
private synchronized void sync(){ method1(); syncMethod(); method2(); }
其中只有第二个方法是并发操做,那么能够修改成
private Object MUTEX = new Object(); private void sync(){ method1(); synchronized (MUTEX){ syncMethod(); } method2(); }
由于一个对象与一个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
都是互相独立的,这样就失去了同步的意义,起不到互斥的做用。
另外,使用synchronized
还须要注意的是有可能形成死锁的问题,先来看一下形成死锁可能的缘由。
jstack
等工具看不到死锁,可是程序不工做,CPU
占有率高,这种死锁也叫系统假死,难以排查和重现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
,这就是交叉锁形成的死锁。
使用jps
找到进程后,经过jstack
查看:
能够看到明确的提示找到了1个死锁,Thread-0
等待被Thread-1
占有的monitor
,而Thread-1
等待被Thread-0
占有的monitor
。
monitor
这里介绍两个特殊的monitor
:
this monitor
class monitor
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)
。
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)
。
this monitor
:在成员方法上的synchronized
,就是this monitor
,等价于在方法中使用synchronized(this)
class monitor
:在静态方法上的synchronized
,就是class monitor
,等价于在静态方法中使用synchronized(XXX.class)
ThreadGroup
不管什么状况下,一个新建立的线程都会加入某个ThreadGroup
中:
ThreadGroup
,默认就是main
线程所在的ThreadGroup
ThreadGroup
,那么就加入该ThreadGroup
中ThreadGroup
中存在父子关系,一个ThreadGroup
能够存在子ThreadGroup
。
建立ThreadGroup
能够直接经过构造方法建立,构造方法有两个,一个是直接指定名字(ThreadGroup
为main
线程的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
enumerate()
enumerate()
可用于Thread
和ThreadGroup
的复制,由于一个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
表明的是真实的数量而不是数组的长度。
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
没有任何活跃线程,自动被销毁