JUC学习笔记 - 03几种锁的介绍和使用案例

231 阅读3分钟

写在前面: 本文介绍了JUC中的几种锁以及使用案例。

ReentrantLock

synchronized一样,ReentrantLock也是可重入锁,即同一个线程可以对同一把锁锁定多次。ReentrantLock是可以替代synchronized的,只需要将原来写 synchronized的地方换写lock.lock(),最后通过lock.unlock()解锁。

public class TestReentrantLock {

    Lock lock = new ReentrantLock();
    Integer count = 0;

    void m1() {
        synchronized (this) {
            for (int i = 0; i < 100; i++) {
                count ++;
            }
        }
    }

    void m2() {
        try {
            lock.lock();
            for (int i = 0; i < 100; i++) {
                count ++;
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        TestReentrantLock rl = new TestReentrantLock();
        new Thread(rl::m1).start();
        new Thread(rl::m2).start();
    }
}

相比synchronized而言,reentrantlock还有tryLock方法,即尝试锁定。该方法返回锁定结果,并且不管锁定与否,后续方法都将继续执行。tryLock还支持等待时间参数,等待时间内如果拿到锁则立即执行后续代码,拿不到锁但是等待时间结束也会立即执行后续代码。

public class TestReentrantLock2 {

    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 5; i++) {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void m2() {
        boolean locked = false;
        try {
            locked = lock.tryLock();
            System.out.println(Thread.currentThread().getName() + " " + locked);
            for (int i = 0; i < 3; i++) {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (locked) {
                lock.unlock();
            }
        }
    }

    void m3() {
        boolean locked = false;
        try {
            locked = lock.tryLock(3, TimeUnit.SECONDS);
            System.out.println(Thread.currentThread().getName() + " " + locked);
            for (int i = 0; i < 3; i++) {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (locked) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TestReentrantLock2 rl = new TestReentrantLock2();

        new Thread(rl::m1, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2, "t2").start();
        new Thread(rl::m3, "t3").start();
    }
}

reentrantlock还可以使用lockInterruptibly()方法对interrupt()做出响应,即可以被打断的加锁。

public class TestReentrantLock3 {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            boolean flag = false;
            try {
                lock.lockInterruptibly();
                flag = true;
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                if (flag) {
                    lock.unlock();
                }
            }
        });
        t2.start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.interrupt();
    }
}

上面程序中的lock.lockInterruptibly()不同于lock.lock(),在加锁过程中是可以被打断的,即在调用t2.interrupt()时如果还没拿到锁,t2线程就会被打断并抛异常。

ReentrantLock还可以指定为公平锁,公平锁的意思是抢占锁时会有队列来维护谁先谁后,而不是说谁后来了之后就马上让谁执行。如果说这个锁不是公平锁,那么来了一个新线程上来就抢锁是有可能抢到的;如果说这个锁是个公平锁,这个线程上来会先检查队列里有没有原来等着的线程,如果有的话他就先进队列里等着别人先运行。ReentrantLock默认是非公平锁,公平锁需要通过new ReentrantLock(true)来指定。

public class TestReentrantLock4 extends Thread {

    private static ReentrantLock lock = new ReentrantLock(true);

    public void run() {
        for (int i = 0; i < 100; i++) {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TestReentrantLock4 rl = new TestReentrantLock4();
        Thread th1 = new Thread(rl);
        Thread th2 = new Thread(rl);
        th1.start();
        th2.start();
    }
}

可以看到公平锁的情况下,两个线程交替输出。而非公平锁的情况下就不一定了。

CountDownLatch

CountDownLatch就是倒数几个数然后打开门闩的意思。比如现在有10个线程,其中两个线程被latch.await()阻塞住,其他8个线程倒数8个数,每次通过latch.countDown()来倒数。倒数结束后两个被latch.await()阻塞的线程就会继续运行。

public class TestCountDownLatch {
    public static void main(String[] args) {
        usingCountDownLatch();
    }

    private static void usingCountDownLatch() {
        Thread[] threads = new Thread[10];
        CountDownLatch latch = new CountDownLatch(8);


        threads[0] = new Thread(() -> {
            try {
                latch.await();
                System.out.println(Thread.currentThread().getName() + "await");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });


        threads[1] = new Thread(() -> {
            try {
                latch.await();
                System.out.println(Thread.currentThread().getName() + "await");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        for (int i = 2; i < 10; i++) {
            threads[i] = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " countdown");
                latch.countDown();
            });
        }

        for (Thread thread : threads) {
            thread.start();
        }

        System.out.println("end latch");
    }
}

可以考虑用join来实现,但是join必须等待线程结束后才能继续运行,CountDownLatch相比之下更灵活。

CyclicBarrier

CyclicBarrier即循环屏障,加入设定屏障容量为20,当等待的线程达到20了那么这20个线程开始同时运行。接着屏障继续立起来等待后面20个线程满了再继续同时运行,以此类推。

public class TestCyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满人"));

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

可以看到“满人”输出了5次,即屏障生效了5次,每次都是20个线程同时运行。

Phaser

Phaser像是结合了CountDownLatchCyclicBarrier,它可以将线程工作分为多个阶段,每个阶段可以让部分线程往前走,让部分线程停止。具体就是调用phaser.arriveAndAwaitAdvance()让线程停止,等到等待线程满了再继续执行。还可以调用CyclicBarrier增加屏障个数。下面是个简单的例子方便理解:

public class TestPhaser {
    static MarriagePhaser phaser = new MarriagePhaser();

    static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        phaser.bulkRegister(6);
        for (int i = 0; i < 5; i++) {
            new Thread(new Person("考生" + i)).start();
        }
        new Thread(new Person("考官")).start();
    }

    static class MarriagePhaser extends Phaser {
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人到达现场!" + registeredParties);
                    System.out.println();
                    return false;
                case 1:
                    System.out.println("准备考试!" + registeredParties);
                    System.out.println();
                    return false;
                case 2:
                    System.out.println("考试中!" + registeredParties);
                    System.out.println();
                    return false;
                case 3:
                    System.out.println("考试结束!" + registeredParties);
                    System.out.println();
                    return false;
                case 4:
                    System.out.println("所有人离开" + registeredParties);
                    return false;
                case 5:
                    System.out.println("测试register" + registeredParties);
                    return true;
                default:
                    return true;
            }
        }
    }

    static class Person implements Runnable {
        String name;

        public Person(String name) {
            this.name = name;
        }

        @Override
        public void run() {

            milliSleep(1000);
            System.out.printf("%s 到达现场!\n", name);
            phaser.arriveAndAwaitAdvance();

            if (name.equals("考官")) {
                milliSleep(1000);
                System.out.printf("%s 分发试卷!\n", name);
                phaser.arriveAndAwaitAdvance();
            } else {
                milliSleep(1000);
                System.out.printf("%s 等待考试!\n", name);
                phaser.arriveAndAwaitAdvance();
            }

            milliSleep(1000);
            System.out.printf("%s 考试中!\n", name);
            phaser.arriveAndAwaitAdvance();

            milliSleep(1000);
            System.out.printf("%s 考试结束!\n", name);
            phaser.arriveAndAwaitAdvance();

            if (name.equals("考官")) {
                milliSleep(1000);
                System.out.printf("%s 收试卷!\n", name);
                phaser.arriveAndAwaitAdvance();
            } else {
                milliSleep(1000);
                System.out.printf("%s 离开考场!\n", name);
                phaser.arriveAndDeregister();
            }

            milliSleep(1000);
            System.out.printf("%s 测试register!\n", name);
            phaser.register();
        }
    }
}

ReadWriteLock

ReadWriteLock即读写锁,其中读锁是共享锁,写锁是排他锁。在一个读多写少的场景下,如果使用普通的锁,那么效率会非常低。我们希望读线程加锁时,其他读线程也可以读。写线程在写的时候读线程和其他写线程需要等待。举个例子:

public class TestReadWriteLock {
    private static int value;

    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read over! v = " + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Runnable readR = () -> read(readLock);
        Runnable writeR = () -> write(writeLock, new Random().nextInt());
        for (int i = 0; i < 5; i++) {
            new Thread(writeR).start();
        }
        for (int i = 0; i < 18; i++) {
            new Thread(readR).start();
        }
    }
}

