In JMeter, why can one ${__BeanShell(...)} call serialise every thread in the plan?
answer
- Compare the element rule with the function rule
- Ask what a function instance belongs to
- One keyword in the function's source explains it
- Occurrence, not element, is the unit
basics
~20 sEach occurrence of a function call is one Function instance shared by all threads, and JMeter's BeanShell function declares execute() synchronized. Every thread that reaches that field therefore queues on one monitor and one interpreter.
solid answer
~40 sJMeter's functions documentation states that functions are shared between threads and that each occurrence of a call is handled by a separate function instance. The code agrees: `FunctionProperty.clone()` copies the function reference rather than building a new one, so every thread's clone of the element points at the same object. `org.apache.jmeter.functions.BeanShell` creates its `BeanShellInterpreter` once in `setParameters()` and declares `execute()` **synchronized**, so the binding of `Sampler`, `SampleResult`, `ctx`, `vars`, `props` and `threadName` plus the whole interpretation happen under one lock. Adding threads lengthens the queue in front of that field instead of raising throughput through it. BeanShell **test elements** behave the opposite way: each element in each thread has its own interpreter and no shared monitor.
go deeper
Know that JMeter has a __BeanShell function as well as BeanShell elements, and that the two are not the same thing even when the script text is identical.
Explain the sharing rule: one function instance per occurrence of the call, shared across threads, versus one interpreter per element per thread for the test elements.
Diagnose it in a real plan. Recognise that raising the thread count past a synchronized shared function adds queueing rather than rate, and know which of the two shapes you are looking at.
Decide whether an inherited plan's scripted fields should be replaced by built-in functions, moved into elements, or removed, and weigh the review cost of touching many fields at once.
## Two different sharing rules in one family The BeanShell surface has an element side and a function side, and they do not share an ownership model. `bin/jmeter.properties` is explicit about the elements: *"Beanshell test elements do not share interpreters. Each element in each thread has its own interpreter."* JMeter's functions documentation is equally explicit about the other side: *"Functions are shared between threads. Each occurrence of a function call in a test plan is handled by a separate function instance."* The code backs both. A test element is cloned per thread and `clone()` builds a new interpreter. A function is not: `FunctionProperty.clone()` copies the reference (`prop.function = function`), so every thread's clone of the element points at the same `Function` object. ## Why one instance becomes one queue `org.apache.jmeter.functions.BeanShell` builds its interpreter once, inside `setParameters()`: - `setParameters(...)` is declared `synchronized` and does `new BeanShellInterpreter(JMeterUtils.getProperty("beanshell.function.init"), log)`. - `execute(SampleResult, Sampler)` is **also declared `synchronized`**. Because the instance is shared across threads, that `synchronized` keyword is a monitor every thread must acquire. Inside it the function binds `Sampler`, `SampleResult`, `ctx`, `vars`, `props` and `threadName`, evaluates the script, and optionally stores the result into a variable. All of that — the binding and the interpretation — happens with the lock held. The result is that a single `${__BeanShell(...)}` occurrence in a field on a hot request turns into a serialisation point. Raising the thread count does not raise the rate through that field; it lengthens the queue in front of it, and the queueing shows up as time the thread is not sampling. ## Occurrence, not element The unit that gets its own instance is the **occurrence of the call**, not the element containing it. Consequences worth being precise about: - The same `${__BeanShell(...)}` text written into two different fields is two occurrences, two instances, two interpreters and two independent monitors. - One occurrence inherited by a hundred threads is one interpreter and one monitor. - Copying a scripted element to "spread the load" therefore does help the function case, and does nothing for the element case, which was never shared to begin with. ## What the elements do instead A BeanShell PostProcessor under the same load has no shared monitor at all — every thread walks its own interpreter concurrently. It is still interpreting the script once per sample, so it is still a per-sample CPU cost, but it is a cost that scales with cores rather than a cost that refuses to. That difference is the practical reason a plan can look fine when its scripting lives in elements and collapse when the same logic is written as a function call in a shared field. ## What to do about it JMeter's best-practices page has advised, since 3.1, switching the `__BeanShell` function to the `__groovy` function and the BeanShell test elements to the JSR223 family; the compiled-script mechanism that makes that worthwhile belongs to the JSR223 elements, not here. Independently of any migration, two things are true of the plan in front of you: 1. A field that needs a value per request rarely needs a whole interpreter. Most inherited `${__BeanShell(...)}` calls are doing string work a built-in function already does. 2. If the script genuinely must run, moving it out of a field and into a scripted element removes the shared monitor even before you change languages.
- Does writing the same ${__BeanShell(...)} call into two fields halve the contention?Yes, in the sense that it creates two occurrences, and JMeter's documentation says each occurrence gets its own function instance — so two interpreters and two independent monitors. It is a poor fix, though: you have doubled the interpreters rather than removed the reason for the lock, and the two copies now drift apart under maintenance.
- Why does the same script cause no lock contention when it lives in a BeanShell PostProcessor?Because a test element is cloned per thread and its `clone()` builds a fresh `BeanShellInterpreter`, so each thread walks its own interpreter concurrently with no shared monitor. The per-sample interpretation cost is still there, but it scales across cores instead of serialising.
saying these in an interview costs you the question
- Assuming each thread gets its own __BeanShell interpreter
- Blaming the script's length rather than the shared lock
- Expecting more threads to raise throughput through a synchronized function
- Treating the element rule and the function rule as the same rule
- Concluding the whole plan is locked when only that occurrence is