Java高并发学习笔记(二):线程安全与ThreadGroup
1 来源
- 来源:《Java高并发编程详解 多线程与架构设计》,汪文君著
- 章节:第四、六章
本文是两章的笔记整理。
2 概述
本文主要讲述了synchronized
以及ThreadGroup
的基本用法。
3 synchronized
3.1 简介
synchronized
可以防止线程干扰和内存一致性错误,具体表现如下:
synchronized
提供了一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致的问题synchronized
包括monitor enter
和monitor 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
关于monitorenter
与monitorexit
说明如下:
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
可以直接通过构造方法创建,构造方法有两个,一个是直接指定名字(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
4.3 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
代表的是真实的数量而不是数组的长度。
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
没有任何活跃线程,自动被销毁