可以看到read(Lock lock)方法里有sleep方法进行阻塞,但是由于读锁是共享锁,所以看输出好像read方法是同时运行的,并没有每个线程相隔1秒依次运行。而write方法中的锁是排他锁,所以 write方法运行时其他线程都在等待。

Semaphore

Semaphore译为信号灯,个人觉得就是来控制同时可以运行几个线程的。例如现在Semaphore s = new Semaphore(1),来个线程t1acquire一下,那就由1变成0,并且当前线程可以继续执行后续任务。另一个线程t2调用acquire时,如果还是0,则阻塞在这里。直到第一个线程t1通过release后又把0变回1,t2才会将1变成0,并且继续运行下去。同时Semaphore也是可以设置为公平锁和非公平锁的。例子如下:

public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore s = new Semaphore(1, true);

        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                s.release();
            }
        }).start();

        new Thread(() -> {
            try {
                s.acquire();
                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");
                s.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Exchanger

Exchanger比较好理解,就是两个线程交换数据用的。通过exchanger.exchange()方法阻塞住线程来交换数据。例子如下:

public class TestExchanger {

    static Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) {
        new Thread(()->{
            String s = "T1";
            try {
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t1").start();

        new Thread(()->{
            String s = "T2";
            try {
                s = exchanger.exchange(s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + s);
        }, "t2").start();
    }
}

LockSupport

LockSupport是一个比较底层的工具类,是用来创建锁和其他同步工具类的基本线程阻塞原语,是Java锁和同步器框架的核心。AQS: AbstractQueuedSynchronizer,就是通过调用LockSupport.park()LockSupport.unpark()的方法,来实现线程的阻塞和唤醒的。

public class TestLockSupport {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if (i == 5) {
                    LockSupport.park();
                    System.out.println("first park");
                }
                if (i == 8) {
                    LockSupport.park();
                    System.out.println("second park");
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        LockSupport.unpark(t);
        try {
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after 8 seconds!");
        LockSupport.unpark(t);
    }
}

LockSupport.park()可以让线程阻塞,LockSupport.unpark(t)可以唤醒线程。且LockSupport.unpark(t)可以先于LockSupport.park()执行,并且线程不会阻塞。

不同锁的应用

现在尝试用不同的锁来实现这样一个需求:实现一个容器,提供两个方法addsize。并且写两个线程,满足线程1添加10个元素到容器中,线程2监控元素个数,当元素个数到达5时线程2给出提示并结束。

使用synchronized

public class TestSynchronized {

	volatile List<Object> lists = new ArrayList<>();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}
	
	public static void main(String[] args) {
		final Object lock = new Object();
		TestSynchronized c = new TestSynchronized();

		new Thread(() -> {
			synchronized(lock) {
				System.out.println("t2启动");
				if(c.size() != 5) {
					try {
						lock.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("t2结束");
				lock.notify();
			}
			
		}, "t2").start();
		
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1启动");
			synchronized(lock) {
				for(int i=0; i<10; i++) {
					c.add(new Object());
					System.out.println("add " + i);
					
					if(c.size() == 5) {
						lock.notify();
						try {
							lock.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}, "t1").start();
	}
}

使用CountDownLatch

public class TestCountDownLatch {

	volatile List<Object> lists = new ArrayList<>();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}

	public static void main(String[] args) {
		TestCountDownLatch c = new TestCountDownLatch();

		CountDownLatch latch = new CountDownLatch(1);
		CountDownLatch latch2 = new CountDownLatch(1);

		new Thread(() -> {
			System.out.println("t2启动");
			if (c.size() != 5) {
				try {
					latch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("t2 结束");
			latch2.countDown();

		}, "t2").start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}

		new Thread(() -> {
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
					latch.countDown();
					try {
						latch2.await();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}

		}, "t1").start();
	}
}

使用LockSupport

public class TestLockSupport {

	volatile List<Object> lists = new ArrayList<>();

	public void add(Object o) {
		lists.add(o);
	}

	public int size() {
		return lists.size();
	}

	static Thread t1 = null, t2 = null;

	public static void main(String[] args) {
		TestLockSupport c = new TestLockSupport();

		t1 = new Thread(() -> {
			System.out.println("t1启动");
			for (int i = 0; i < 10; i++) {
				c.add(new Object());
				System.out.println("add " + i);

				if (c.size() == 5) {
					LockSupport.unpark(t2);
					LockSupport.park();
				}
			}
		}, "t1");

		t2 = new Thread(() -> {
			LockSupport.park();
			System.out.println("t2 结束");
			LockSupport.unpark(t1);
		}, "t2");

		t1.start();
		t2.start();
	}
}