java锁
- 公平锁:多个线程按照申请锁的顺序获取锁,先来后到,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则加入到等待队列中,遵从FIFO。
- 非公平锁:多个线程获取锁的顺序不一定是申请锁的顺序,高并发情况下可能会优先级反转、出现饥饿现象(某些线程一直等待)。每个线程上来就尝试占有锁,如果失败,就采用类似公平锁的方式。优点是吞吐量大,synchronized也是非公平锁。
- 不可重入锁(递归锁):同一线程,执行某个方法已经获取了该锁,那么在该方法中再次获取该锁(同一线程)就会获取不到而阻塞。
- 可重入锁:同一线程外层函数获得锁后,内层函数可自动加锁 (前提锁对象得是同一个对象或者class)。且线程可以进入它已经拥有的锁的同步代码块儿。synchronized和ReentrantLock都是可重入锁,可重入锁最大的作用是避免一定程度的死锁。
- 自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样做的好处是减少线程上下文的切换消耗,缺点是循环会消耗CPU。
- 独占锁:该锁只能被一个线程所持有,ReentrantLock和synchronized都是独占锁。
- 共享锁:该锁可以被多个线程所持有。ReentrantReadWriteLock而言,读锁是共享锁,写锁是独占锁。读锁是共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的,只要包含写的操作都只有一个线程能够占有。
可重入锁及其验证
class Duck {
private synchronized void swim() {
System.out.println(Thread.currentThread().getName() + "游泳游泳");
sound();
}
private synchronized void sound() {
System.out.println(Thread.currentThread().getName() + "叫唤");
}
}
class Bird {
Lock mLock = new ReentrantLock();
void fly() {
mLock.lock();
System.out.println(Thread.currentThread().getName() + "fly fly~~");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
eat();
mLock.unlock();
}
private void eat() {
mLock.lock();
System.out.println(Thread.currentThread().getName() + "eat eat~~");
mLock.unlock();
}
}
public class LockTest {
//可重入锁
public static void main(String args[]) {
Duck duck = new Duck();
new Thread(duck::swim, "t1").start();
new Thread(duck::swim, "t2").start();
Bird bird = new Bird();
new Thread(bird::fly, "t3").start();
new Thread(bird::fly, "t4").start();
}
}
t1游泳游泳
t1叫唤
t2游泳游泳
t2叫唤
t3fly fly~~
t3eat eat~~
t4fly fly~~
t4eat eat~~Process finished with exit code 0
值得注意的是,使用Lock后,一段代码可以重复进行加锁,但加锁后一定要匹配释放锁,否则少一个释放锁线程不会结束。
手写一个自旋锁
public class MySpinLock {
private AtomicReference<Thread> mThreadAtomicReference = new AtomicReference<>();
private volatile AtomicInteger version;
private volatile AtomicInteger count;
public static void main(String[] args) throws InterruptedException {
spinLock1();
}
private static void spinLock1() throws InterruptedException {
MySpinLock mySpinLock = new MySpinLock();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
mySpinLock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mySpinLock.myUnlock();
}
}, "t1").start();
new Thread(() -> {
mySpinLock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mySpinLock.myUnlock();
}
}, "t2").start();
}
private void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "lock");
while (!mThreadAtomicReference.compareAndSet(null, thread)) {
}
}
private void myUnlock() {
Thread thread = Thread.currentThread();
mThreadAtomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "unlock");
}
}
读写锁代码验证
读写锁,只要包含写操作都应该是独占的,读与读之间可以共享。写的时候加写锁,必须保证线程互斥,不能有其他线程干扰,读的时候可以共享,所以是共享锁。
也就是说,读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占读写锁的。当有读锁时,写锁就不能获得;而当有写锁时,除了已经获得写锁的这个线程外,其他线程不能获取该读写锁的任何读锁。
读写锁升降级:
- 锁升级:读锁在没有释放的情况下,就去申请写锁,属于锁升级,ReentrantReadWriteLock不支持。
- 锁降级:写锁在没有释放的情况下,就去申请读锁,属于锁降级,ReentrantReadWriteLock支持。需要注意的是,从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。
//资源类
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在写入");
try {
Thread.sleep(300);
} catch (Exception e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
//读共享且时间段不一样
public void get(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在读取");
try {
Thread.sleep(300);
} catch (Exception e) {
e.printStackTrace();
}
Object res = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取完成:" + res);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
public class MyReadWriteLock {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI + "", finalI);
}, "t" + i).start();
}
for (int i = 5; i < 10; i++) {
int finalI = i;
new Thread(() -> {
myCache.get((finalI - 5) + "");
}, "t" + i).start();
}
}
}
CountDownLatch
非同步代码:
public class CountDownLatchTest {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
}
},"t" + i).start();
}
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
}
},"班长").start();
}
}
t1上完自习走人,离开教室
t0上完自习走人,离开教室
t2上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
班长班长上完自习走人,锁上门窗,离开教室
t3上完自习走人,离开教室Process finished with exit code 0
使用CountDownLatch进行计数同步后,前六个线程执行完(顺序随机)才能执行被CountDownLatch锁住的线程:
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
countDownLatch.countDown();
},"t" + i).start();
}
new Thread(() -> {
try {
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"班长").start();
}
}
t0上完自习走人,离开教室
t3上完自习走人,离开教室
t2上完自习走人,离开教室
t1上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
班长班长上完自习走人,锁上门窗,离开教室Process finished with exit code 0
CyclicBarrier
同理,和CountDownLatch类似,不过它是做加法:
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(6, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "班长上完自习走人,锁上门窗,离开教室");
}
});
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "上完自习走人,离开教室");
cyclicBarrier.await();//在此阻塞班长线程
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t" + i).start();
}
}
}
t1上完自习走人,离开教室
t0上完自习走人,离开教室
t3上完自习走人,离开教室
t4上完自习走人,离开教室
t5上完自习走人,离开教室
t2上完自习走人,离开教室
t2班长上完自习走人,锁上门窗,离开教室Process finished with exit code 0
Semaphore
抢车位,车位资源有限,先抢到先停,停满了后面的排队等里面的释放。
public class SemaphoreTest {
public static void main(String[] args) {
//模拟3个停车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位");
Thread.sleep(300);
System.out.println(Thread.currentThread().getName() + "停车后离开");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}, "t" + i).start();
}
}
}
t0抢到车位
t2抢到车位
t1抢到车位
t0停车后离开
t1停车后离开
t2停车后离开
t3抢到车位
t4抢到车位
t5抢到车位
t3停车后离开
t5停车后离开
t4停车后离开Process finished with exit code 0
synchronized和ReentrantLock区别
- 原始构成:
- sync是java关键字,属于JVM层面。使用java p编译后可以看到机器码中是monitorenter和monitorexit,其底层是通过monitor实现的,其实wait和notify等方法也依赖于monitor对象,只有在同步代码块中才能被调用。
- lock是具体的类,属于API层面。
- 使用方法:
- sync不需要手动释放锁,代码块执行完成后系统会自动让线程释放锁,且出现异常 不会退出失败,汇编码可以看到monitor其实退出了两次,就算是有异常发生也能退出。
- lock需要手动释放锁,若没有主动释放锁,线程一直运行下去,可能会造成死锁,需要lock,unlock,try,catch,finally语句块来完成
- 等待中断:
- sync,如果thread1不释放锁,thread2将一直阻塞等待下去,不能被中断,除非thread1抛出异常或运行完成。(1.6之后引入自旋等技术可能会好点)
- lock, 可中断,设置超时方法tryLock(long timeout, TimeUnit unit)或者lockInterruptible()放代码块中,调用interrupt()可中断等待。
- 加锁是否公平:
- sync只可能是非公平锁
- lock可以通过构造方法指定,默认是非公平
- 多个Condition
- sync不能绑定多个条件
- lock可以实现分组唤醒需要唤醒的线程,可以精确唤醒,不像是sync要么随机唤醒(notify)一个要么全部唤醒(notifyAll)。
- 性能
- lock底层实现使用了自旋CAS,可以很好地避免使线程由用户态转为内核态,减少线程上下文消耗,且灵活性较高,但使用起来没有sync方便。
- sync在1.6之前性能较lock有很大不足,1.6之后引入了自旋、偏量锁等,性能几乎持平,两者都可以用的情况下官方推荐sync,并且JVM未来发展更倾向于sync,阅读1.8的源码可以发现,很多地方已经由lock改成sync,如写时复制的List、map、set等(CopyOnWriteArrayList)。而sync使用起来更为方便,但是唤醒操作单一随机,没有lock那么灵活,所以具体使用哪个还是要看场景。
死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都无法推进下去,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的概率就很低。
代码验证:
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + ", 自己持有" + lockA + "尝试获得" + lockB);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + ", 自己持有" + lockB + "尝试获得" + lockA);
}
}
}
}
public class DeadLockTest {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB)).start();
new Thread(new HoldLockThread(lockB, lockA)).start();
}
}
死锁排查:
jps -l查看所有java进程
找到当前程序的进程号:jstack {进程号},就可以看到栈信息,有没有死锁了。
解决办法:重启程序、优化代码,避免出现死锁。