Banyak otomasi perlu berjalan berkala: backup harian, rotasi log setiap jam, atau sinkronisasi data setiap menit. Java dan Kotlin menyediakan beberapa API scheduling dengan trade-off berbeda. Pada bab ini kita akan membandingkan tiga API inti: java.util.Timer untuk skenario sederhana, ScheduledExecutorService untuk paralelisme yang lebih baik, dan Quartz Scheduler untuk produksi yang memerlukan persistensi serta clustering. Bab diakhiri dengan ekspresi cron-like yang familier di sistem Unix.
Tiga API Scheduling #
Pilihan API scheduling bergantung pada kompleksitas kebutuhan. java.util.Timer adalah API tertua dan paling sederhana, namun punya kelemahan: semua tugas berbagi satu thread. Bila satu tugas lambat, tugas lain tertunda. ScheduledExecutorService memperbaiki ini dengan thread pool. Quartz Scheduler menambahkan persistensi ke database dan dukungan clustering untuk aplikasi multi-node.
| API | Sumber | Cocok untuk |
|---|---|---|
java.util.Timer | Java SE | Tugas sederhana, satu app, delay pendek |
ScheduledExecutorService | Java SE | Aplikasi menengah, paralelisme terbatas |
| Quartz Scheduler | Library eksternal | Produksi, persisten, clustering |
Spring @Scheduled | Spring Framework | Deklaratif lewat anotasi |
| WorkManager (Android) | Jetpack | Aplikasi Android, tugas background |
java.util.Timer #
Timer menjadwalkan TimerTask (class abstract dengan method run()) untuk dieksekusi setelah delay tertentu atau secara berkala. Konstruktor menerima nama thread dan flag isDaemon — daemon thread tidak menghentikan JVM exit.
val timer = java.util.Timer("demo", true)
val task = object : java.util.TimerTask() {
override fun run() {
println("Timer tick pada ${System.currentTimeMillis()}")
}
}
timer.scheduleAtFixedRate(task, 100L, 50L) // delay awal 100ms, period 50msMethod scheduleAtFixedRate menjaga period ideal. Method alternatif schedule memakai period relatif terhadap completion sebelumnya. Untuk tugas yang harus tetap on-time (misalnya heartbeat), scheduleAtFixedRate lebih tepat.
ScheduledExecutorService #
ScheduledExecutorService adalah bagian dari java.util.concurrent yang lebih modern. Method schedule(), scheduleAtFixedRate(), dan scheduleWithFixedDelay() mendukung berbagai pola. Thread pool bisa lebih dari satu, sehingga tugas lambat tidak menahan tugas lain.
val scheduler = Executors.newScheduledThreadPool(2)
val future = scheduler.schedule({
println("Tugas dijalankan")
}, 100L, TimeUnit.MILLISECONDS)
future.get() // tunggu selesai
scheduler.shutdown()Pemanggilan shutdown() penting agar thread pool tidak menggantungkan JVM. Tanpa shutdown, daemon false akan menyebabkan program tidak pernah exit.
Demo Scheduling #
Cuplikan berikut mendemokan ketiga API. Waktu dicetak dalam format beku agar output deterministik. Eksekusi sesungguhnya tetap memakai thread timer/scheduler yang asli:
// Demo scheduled jobs: java.util.Timer, ScheduledExecutorService,
// dan simulasi cron-like. Output deterministik dengan urutan tetap.
import java.time.format.DateTimeFormatter
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
// Format waktu tetap agar output deterministik
val timeFmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
fun now(): String = "2026-01-15 10:00" // waktu beku untuk determinisme
fun main() {
println("=== Scheduled Jobs ===")
println("Waktu sekarang (beku untuk demo): ${now()}")
println()
// 1. java.util.Timer: jadwalkan tugas delay 100ms, lalu fixed-rate
println("=== 1. java.util.Timer (delay 100ms, fixed-rate 50ms) ===")
val timer = java.util.Timer("demo-timer", true)
var timerHit = 0
val timerTask = object : java.util.TimerTask() {
override fun run() {
timerHit++
println(" Timer tick #${timerHit}")
if (timerHit >= 3) timer.cancel()
}
}
timer.scheduleAtFixedRate(timerTask, 100L, 50L)
Thread.sleep(350L) // tunggu tugas selesai
println(" Total tick Timer: $timerHit")
println()
// 2. ScheduledExecutorService: jadwalkan dengan delay berbeda
println("=== 2. ScheduledExecutorService (delay 100ms, 200ms, 300ms) ===")
val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(2)
val futures = (1..3).map { i ->
scheduler.schedule({
println(" Scheduler task #$i jalan pada ${now()}")
}, (i * 100).toLong(), TimeUnit.MILLISECONDS)
}
futures.forEach { it.get() }
scheduler.shutdown()
println(" Semua task scheduler selesai.")
println()
// 3. Cron-like: konversi ekspresi cron ke delay
println("=== 3. Cron-like Parser ===")
val cronExprs = listOf(
"0 * * * * ?" to "Setiap menit",
"0 0 * * * ?" to "Setiap jam (menit 0, detik 0)",
"0 0 9 * * MON-FRI" to "Jam 9 pagi, hari kerja",
"0 0 0 1 1 ?" to "Tengah malam 1 Januari"
)
cronExprs.forEach { (expr, desc) ->
val next = nextRun(expr)
println(" Cron '$expr' → $desc")
println(" Eksekusi berikutnya: $next")
}
println()
// 4. Perbandingan strategi
println("=== 4. Perbandingan Strategi Scheduling ===")
val strategi = listOf(
Triple("Timer", "Java SE", "Cocok untuk task sederhana di app kecil"),
Triple("ScheduledExecutorService", "Java SE", "Robust, mendukung pool thread"),
Triple("Quartz Scheduler", "Library eksternal", "Production-grade, persisten, clustering"),
Triple("Spring @Scheduled", "Spring Framework", "Deklaratif lewat anotasi")
)
strategi.forEach { (nama, sumber, cocok) ->
println(" $nama ($sumber)")
println(" Cocok: $cocok")
}
}
// Cron-like sederhana: hanya mendukung format 6-field Quartz.
// Mengembalikan string deskriptif (tidak menghitung waktu sebenarnya).
fun nextRun(expr: String): String {
val parts = expr.split(" ")
if (parts.size != 6) return "<format tidak dikenal>"
val sec = parts[0]
val min = parts[1]
val hour = parts[2]
val dom = parts[3]
val mon = parts[4]
val dow = parts[5]
return "detik=$sec menit=$min jam=$hour hari=$dom bulan=$mon hariMinggu=$dow"
}Output yang Dihasilkan:
=== Scheduled Jobs ===
Waktu sekarang (beku untuk demo): 2026-01-15 10:00
=== 1. java.util.Timer (delay 100ms, fixed-rate 50ms) ===
Timer tick #1
Timer tick #2
Timer tick #3
Total tick Timer: 3
=== 2. ScheduledExecutorService (delay 100ms, 200ms, 300ms) ===
Scheduler task #1 jalan pada 2026-01-15 10:00
Scheduler task #2 jalan pada 2026-01-15 10:00
Scheduler task #3 jalan pada 2026-01-15 10:00
Semua task scheduler selesai.
=== 3. Cron-like Parser ===
Cron '0 * * * * ?' → Setiap menit
Eksekusi berikutnya: detik=0 menit=* jam=* hari=* bulan=* hariMinggu=?
Cron '0 0 * * * ?' → Setiap jam (menit 0, detik 0)
Eksekusi berikutnya: detik=0 menit=0 jam=* hari=* bulan=* hariMinggu=?
Cron '0 0 9 * * MON-FRI' → Jam 9 pagi, hari kerja
Eksekusi berikutnya: detik=0 menit=0 jam=9 hari=* bulan=* hariMinggu=MON-FRI
Cron '0 0 0 1 1 ?' → Tengah malam 1 Januari
Eksekusi berikutnya: detik=0 menit=0 jam=0 hari=1 bulan=1 hariMinggu=?
=== 4. Perbandingan Strategi Scheduling ===
Timer (Java SE)
Cocok: Cocok untuk task sederhana di app kecil
ScheduledExecutorService (Java SE)
Cocok: Robust, mendukung pool thread
Quartz Scheduler (Library eksternal)
Cocok: Production-grade, persisten, clustering
Spring @Scheduled (Spring Framework)
Cocok: Deklaratif lewat anotasiMari kita bedah bagian-bagian kunci dari kode di atas:
java.util.Timer("demo-timer", true): parameter keduaisDaemon = truemembuat thread timer menjadi daemon, sehingga tidak menghalangi JVM exit. Tanpa ini, program bisa menggantung setelahmain()selesai.object : TimerTask(): anonymous class yang meng-overriderun(). Setiap tick menambah counter dan setelah 3 tick,timer.cancel()menghentikan scheduling. Tanpa cancel, timer akan berjalan selamanya.scheduleAtFixedRate(task, 100L, 50L): 100 ms delay awal sebelum tick pertama, lalu 50 ms period antar tick. MethodscheduleWithFixedDelayberbeda: period diukur dari akhir tick sebelumnya.scheduler.schedule({...}, i * 100L, TimeUnit.MILLISECONDS): setiap tugas dijadwalkan dengan delay berbeda (100ms, 200ms, 300ms).ScheduledFuture.get()memblokir hingga tugas selesai.scheduler.shutdown(): menutup thread pool setelah semua tugas selesai. Tanpa ini, scheduler thread (non-daemon) akan menjaga JVM tetap hidup.nextRun(expr): parser sederhana untuk ekspresi cron 6-field (detik menit jam hari bulan hariMinggu). Implementasi nyata memerlukan parsing*, rangeMON-FRI, step*/5, dan list1,3,5.
Ekspresi Cron #
Cron adalah standar de facto untuk ekspresi scheduling di sistem Unix. Format Quartz memakai 6 field (menambahkan detik), sementara cron standar Linux memakai 5 field. Karakter khusus mendukung berbagai pola:
| Karakter | Arti | Contoh |
|---|---|---|
* | Setiap nilai | * * * * * = setiap menit |
*/N | Setiap N unit | */5 * * * * = setiap 5 menit |
A-B | Rentang | MON-FRI = hari kerja |
A,B,C | List | 1,15 = tanggal 1 dan 15 |
? | Don’t care (hanya field hari) | ? pada hariMinggu |
Quartz Scheduler untuk Produksi #
Quartz Scheduler menambahkan tiga fitur yang tidak ada di ScheduledExecutorService: persistensi job ke database, clustering di multi-node, dan recovery job yang gagal. Untuk aplikasi production yang berjalan di banyak server, Quartz memastikan job tidak dieksekusi dobel.
val scheduler = StdSchedulerFactory.getDefaultScheduler()
scheduler.start()
val job = JobBuilder.newJob(BackupJob::class.java)
.withIdentity("backupJob", "group1")
.build()
val trigger = TriggerBuilder.newTrigger()
.withIdentity("backupTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 2 * * ?")) // jam 2 pagi
.build()
scheduler.scheduleJob(job, trigger)Job adalah class yang meng-implement execute(JobExecutionContext). Trigger menentukan kapan job dijalankan. Quartz menyimpan job di JDBC store sehingga setelah restart, job tetap terdaftar dan jadwal tidak hilang.
Referensi Lanjutan #
- Kotlin Command Line
— panduan resmi
kotlinCLI untuk pembuatan biner terjadwal viakotlinc -script
Selanjutnya #
Setelah tugas terjadwal berjalan, langkah berikutnya adalah mengemasnya menjadi distribusi yang mudah dipasang. Bab berikutnya membahas packaging CLI sebagai JAR mandiri atau native binary. Materi tersebut ada di chapter Packaging CLI Applications.