网站首页 > 精选教程 正文
悲观锁和乐观锁都是并发编程中常用的锁机制,用于处理多线程对共享资源的访问。
悲观锁:认为并发访问会导致冲突,因此在访问共享资源前,悲观地认为其他线程可能会修改该资源,因此采取加锁的方式,保证当前线程独占该资源的访问权,其他线程必须等待当前线程释放锁后才能访问该资源。例如,Java中的synchronized关键字就是一种悲观锁机制。
乐观锁:相反,乐观锁认为并发访问不会导致冲突,因此不加锁,而是在访问共享资源时,先获取该资源的版本号或者时间戳等标识,然后进行操作,在更新该资源时,比较当前版本号或时间戳是否与之前获取的相同,如果相同,则说明该资源没有被其他线程修改过,可以进行更新,否则说明该资源已经被其他线程修改,操作失败,需要重试。例如,Java中的CAS(Compare and Swap)操作就是一种乐观锁机制。
悲观锁和乐观锁都有各自的优缺点。悲观锁虽然保证了数据的安全性,但是需要频繁地加锁和释放锁,会带来较大的性能开销,尤其在高并发的情况下会造成资源的浪费。乐观锁虽然避免了加锁和释放锁的开销,但是需要进行额外的版本号或时间戳的比较和更新操作,如果并发冲突较多,重试次数会增多,影响性能。因此,在实际开发中需要根据具体情况选择合适的锁机制。
乐观锁实现方式
乐观锁通常使用版本号(Version Number)或时间戳(Timestamp)等方式来实现。
使用版本号实现乐观锁的步骤如下:
- 在需要并发访问的数据结构中,增加一个版本号字段,初始值为1。
- 当某个线程需要读取该数据时,先读取版本号并保存到本地。
- 当该线程需要对数据进行更新时,先对本地保存的数据进行修改,并将版本号加1。
- 当该线程尝试将更新后的数据提交回数据结构时,需要比较本地保存的版本号和当前数据结构中的版本号是否相同。如果相同,则表示该数据没有被其他线程修改,可以更新数据结构中的数据和版本号;否则,需要重试更新操作。
使用时间戳实现乐观锁的步骤如下:
- 在需要并发访问的数据结构中,增加一个时间戳字段,记录最后一次修改该数据的时间。
- 当某个线程需要读取该数据时,先读取时间戳并保存到本地。
- 当该线程需要对数据进行更新时,先对本地保存的数据进行修改,并将时间戳更新为当前时间。
- 当该线程尝试将更新后的数据提交回数据结构时,需要比较本地保存的时间戳和当前数据结构中的时间戳是否相同。如果相同,则表示该数据没有被其他线程修改,可以更新数据结构中的数据和时间戳;否则,需要重试更新操作。
乐观锁的实现方式相对简单,但需要考虑到并发冲突的情况,需要设计合适的重试机制,避免出现死锁或者数据不一致的情况。
Java 锁的分类
Java中锁的分类可以根据不同的维度进行划分,下面是一些常见的分类方式:
- 按照锁的粒度分类:
- 细粒度锁:例如synchronized关键字,锁的粒度较小,可以用于保护单个对象的状态。
- 粗粒度锁:例如ReentrantLock等,锁的粒度较大,可以用于保护多个对象的状态。
- 按照锁的可重入性分类:
- 可重入锁:例如ReentrantLock等,同一个线程可以多次获得该锁。
- 不可重入锁:例如ReadWriteLock中的读锁,同一个线程不能多次获得该锁。
- 按照锁的公平性分类:
- 公平锁:按照线程请求锁的顺序来获取锁,避免线程饥饿现象。
- 非公平锁:不考虑线程请求锁的顺序,有可能会导致某些线程一直无法获得锁。
- 按照锁的实现方式分类:
- 内置锁:例如synchronized关键字,由Java虚拟机实现,通常与对象关联。
- 显式锁:例如ReentrantLock等,需要程序员手动创建和管理,提供了更加灵活的控制方式。
- 按照锁的功能分类:
- 独占锁:只允许一个线程获得锁,其他线程需要等待。
- 共享锁:允许多个线程同时获得锁,例如ReadWriteLock中的读锁就是共享锁。
不同类型的锁各有优缺点,需要根据具体场景进行选择。例如,对于访问单个对象的情况,可以使用内置锁;对于需要细粒度控制的情况,可以使用ReentrantLock等显式锁;对于需要高并发读写的情况,可以使用ReadWriteLock等读写锁。
公平锁与非公平锁之间的区别
公平锁和非公平锁是Java中锁的两种不同实现方式,它们的主要区别在于线程获取锁的顺序不同:
- 公平锁:按照线程请求锁的顺序来获取锁,即等待时间最长的线程将最先获得锁。当锁释放后,等待时间最长的线程会优先获取锁,避免了某些线程长时间等待的问题,确保了线程的公平性。
- 非公平锁:不考虑线程请求锁的顺序,有可能会导致某些线程一直无法获得锁。当锁释放后,不一定是等待时间最长的线程获得锁,而是竞争最激烈的线程优先获得锁。这种方式可以提高锁的吞吐量,减少线程的上下文切换次数,但是可能会导致某些线程长时间等待。
公平锁和非公平锁的选择取决于具体的应用场景。对于对公平性要求比较高的场景,例如资源分配、任务调度等,应该优先选择公平锁,以避免某些线程一直无法获得锁,造成线程饥饿现象。而对于需要高并发的场景,例如缓存、连接池等,为了提高锁的吞吐量,可以使用非公平锁,以减少线程的上下文切换次数。
需要注意的是,公平锁在竞争激烈的情况下可能会导致线程间的上下文切换过多,降低系统性能。而非公平锁在高并发情况下可能会导致某些线程一直无法获得锁,造成线程饥饿现象。因此,在选择锁的类型时,需要根据具体情况进行综合考虑,权衡各种因素。
CAS(Compare and Swap)
CAS(Compare and Swap)是一种乐观锁技术,用于实现多线程之间的同步操作。它利用CPU底层提供的原子指令来实现无锁操作,从而避免了使用传统锁机制所带来的性能问题。
CAS操作通常包括三个参数:内存位置、期望的值和新的值。当执行CAS操作时,只有当内存位置的值与期望的值相同时,才会将新的值写入内存,并返回true,否则不会进行任何操作,并返回false。
CAS操作的基本流程如下:
- 将内存位置的值与期望的值进行比较。
- 如果相等,则将新的值写入内存位置。
- 如果不相等,则不做任何操作。
在多线程环境中,CAS操作可以用来解决并发访问共享数据时的同步问题。如果多个线程同时尝试更新同一个共享数据,则只有一个线程会成功执行CAS操作,并更新共享数据。其他线程由于CAS操作失败,需要重新获取共享数据并再次尝试执行CAS操作。
使用CAS操作的优点是可以避免传统锁机制所带来的性能问题,因为它不会导致线程的阻塞和切换。另外,CAS操作通常可以实现无锁或轻量级锁的效果,从而提高并发性能。
然而,CAS操作也存在一些缺点。首先,由于CAS操作是基于底层硬件提供的原子指令实现的,因此在高并发场景下,多个线程同时尝试执行CAS操作可能会导致竞争激烈,从而出现CAS自旋等待的情况,这会浪费一些CPU资源。其次,CAS操作只能保证单个变量的原子性,不能保证多个变量之间的原子性。
在Java中,CAS操作通常通过sun.misc.Unsafe类来实现。同时,Java也提供了一些基于CAS操作的原子类,如AtomicInteger、AtomicLong、AtomicBoolean等,这些类都提供了一些常用的原子操作方法,如getAndIncrement()、compareAndSet()等,方便我们进行并发编程。
AQS(AbstractQueuedSynchronizer)
AQS(AbstractQueuedSynchronizer)是Java中用于实现锁和同步器的基础框架,其核心思想是使用一个FIFO的双向链表来实现线程的阻塞和唤醒。AQS提供了一些基本的同步操作方法,如获取锁、释放锁、阻塞线程、唤醒线程等,同时也允许用户自定义同步器,并实现自己的同步操作方法。
AQS的设计非常巧妙,其基本思路是将线程的阻塞和唤醒操作交给同步器来管理,而同步器通过维护一个双向链表来管理等待线程,从而实现线程的阻塞和唤醒。具体来说,AQS通过内部类Node来表示等待线程,每个Node包含一个等待状态(waitStatus)和一个指向前一个和后一个Node的引用(prev和next),同时还有一个Thread类型的字段表示持有该Node的线程。当一个线程请求获取锁时,如果锁已经被占用,则该线程会被封装成一个Node并加入等待队列中,并在后续的自旋过程中不断尝试获取锁。当锁的持有者释放锁时,会唤醒等待队列中的一个线程,并将其从等待队列中移除。
AQS提供了两种同步模式:独占模式和共享模式。在独占模式下,同一时刻只能有一个线程持有锁,其他线程必须等待锁的释放。而在共享模式下,多个线程可以同时获取锁,并进行并发访问。
AQS的基本操作方法包括:
- tryAcquire(int arg):尝试获取锁,如果获取成功则返回true,否则返回false。
- tryRelease(int arg):释放锁,如果释放成功则返回true,否则返回false。
- tryAcquireShared(int arg):尝试获取共享锁,如果获取成功则返回一个大于等于0的整数,否则返回一个小于0的整数。
- tryReleaseShared(int arg):释放共享锁,如果释放成功则返回true,否则返回false。
- isHeldExclusively():判断当前线程是否持有独占锁。
AQS的实现非常复杂,涉及到许多底层细节,因此不适合直接使用。但是,Java提供了一些基于AQS的同步类,如ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等,这些类都使用了AQS提供的同步基础框架,可以直接使用,并提供了一些常用的同步操作方法,方便我们进行并发编程。
ReentrantLock
ReentrantLock是Java中的一种独占锁,它是一种可重入锁,也就是同一个线程可以多次获取锁而不会死锁,这种锁可以有效地防止资源的竞争和数据的不一致。
ReentrantLock实现了Lock接口,提供了一些与锁相关的操作方法,包括:
- lock():获取锁,如果锁已经被占用,则当前线程会阻塞,直到获取到锁。
- tryLock():尝试获取锁,如果锁未被占用,则立即获取锁并返回true,否则立即返回false。
- tryLock(long timeout, TimeUnit unit):尝试获取锁,如果在指定的时间内(以指定的时间单位为准)未能获取到锁,则返回false。
- unlock():释放锁,如果当前线程持有锁,则释放锁。
ReentrantLock的实现原理比较复杂,它底层基于AQS(AbstractQueuedSynchronizer)实现了锁的语义,具体来说,它使用一个state变量来表示锁的状态,当state为0时表示锁未被占用,当state为1时表示锁已被占用。当一个线程请求获取锁时,如果锁未被占用,则该线程可以立即获取锁,并将state设置为1;如果锁已经被占用,则该线程会被封装成一个Node并加入等待队列中,并在后续的自旋过程中不断尝试获取锁。当锁的持有者释放锁时,会唤醒等待队列中的一个线程,并将其从等待队列中移除。
ReentrantLock相较于synchronized关键字具有一些优点,包括:
- 可中断性:如果一个线程在等待获取锁的过程中,可以被中断,则该线程将会从等待队列中移除,并抛出InterruptedException异常。
- 公平性:ReentrantLock可以通过构造函数指定锁的公平性,如果指定为公平锁,则锁将按照FIFO的顺序进行获取;如果指定为非公平锁,则锁将根据CPU的调度而获取,可能会存在饥饿线程。
- 条件变量:ReentrantLock提供了Condition接口来实现条件变量,可以让线程在特定条件下等待和唤醒。
总的来说,ReentrantLock是一种高级的同步机制,可以有效地控制并发访问,并提供了比synchronized关键字更多的特性和灵活性。但是,由于它的实现较为复杂,使用时需要谨慎考虑,避免出现死锁等问题。
ReentrantLock是Java中一个可重入锁的实现,它允许线程获取已经持有的锁,同时还能保证锁的互斥性和可见性。下面是一个简单的示例,说明如何使用ReentrantLock。
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private ReentrantLock lock = new ReentrantLock();
public void foo() {
lock.lock();
try {
// 线程安全的代码
} finally {
lock.unlock();
}
}
}
ReentrantLock是Java中一个可重入锁的实现,它允许线程获取已经持有的锁,同时还能保证锁的互斥性和可见性。下面是一个简单的示例,说明如何使用ReentrantLock。
javaCopy codeimport java.util.concurrent.locks.ReentrantLock;
public class Example {
private ReentrantLock lock = new ReentrantLock();
public void foo() {
lock.lock();
try {
// 线程安全的代码
} finally {
lock.unlock();
}
}
}
在这个示例中,我们首先创建了一个ReentrantLock对象,然后在foo方法中使用lock方法获取锁,执行线程安全的代码,最后使用unlock方法释放锁。请注意,我们使用了try-finally语句块来确保在任何情况下都会释放锁。
ReentrantLock还提供了其他方法,如tryLock、tryLock(long timeout, TimeUnit unit)等,它们允许我们在尝试获取锁时设置超时时间。此外,它还提供了一些高级功能,如公平锁、条件变量等,可以根据需要使用。
总的来说,ReentrantLock是Java中一个非常强大的锁实现,可以满足大部分线程同步的需求。
手写实现 ReentrantLock
ReentrantLock是Java中的可重入锁实现类,可以用于替代synchronized实现线程同步。以下是ReentrantLock的简单手写实现,仅供参考。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyReentrantLock implements Lock {
private boolean isLocked = false;
private Thread lockedBy = null;
private int lockCount = 0;
private final Object lock = new Object();
@Override
public void lock() {
synchronized (lock) {
Thread currentThread = Thread.currentThread();
while (isLocked && lockedBy != currentThread) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
lockedBy = currentThread;
lockCount++;
}
}
@Override
public void unlock() {
synchronized (lock) {
if (Thread.currentThread() != lockedBy) {
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
lockCount--;
if (lockCount == 0) {
isLocked = false;
lockedBy = null;
lock.notifyAll();
}
}
}
//以下方法暂不实现
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("Not supported yet.");
}
}
需要注意的是,上述代码是一个简单的可重入锁的实现,可能存在性能和安全方面的问题,仅适用于学习和理解ReentrantLock的原理。在实际应用中,应该使用Java中提供的ReentrantLock实现类。
Semaphore
Semaphore是一种计数信号量,它可以用来控制同时访问某个资源的线程数,或者限制流量的大小。
Semaphore主要有两个操作:acquire()和release()。acquire()操作会尝试获取一个许可证,如果当前有许可证可用,则获取成功并立即返回;如果没有许可证可用,则当前线程会被阻塞,直到有许可证可用。release()操作会释放一个许可证,并通知等待的线程有许可证可用。
Semaphore可以被用于解决多线程并发问题,例如限制并发访问某个资源的线程数。下面是一个简单的例子,假设有一个容器类Container,其中有一个put()方法用于往容器中添加元素,一个get()方法用于从容器中获取元素,但是容器中最多只能存放5个元素,当容器中元素个数超过5个时,新添加的元素需要等待容器中的元素被消费才能继续添加。
import java.util.concurrent.Semaphore;
public class Container {
private final Semaphore mutex = new Semaphore(1);
private final Semaphore full = new Semaphore(0);
private final Semaphore empty = new Semaphore(5);
private final Object[] elements = new Object[5];
private int putIndex = 0, getIndex = 0, count = 0;
public void put(Object element) throws InterruptedException {
empty.acquire();
mutex.acquire();
try {
elements[putIndex] = element;
putIndex = (putIndex + 1) % 5;
count++;
} finally {
mutex.release();
full.release();
}
}
public Object get() throws InterruptedException {
full.acquire();
mutex.acquire();
try {
Object element = elements[getIndex];
elements[getIndex] = null;
getIndex = (getIndex + 1) % 5;
count--;
return element;
} finally {
mutex.release();
empty.release();
}
}
}
在上面的例子中,容器类中维护了3个Semaphore对象:mutex用于保证对容器的操作互斥进行,full用于表示容器中元素个数不为0,empty用于表示容器中还可以添加的元素个数
CountDownLatch
CountDownLatch是Java并发包中的一个类,它可以用于实现线程间的协作和同步。它可以让一个或多个线程等待其他线程完成某些操作,然后再继续执行。下面对CountDownLatch进行深入讲解。
构造方法
CountDownLatch有一个构造方法,需要传入一个int类型的参数count,表示需要等待的操作数量。当CountDownLatch的计数器变为0时,等待该CountDownLatch的线程才会被唤醒。示例代码如下:
CountDownLatch latch = new CountDownLatch(3);
上面的代码表示需要等待3个操作完成后,CountDownLatch的计数器才会变为0。
方法介绍
CountDownLatch提供了如下几个方法:
- void countDown():将CountDownLatch的计数器减1。
- void await():等待计数器变为0,如果当前计数器为0,则该方法立即返回。
- boolean await(long timeout, TimeUnit unit):等待计数器变为0,如果超过指定时间,则该方法会返回false,否则返回true。
- long getCount():获取当前计数器的值。
使用示例
下面是一个使用CountDownLatch的示例。该示例中有三个线程,它们都需要执行某些操作后才能继续执行。我们可以使用CountDownLatch来实现这个需求。
import java.util.concurrent.CountDownLatch;
public class Example {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 is working...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is done.");
latch.countDown();
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 is working...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 is done.");
latch.countDown();
});
Thread thread3 = new Thread(() -> {
System.out.println("Thread 3 is working...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 3 is done.");
latch.countDown();
});
thread1.start();
thread2.start();
thread3.start();
latch.await();
System.out.println("All threads are done.");
}
}
在上面的示例中,我们创建了一个CountDownLatch对象,计数器的值为3。然后创建了三个线程,每个线程都需要执行一些操作后才能调用CountDownLatch的countDown方法,将计数器减1。最后,主线程调用CountDownLatch的await方法等待计数器变为0,然后输出"All threads are done."。
运行上面的示例,可以看到如下输出:
Thread 1 is working...
Thread 3 is working...
Thread 2 is working...
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads
CyclicBarrier
CyclicBarrier是Java并发包中的一个类,用于实现多个线程之间的同步。CyclicBarrier可以让多个线程在一个集合点处进行等待,当所有线程都到达集合点后,再一起继续执行。下面对CyclicBarrier进行深入讲解。
构造方法
CyclicBarrier有两个构造方法,分别是:
public CyclicBarrier(int parties, Runnable barrierAction);
public CyclicBarrier(int parties);
第一个构造方法需要传入一个int类型的参数parties,表示需要等待的线程数量,以及一个Runnable类型的barrierAction,表示所有线程到达集合点后需要执行的操作。第二个构造方法只需要传入一个int类型的参数parties,表示需要等待的线程数量。
方法介绍
CyclicBarrier提供了如下几个方法:
- int await():当前线程到达集合点,等待其他线程到达,当所有线程都到达集合点时,返回一个唯一的整数,表示当前线程是第几个到达集合点的线程。
- int await(long timeout, TimeUnit unit):等待指定时间后,如果当前线程还没有到达集合点,则会抛出TimeoutException异常。
- int getParties():获取需要等待的线程数量。
- int getNumberWaiting():获取已经到达集合点但还在等待的线程数量。
- boolean isBroken():获取所有线程是否都已经到达集合点,如果有任意一个线程抛出异常,则返回true。
使用示例
下面是一个使用CyclicBarrier的示例。该示例中有三个线程,每个线程都需要执行一些操作后才能调用CyclicBarrier的await方法,等待其他线程到达集合点。当所有线程都到达集合点后,输出"All threads are done."。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class Example {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads are arrived.");
});
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 is working...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 is done.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 is working...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 is done.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread3 = new Thread(() -> {
System.out.println("Thread 3 is working...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 3 is done.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
thread3.start();
}
}
运行上述示例,输出结果如下:
Thread 1 is working...
Thread 2 is working...
Thread 3 is working...
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads are arrived.
可以看到,三个线程都到达了集合点,才会执行CyclicBarrier的barrierAction,并输出"All threads are arrived."。
另外,需要注意的是,CyclicBarrier可以被重用,即在所有线程到达集合点后,可以继续使用CyclicBarrier进行同步等待。但是,需要保证所有线程已经执行完当前任务,否则可能会出现死锁情况。
- 上一篇: java中的锁及优化机制
- 下一篇: 详细介绍一下Java中的读写锁?
猜你喜欢
- 2024-11-21 Java中的重重“锁”事
- 2024-11-21 线程进阶:多任务处理——Java中的锁(Unsafe基础)
- 2024-11-21 深入理解MySQL锁机制原理
- 2024-11-21 Java并发锁的原理,你所不知道的Java“锁”事
- 2024-11-21 阿里二面:你知道Java中的同步与锁机制详解?
- 2024-11-21 知识点深度解读系列-JAVA锁
- 2024-11-21 图解Java中的锁:什么是死锁?怎么排查死锁?怎么避免死锁?
- 2024-11-21 Java锁与线程的那些“不可描述”的事儿
- 2024-11-21 让人闻风丧胆的 Mysql 锁机制
- 2024-11-21 Java中各种锁的理解
你 发表评论:
欢迎- 04-11Java面试“字符串三兄弟”String、StringBuilder、StringBuffer
- 04-11Java中你知道几种从字符串中找指定的字符的数量
- 04-11探秘Java面试中问的最多的String、StringBuffer、StringBuilder
- 04-11Python字符串详解与示例(python字符串的常见操作)
- 04-11java正则-取出指定字符串之间的内容
- 04-11String s1 = new String("abc");这句话创建了几个字符串对象?
- 04-11java判断字符串中是否包含某个字符
- 04-11关于java开发中正确的发牌逻辑编写规范
- 最近发表
-
- Java面试“字符串三兄弟”String、StringBuilder、StringBuffer
- Java中你知道几种从字符串中找指定的字符的数量
- 探秘Java面试中问的最多的String、StringBuffer、StringBuilder
- Python字符串详解与示例(python字符串的常见操作)
- java正则-取出指定字符串之间的内容
- String s1 = new String("abc");这句话创建了几个字符串对象?
- java判断字符串中是否包含某个字符
- 关于java开发中正确的发牌逻辑编写规范
- windows、linux如何后台运行jar(并且显示进程名)
- 腾讯大佬私人收藏,GitHub上最受欢迎的100个JAVA库,值得学习
- 标签列表
-
- nginx反向代理 (57)
- nginx日志 (56)
- nginx限制ip访问 (62)
- mac安装nginx (55)
- java和mysql (59)
- java中final (62)
- win10安装java (72)
- java启动参数 (64)
- java链表反转 (64)
- 字符串反转java (72)
- java逻辑运算符 (59)
- java 请求url (65)
- java信号量 (57)
- java定义枚举 (59)
- java字符串压缩 (56)
- java中的反射 (59)
- java 三维数组 (55)
- java插入排序 (68)
- java线程的状态 (62)
- java异步调用 (55)
- java中的异常处理 (62)
- java锁机制 (54)
- java静态内部类 (55)
- java怎么添加图片 (60)
- java 权限框架 (55)
本文暂时没有评论,来添加一个吧(●'◡'●)