java中的synchronized的锁是有个膨胀的过程,时间越久获取的速度就越慢。所以有一些场景我们要针对线程,线程执行时间,以及线程个数有所调整。
执行时间可以通过合理控制锁的范围实现。个数就得根据业务来了。因为有可能同时出现锁竞争的概率很低,大部分时间都是一个线程在运行。
那如何去检测呢?
业务层面是不好区分的,究竟这个锁现在就几个线程在等待。java层面暂时没有提供一个api。我们把思路查看到jvmti,确实找到了对应的事件。我们下面来看看怎么利用jvmti来实现。
实现
jvmti有两种agent,一种是java的,一种是c/c++的。我们接下来使用的是c/c++的agent。
jvmtiEnv *jvmti = NULL;
jint ret = vm->GetEnv(reinterpret_cast<void **>(&jvmti), JVMTI_VERSION_1_2);
if (ret != JNI_OK) {
fprintf(stderr, "ERROR: Couldn't get JVMTI environment");
return JNI_ERR;
}
上来先是一段讨论操作,初始化jvmtienv。确定成功后才可以继续做一些操作。
jvmtiCapabilities caps;
std::memset(&caps, 0, sizeof(caps));
caps.can_generate_monitor_events = 1;
这里就是把我们关注的事件的开关打开,我们是关注锁信息,所以can_generate_monitor_events需要打开。我们接下来看看我们能关注那些事件。我们可以查看文档或者对应的jvmti的头文件都可以看到。
/* 73 : Monitor Wait */
jvmtiEventMonitorWait MonitorWait;
/* 74 : Monitor Waited */
jvmtiEventMonitorWaited MonitorWaited;
/* 75 : Monitor Contended Enter */
jvmtiEventMonitorContendedEnter MonitorContendedEnter;
/* 76 : Monitor Contended Entered */
jvmtiEventMonitorContendedEntered MonitorContendedEntered;
我们可以看到和锁相关的事件有4个。通过命名,我们可以猜到功能。
MonitorWait是锁对象触发wait操作。
MonitorWaited是锁对象成功wait。
MonitorContendedEnter是进入锁操作。
MonitorContendedEntered是功能进入锁操作。
这里其实就是对应两种状态,一直是开始做了,一个是成功做到了。我们这次选择MonitorContendedEnter操作或者MonitorContendedEntered都可以,他们都能反映锁的争抢情况,MonitorContendedEnter是执行到synchronized就会触发事件,MonitorContendedEntered则是终于拿到锁资源才触发的事件,我们想看多少在抢锁资源,所以我们选择MonitorContendedEnter事件,只要大家排队等待就可以。
我们获取的信息也很简单,只要线程id和name。为什么这么少呢,因为jni操作太麻烦了,下面贴出展示代码。
void JNICALL jvmti_EventMonitorContendedEnter(jvmtiEnv *jvmti_env,
JNIEnv *jni_env,
jthread thread,
jobject object) {
jclass threadClass = jni_env->GetObjectClass(thread);
jmethodID getId = jni_env->GetMethodID(threadClass, "getId", "()J");
jlong id = (jlong) jni_env->CallObjectMethod(thread, getId);
jmethodID getName = jni_env->GetMethodID(threadClass, "getName", "()Ljava/lang/String;");
const char *name = jni_env->GetStringUTFChars((jstring) jni_env->CallObjectMethod(thread, getName), NULL);
std::cout << id << name << std::endl;
}
为什么是这么一个参数呢,因为这是一个回调函数,我们需要把回调的传过去就好,这里用的是方法指针,我们只要是这个返回值,是这个参数,名字无所谓。
上面的操作纯粹的jni操作,他可以给我们线程对象,以及锁对象,只不过都需要通过JNI_ENV进行打印操作,调用java对象的方法,我这里只是输出一个id和name。
jvmtiEventCallbacks cb;
cb.MonitorContendedEnter = &jvmti_EventMonitorContendedEnter;
jvmti->SetEventCallbacks(&cb, sizeof(cb));
jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_MONITOR_CONTENDED_ENTER, NULL);
这里设置我们上面的方法指针给对应的事件方法。并且打开通知模式。然后编译成动态库,这里的操作以及头文件和jni的操作一模一样。就不重复介绍了。最终生成动态库。
测试程序
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
new Thread(()->{
main.hello();
}).start();
new Thread(()->{
main.hello();
}).start();
new Thread(()->{
main.hello();
}).start();
main.hello();
}
public synchronized void hello() {
System.out.println("++++++java-"+Thread.currentThread().getName());
System.out.println("hello");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面是一个简单的java多线程程序。就是锁直接加在了方法上,然后打印现在的线程名。
启动java进程带上agent,通过-agentpath:动态库地址。
然后我们会看到这样的输出
++++++java-Thread-0
hello
12Thread-1
1main
13Thread-2
++++++java-Thread-2
hello
++++++java-main
hello
++++++java-Thread-1
hello
num+name的组合就是我们agent的输出。我们可以看到先后进入等待的是线程Thread-1,main,Thread-2。执行的时候是java-Thread-2,main,Thread-1。正好是反过来的,这个也是说明synchronized确实不是公平锁。
共同学习,写下你的评论
评论加载中...
作者其他优质文章