原创文章,转载请勿必将下边这段话放在文章开头处(保留超链接)。
本文转发自技术世界,原文链接
sleep和wait究竟哪些区别
虽然这个问题应当如此问——sleep和wait有哪些相同点。由于这两个方式不仅都能让当前线程暂停执行完,几乎没有其它相同点。
wait方式是Object类的方式,这意味着所有的Java类都可以调用该技巧。sleep方式是Thread类的静态方式。
wait是在当前线程持有wait对象锁的情况下,暂时舍弃锁,并让出CPU资源,并积极等待其它线程调用同一对象的notify或则notifyAll技巧。注意,虽然只有一个线程在等待,但是有其它线程调用了notify或则notifyAll方式,等待的线程只是被激活,而且它必须得再度获得锁能够继续往下执行。换言之,虽然notify被调用,但只要锁没有被释放,原等待线程由于未获得锁一直未能继续执行。测试代码如下所示
import java.util.Date;
public class Wait {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (Wait.class) {
try {
System.out.println(new Date() + " Thread1 is running");
Wait.class.wait();
System.out.println(new Date() + " Thread1 ended");
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
synchronized (Wait.class) {
try {
System.out.println(new Date() + " Thread2 is running");
Wait.class.notify();
// Don't use sleep method to avoid confusing
for(long i = 0; i < 200000; i++) {
for(long j = 0; j < 100000; j++) {}
}
System.out.println(new Date() + " Thread2 release lock");
} catch (Exception ex) {
ex.printStackTrace();
}
}
for(long i = 0; i < 200000; i++) {
for(long j = 0; j < 100000; j++) {}
}
System.out.println(new Date() + " Thread2 ended");
});
// Don't use sleep method to avoid confusing
for(long i = 0; i < 200000; i++) {
for(long j = 0; j < 100000; j++) {}
}
thread2.start();
}
}
复制
执行结果如下
Tue Jun 14 22:51:11 CST 2016 Thread1 is running
Tue Jun 14 22:51:23 CST 2016 Thread2 is running
Tue Jun 14 22:51:36 CST 2016 Thread2 release lock
Tue Jun 14 22:51:36 CST 2016 Thread1 ended
Tue Jun 14 22:51:49 CST 2016 Thread2 ended
复制
从运行结果可以看出
注意:wait方式须要释放锁,前提条件是它早已持有锁。所以wait和notify(或则notifyAll)方式都必须被包裹在synchronized句子块中,但是synchronized后锁的对象应当与调用wait方式的对象一样。否则抛出IllegalMonitorStateException
sleep方式告诉操作系统起码指定时间内不需为线程调度器为该线程分配执行时间片,并不释放锁(假如当前早已持有锁)。实际上,调用sleep方式时并不要求持有任何锁。
package com.test.thread;
import java.util.Date;
public class Sleep {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (Sleep.class) {
try {
System.out.println(new Date() + " Thread1 is running");
Thread.sleep(2000);
System.out.println(new Date() + " Thread1 ended");
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
synchronized (Sleep.class) {
try {
System.out.println(new Date() + " Thread2 is running");
Thread.sleep(2000);
System.out.println(new Date() + " Thread2 ended");
} catch (Exception ex) {
ex.printStackTrace();
}
}
for(long i = 0; i < 200000; i++) {
for(long j = 0; j < 100000; j++) {}
}
});
// Don't use sleep method to avoid confusing
for(long i = 0; i < 200000; i++) {
for(long j = 0; j < 100000; j++) {}
}
thread2.start();
}
}
复制
执行结果如下
Thu Jun 16 19:46:06 CST 2016 Thread1 is running
Thu Jun 16 19:46:08 CST 2016 Thread1 ended
Thu Jun 16 19:46:13 CST 2016 Thread2 is running
Thu Jun 16 19:46:15 CST 2016 Thread2 ended
复制
因为thread1和thread2的run方式实现都在同步块中,无论那个线程先领到锁,执行sleep时并不释放锁,因而其它线程未能执行。直至上面的线程sleep结束并退出同步块(释放锁),另一个线程才得到锁并执行。
注意:sleep方式并不须要持有任何方式的锁,也就不须要包裹在synchronized中。
本文所有示例均基于JavaHotSpot(TM)64-BitServerVM
调用sleep方式的线程,在jstack中显示的状态为sleeping。
java.lang.Thread.State: TIMED_WAITING (sleeping)
复制
调用wait方式的线程,在jstack中显示的状态为onobjectmonitor
java.lang.Thread.State: WAITING (on object monitor)
复制
synchronized几种用法
每位Java对象都可以用做一个实现同步的互斥锁,这种锁被称为外置锁。线程步入同步代码块或方式时手动获得外置锁,退出同步代码块或方式时手动释放该外置锁。步入同步代码块或则同步方式是获得外置锁的惟一途径。
实例同步方式
synchronized用于修饰实例方式(非静态方式)时,执行该方式须要获得的是该类实例对象的外置锁(同一个类的不同实例拥有不同的外置锁)。假如多个实例方式都被synchronized修饰,则当多个线程调用同一实例的不同同步方式(或则同一方式)时,须要竞争锁。但当调用的是不同实例的方式时,并不须要竞争锁。
静态同步方式
synchronized用于修饰静态方式时,执行该方式须要获得的是该类的class对象的外置锁(一个类只有惟一一个class对象)。调用同一个类的不同静态同步方式时会形成锁竞争。
同步代码块
synchronized用于修饰代码块时,步入同步代码块须要获得synchronized关键字前面括弧内的对象(可以是实例对象也可以是class对象)的外置锁。
synchronized使用总结
锁的使用是为了操作临界资源的正确性,而常常一个方式中并非所有的代码都操作临界资源。换句话说,技巧中的代码常常并不都须要同步。此时建议不使用同步方式,而使用同步代码块,只对操作临界资源的代码,也即须要同步的代码加锁。这样做的益处是,当一个线程在执行同步代码块时,其它线程一直可以执行该方式内同步代码块以外的部份,充分发挥多线程并发的优势,因而相较于同步整个方式而言提高性能。
释放Java外置锁的惟一方法是synchronized方式或则代码块执行结束。若某一线程在synchronized方式或代码块内发生死锁,则对应的外置锁未能释放,其它线程也难以获取该外置锁(即步入跟该外置锁相关的synchronized方式或则代码块)。
使用jstackdump线程栈时,可查看到相关线程通过synchronized获取到或等待的对象,但Lockedownablesynchronizers一直显示为None。下例中,线程thead-test-b已获取到类型为java.lang.Double的对象的外置锁(monitor),且该对象的显存地址为0x000000076ab95cb8
"thread-test-b" #11 prio=5 os_prio=31 tid=0x00007fab0190b800 nid=0x5903 runnable [0x0000700010249000]
java.lang.Thread.State: RUNNABLE
at com.jasongj.demo.TestJstack.lambda$1(TestJstack.java:27)
- locked <0x000000076ab95cb8> (a java.lang.Double)
at com.jasongj.demo.TestJstack$$Lambda$2/1406718218.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
复制
Java中的锁重入锁
Java中的重入锁(即ReentrantLock)与Java外置锁一样,是一种排它锁。使用synchronized的地方一定可以用ReentrantLock取代。
重入锁须要显示恳求获取锁,并显示释放锁。为了防止获得锁后,没有释放锁能让线程停止执行的有,而导致其它线程未能获得锁而导致死锁,通常建议将释放锁操作置于finally块里,如下所示。
try{
renentrantLock.lock();
// 用户操作
} finally {
renentrantLock.unlock();
}
复制
假如重入锁早已被其它线程持有,则当前线程的lock操作会被阻塞。不仅lock()方式之外,重入锁(或则说锁插口)还提供了其它获取锁的方式以实现不同的疗效。
重入锁可定义为公正锁或非公正锁,默认实现为非公正锁。
使用jstackdump线程栈时,可查看到获取到或正在等待的锁对象,获取到该锁的线程会在Lockedownablesynchronizers处显示该锁的对象类型及显存地址。在下例中,从Lockedownablesynchronizers部份可看见,线程thread-test-e获取到公正重入锁,且该锁对象的显存地址为0x000000076ae3d708
"thread-test-e" #17 prio=5 os_prio=31 tid=0x00007fefaa0b6800 nid=0x6403 runnable [0x0000700002939000]
java.lang.Thread.State: RUNNABLE
at com.jasongj.demo.TestJstack.lambda$4(TestJstack.java:64)
at com.jasongj.demo.TestJstack$$Lambda$5/466002798.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- <0x000000076af86810> (a java.util.concurrent.locks.ReentrantLock$FairSync)
复制
而线程thread-test-f因为未获取到锁,而处于WAITING(parking)状态,且它等待的锁正是上文线程thread-test-e获取的锁(显存地址0xx000000076000000076af86810)
"thread-test-f" #18 prio=5 os_prio=31 tid=0x00007fefaa9b2800 nid=0x6603 waiting on condition [0x0000700002a3c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076af86810> (a java.util.concurrent.locks.ReentrantLock$FairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:224)
at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
at com.jasongj.demo.TestJstack.lambda$5(TestJstack.java:69)
at com.jasongj.demo.TestJstack$$Lambda$6/33524623.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
复制
读写锁
如上文《Java进阶(二)当我们说线程安全时,究竟在说哪些》所述,锁可以保证原子性和可见性。而原子性更多是针对写操作而言。对于读多写少的场景,一个读操作无须阻塞其它读操作,只须要保证读和写或则写与写不同时发生即可。此时,假如使用重入锁(即排它锁),对性能影响较大。Java中的读写锁(ReadWriteLock)就是为这些读多写少的场景而创造的。
实际上,ReadWriteLock插口并非承继自Lock插口,ReentrantReadWriteLock也只实现了ReadWriteLock插口而未实现Lock插口。ReadLock和WriteLock,是ReentrantReadWriteLock类的静态内部类,它们实现了Lock插口。
一个ReentrantReadWriteLock实例包含一个ReentrantReadWriteLock.ReadLock实例和一个ReentrantReadWriteLock.WriteLock实例。通过readLock()和writeLock()方式可分别获得读锁实例和写锁实例,并通过Lock插口提供的获取锁方式获得对应的锁。
读写锁的锁定规则如下:
package com.test.thread;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println(new Date() + "\tThread 1 started with read lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println(new Date() + "\tThread 1 ended");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println(new Date() + "\tThread 2 started with read lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println(new Date() + "\tThread 2 ended");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
Lock lock = readWriteLock.writeLock();
lock.lock();
try {
System.out.println(new Date() + "\tThread 3 started with write lock");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(new Date() + "\tThread 3 ended");
} finally {
lock.unlock();
}
}).start();
}
}
复制
执行结果如下
Sat Jun 18 21:33:46 CST 2016 Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016 Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016 Thread 2 ended
Sat Jun 18 21:33:48 CST 2016 Thread 1 ended
Sat Jun 18 21:33:48 CST 2016 Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016 Thread 3 ended
复制
从前面的执行结果可见,thread1和thread2都只需获得读锁,因而它们可以并行执行。而thread3由于须要获取写锁,必须等到thread1和thread2释放锁后才会获得锁。
条件锁
条件锁只是一个帮助用户理解的概念,实际上并没有条件锁这些锁。对于每位重入锁,都可以通过newCondition()方式绑定若干个条件对象。
条件对象提供以下方式以实现不同的等待语义
调用条件等待的注意事项
signal()与signalAll()
package com.test.thread;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(new Date() + "\tThread 1 is waiting");
try {
long waitTime = condition.awaitNanos(TimeUnit.SECONDS.toNanos(2));
System.out.println(new Date() + "\tThread 1 remaining time " + waitTime);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(new Date() + "\tThread 1 is waken up");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try{
System.out.println(new Date() + "\tThread 2 is running");
try {
Thread.sleep(4000);
} catch (Exception ex) {
ex.printStackTrace();
}
condition.signal();
System.out.println(new Date() + "\tThread 2 ended");
} finally {
lock.unlock();
}
}).start();
}
}
复制
执行结果如下
Sun Jun 19 15:59:09 CST 2016 Thread 1 is waiting
Sun Jun 19 15:59:09 CST 2016 Thread 2 is running
Sun Jun 19 15:59:13 CST 2016 Thread 2 ended
Sun Jun 19 15:59:13 CST 2016 Thread 1 remaining time -2003467560
Sun Jun 19 15:59:13 CST 2016 Thread 1 is waken up
复制
从执行结果可以看出,即使thread2一开始就调用了signal()方式去唤起thread1,并且由于thread2在4秒钟后才释放锁,也即thread1在4秒后才获得锁,所以thread1的await方式在4秒钟后才返回,但是返回负值。
讯号量Semaphore
讯号量维护一个许可集,可通过acquire()获取许可(若无可用许可则阻塞),通过release()释放许可,因而可能唤起一个阻塞等待许可的线程。
与互斥锁类似,讯号量限制了同一时间访问临界资源的线程的个数,而且讯号量也分公正讯号量与非公正讯号量。而不同的是,互斥锁保证同一时间只会有一个线程访问临界资源,而讯号量可以容许同一时间多个线程访问特定资源。所以讯号量并不能保证原子性。
讯号量的一个典型使用场景是限制系统访问量。每位恳求进来后,处理之前都通过acquire获取许可,若获取许可成功则处理该恳求,若获取失败则等待处理或则直接不处理该恳求。
讯号量的使用方式
注意:与wait/notify和await/signal不同,acquire/release完全与锁无关,因而acquire等待过程中,可用许可满足要求时acquire可立刻返回,而不用像锁的wait和条件变量的await那样重新获取锁才会返回。或则可以理解成能让线程停止执行的有,只要可用许可满足需求,就早已获得了锁。
Java进阶系列