概述
Java有两种锁,一种是使用关键字Synchronized对方法或者代码块进行加锁,一种是使用接口Lock(实际上其实现类)进行上锁和解锁。
区别:
- Synchronized是java的一个关键字,而Lock是一个java类。
- Synchronized是一个“自动”的“隐式”锁,也就是只要在方法或者代码块上加上该关键字,就会实现自动的上锁和解锁,而Lock是一个非自动的显式锁,上锁和解锁都需要用代码显示指定。如果不显式释放锁,就会造成死锁。
- Synchronized是可重入、非公平锁。而Lock锁是可重入,公平/非公平锁,通过设置可以设置为公平锁或者非公平锁。
- Synchronized修饰成员方法锁是这个对象本身,修饰静态方法的锁是类对象,修饰代码块时可以指定任意非null对象。而Lock锁的锁就是这个Lock对象。
使用
Lock接口有三个实现类:
- ReentrantLock:可重入锁,可以代替上面synchronized关键字的使用。
- ReentrantReadWriteLock.ReadLock :读锁。
- ReentrantReadWriteLock.WriteLock:写锁。
读锁与写锁总是成对存在的,以实现对资源的合理访问,如果使用普通的可重入锁的话,无论是对资源的读和写,都是排他的。如果使用读写锁,那么在资源处于读锁状态时,其他读操作也可以进来访问,写操作就不行,如果处于写锁状态时,那么其他读写操作都不能访问。可以实现并发度、排他写。
可重入锁ReentrantLock的使用:
public class LockDemo {
//创建一个锁对象
private static Lock lock = new ReentrantLock();
public static void main(String[] args){
//代码就是开始是个线程,然后每个线程对以共享变量num循环自增十次。正确输出结果是100
testReentrantLock();
}
public static void testReentrantLock(){
Integer num = 0;
Data data = new Data(num);
for (int i = 0;i<10;i++){
new Thread(data).start();
}
}
static class Data implements Runnable{
private Integer num;
public Data(Integer num){
this.num = num;
}
@Override
public void run() {
for (int i = 0;i<10;i++){
//上锁
lock.lock();
try {
try {
//这里睡眠一毫米是为了让效果更佳明显
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num ++;
System.out.println(num);
}finally {
//释放锁
lock.unlock();
}
}
}
}
}
结果:正确
去掉上锁、解锁的代码。结果错误。
还可以给获取锁设置一个超时时间,时间过了还没获取到锁,就返回一个false并抛出一个中断异常。获取到锁就返回true。
//使用该方法获取锁,可以设置超时,使得线程不会永久阻塞。
boolean tryLock(long time, TimeUnit unit)
使用Synchronized和ReentrantLock实现一个消费者生产者实例并做比较
使用Synchronized
public class SynchronizedConsumerProductDemo {
public static void main(String[] args) {
ConsumerProduct consumerProduct = new ConsumerProduct();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
consumerProduct.consumer();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAAAAAAAAAA").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
consumerProduct.product();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BBBBBBBBBB").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
consumerProduct.consumer();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CCCCCCCCCCCC").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
consumerProduct.product();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"DDDDDDDDDD").start();
}
static class ConsumerProduct{
private int num;
public synchronized void consumer() throws InterruptedException {
while (num == 0){
//如果num等于0,就把当前线程设置为等待状态。
this.wait();
}
num --;
System.out.println(num);
//唤醒其他线程
this.notifyAll();
}
public synchronized void product() throws InterruptedException {
while (num == 1){
//如果num
this.wait();
}
num ++;
System.out.println(num);
this.notifyAll();
}
}
}
使用ReentrantLock
static class ConsumerProduct{
private int num;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void consumer() throws InterruptedException {
while (num == 0){
//如果num等于0,就把当前线程设置为等待状态。
condition.await();
}
num --;
System.out.println(Thread.currentThread().getName() + "====> num = " + num);
//唤醒其他线程
condition.signalAll();
}
public void product() throws InterruptedException {
while (num == 1){
//如果num
condition.await();
}
num ++;
System.out.println(Thread.currentThread().getName() + "====> num = " + num);
condition.signalAll();
}
}
总结:
synchronized是使用wait和notifyAll或者notify配合实现线程间通信的。
ReentrantLock是使用condition的await和signalAll或者signal配合实现线程间通信的。await是等待。
condition可以实现精准的通知唤醒,也就是可以精准地通知唤醒哪些类型的线程。
public class Obj2 {
public static void main(String[] args) {
data1 data = new data1();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
data.print1();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAAAAAAAAAA").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
data.print2();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"BBBBBBBBBB").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
data.print3();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"CCCCCCCCCCCC").start();
new Thread(()->{
try {
for (int i = 0;i<10;i++){
data.print4();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"DDDDDDDDDD").start();
}
public static class data1{
private int num;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
Condition condition4 = lock.newCondition();
public void print1() throws InterruptedException {
lock.lock();
try {
while (num != 0){
//使用condition1进行线程阻塞,要唤醒,就要使用condition1
condition1.await();
}
num = 1;
System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
//唤醒condition2管理的线程
condition2.signalAll();
}finally {
lock.unlock();
}
}
public void print2() throws InterruptedException {
lock.lock();
try {
while (num != 1){
condition2.await();
}
num = 2;
System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
condition3.signalAll();
}finally {
lock.unlock();
}
}
public void print3() throws InterruptedException {
lock.lock();
try {
while (num != 2){
condition3.await();
}
num = 3;
System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
condition4.signal();
}finally {
lock.unlock();
}
}
public void print4() throws InterruptedException {
lock.lock();
try {
while (num != 3){
condition4.await();
}
num = 0;
System.out.println(Thread.currentThread().getName() + "=====> num = " + num);
condition1.signal();
}finally {
lock.unlock();
}
}
}
}
解释:同一个lock可以创建多个condition,使用某个condition来阻塞线程,就要使用这个condition来唤醒线程,所以就可以准确地设置要唤醒那些线程。比如上面代码AAAAAAAAAAA线程会唤醒BBBBBBB线程,BBBBBB线程会唤醒CCCCCCCC线程,CCCCCCCCCCC线程会唤醒DDDDDDDDDDD线程,DDDDDDDDDD线程会唤醒AAAAAAAAAAAAA线程。

线程的执行顺序都是有序的。
读写锁的使用
读写锁是为了区分读线程和写线程,使得对资源的控制更加地合理,因为读操作不会涉及到并发安全问题,所以多个读线程可以同时获得读锁,但是读线程占用锁时,写线程不能访问资源。写线程占用锁时,读线程和其他写线程不能访问资源,这就既保证了读线程总能读到最新数据。资源的方法控制也更加合理。
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLockD readWriteLockD = new ReadWriteLockD();
CountDownLatch latch = new CountDownLatch(50);
for (int i = 0;i<35;i++){
new Thread(()->{
//35个读线程
try {
readWriteLockD.read();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
for (int i = 0;i<15;i++){
new Thread(()->{
//15个写线程
try {
readWriteLockD.write();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
}
static class ReadWriteLockD{
private int num = 0;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void read() throws InterruptedException {
//睡眠十毫秒,使得结果更为明显
TimeUnit.MILLISECONDS.sleep(10);
readLock.lock();
try {
System.out.println("获取到了读锁 num = " + num);
}finally {
System.out.println("释放了读锁");
TimeUnit.MILLISECONDS.sleep(10);
readLock.unlock();
}
}
public void write() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(10);
writeLock.lock();
try {
num ++ ;
System.out.println("获取到了写锁使得num+1");
}finally {
System.out.println("释放了写锁");
TimeUnit.MILLISECONDS.sleep(10);
writeLock.unlock();
}
}
}
}
结果:
上图可以看出可以并发获取读锁,而不用等到上一个读锁释放。
但是写锁就非常有规律,必须等写锁释放,其他线程才能获取锁。