synchronized/Lock锁详解

synchronized/Lock锁详解_第1张图片

synchronized/Lock

synchronized

synchronized是Java中的关键字,是一种同步锁。

synchronized保证同一时刻有且只有一条线程在操作临界资源,其他线程必须等待该线程处理结束后再对共享数据进行操作。此时便产生了互斥锁,互斥锁特性如下:

  • 互斥性:在同一时刻只允许一条线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样就实现了在同一时刻只有一条线程对所需要同步的代码块(符合操作)进行访问。互斥性也成了操作的原子性。
  • 可见性:必须确保在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一线程可见(也就是在获得锁时应获得最新共享变量的值),否则另一线程可能会在本地缓存上继续操作,从而引起数据的不一致。

synchronized关键字保证同一时刻最多只有1个线程执行被synchronized修饰的方法/代码,其他线程必须等待当前线程执行完该方法/代码块后才能执行该方法/代码块。

同步代码块
  • 修饰某一处代码块,被修饰的代码块称为同步代码块。作用范围是{}之间;作用的对象是调用这个代码块的对象。
synchronized (this){
    System.out.println("同步代码块 ");
}
  • 修饰某个类。作用范围是{}之间;作用的对象是这个类的所有对象。
class Ticket {
    public void sale() {
        synchronized (Ticket.class) {
            // 操作临界资源
        }
    }
}
同步方法
  • 修饰在方法上,被修饰的方法称为同步方法。作用范围是整个方法;作用对象是调用这个方法的对象。
public synchronized void sale() {
    // ......
}
  • 修饰某个静态方法。作用范围是整个静态方法;作用对象是这个类的所有对象。
public static synchronized void test(){
    // ......     
}
案例——卖票
public class SynchronizedDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "C").start();
    }
}

class Ticket {
    // 票数
    private int number = 30;

    // 操作方法:卖票
    public synchronized void sale() {
        // 判断是否有余票
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + " : " + (number--) + " " + number);
        }
    }
}
总结

获取锁的线程释放锁的情况:

  • 正常执行结束,自动释放锁;
  • 执行过程中发生异常,JVM让线程自动释放锁。

synchronized的同步效率很低,如果某个代码块被其修饰,当一线程进入synchronized修饰的代码块,那么其余线程只能一直等待,等待持有锁的线程释放锁,才能进入同步代码块。

Lock

如果持有锁的线程由于要等待IO或其他原因(如调用sleep方法),被阻塞了,但是没有释放锁,其他线程就只能等待,非常影响程序性能。因此需要一种机制可以不让等待的线程一直无期限的等待下去(如只等待一定时间,或能够响应中断),通过Lock可以解决。如lock可以判断线程是否成功获取到锁,而synchronized无法做到。

锁类型
  • 可重入锁
  • 可中断锁
  • 公平锁
  • 读写锁
Lock接口
public interface Lock {

    // 获得锁
    // 如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁
    // lock()方法不能被中断,一旦陷入死锁,lock()会进入无限等待
    void lock();

    // 除非当前线程被中断,否则获取锁
    // 如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到出现以下两种情况之一:
    //     1.锁被当前线程获取
    //     2.其他线程中断当前线程,支持中断获取锁
    // 和lock()方法不同的是在锁的获取中可以中断当前线程
    // 如果当前线程在进入此方法时已设置其中断状态
    // 那么获取锁时被中断,并且支持获取锁的中断,然后抛出InterruptedException并清除当前线程的中断状态
    void lockInterruptibly() throws InterruptedException; 

    // 非阻塞获取锁(如果有),并立即返回true;如果锁不可用,则立即返回false
    // 该方法会立即返回,即使拿不到锁的时候也不会一直在那等待
    // 我们可以根据是否能获取到锁来决定后续程序的行为
    boolean tryLock();
    
    // 如果线程在给定的等待时间内获取到锁,且当前线程未中断,则获取锁
    // 如果锁可用,则此方法立即返回true
    // 如果不可用,则出于线程调度目的,当前线程将被挂起,处于休眠状态,直到发生以下3种情况之一:
    //     1.锁被当前线程获取
    //     2.其他线程中断当前线程,支持中断获取锁
    //     3.经过指定的等待时间如果获得了锁,则返回true
    // 如果经过了指定的等待时间,未获得锁,则返回false。如果时间小于等于0,则该方法不需等待
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  

     // 解锁
    void unlock(); 
    
    // 返回绑定到此Lock实例的新Condition实例
    Condition newCondition();
}
lock()/unlock()

如果使用了lock,必须主动释放锁,就算发生了异常,也需要手动释放,因为lock不会像synchronized一样自动释放锁。所以使用lock,必须在try{}catch(){}中进行,并在finally{}中释放锁,防止死锁。

Lock lock = new ReentrantLock();
try {
    lock.lock();
    System.out.println("上锁了");
}catch (Exception e){
    e.printStackTrace();
}finally {
    lock.unlock();
    System.out.println("解锁了");
}

newCondition

关键字synchronized与wait()/notify()一起使用可以实现等待/通知。Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知。使用notify()时,JVM会随机唤醒某个等待的线程,使用Condition类可以选择性通知,Condition常用的两个方法:

  • await():使当前线程进入等待状态,同时释放锁。等到其他线程调用signal()方法时,这个沉睡线程会重新获得锁并继续执行代码(在哪沉睡在哪唤醒)
  • signal():用于唤醒一个等待的线程。

注意

在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在调用signal()后,会从当前Condition对象的等待队列中,唤醒一个线程,被唤醒的线程开始尝试获取锁,一旦成功获得锁就继续往下执行。

例子:

有两个线程,一个初始值是0的number变量,一个线程当number == 0时,对number值+1,另外一个线程当number == 1时,对number-1:

public class LockDemo {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.incr();
            }
        },"AA").start();

        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.decr();
            }
        },"BB").start();
        /**
         * AA::1
         * BB::0
         * AA::1
         * BB::0
         * .....
         */     
    }
}

class Share {

    private Integer number = 0;

    private ReentrantLock lock = new ReentrantLock();

    private Condition newCondition = lock.newCondition();

    // +1 的方法
    public void incr() {
        try {
            // 加锁
            lock.lock();
            while (number != 0) {
                // 沉睡
                newCondition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            // 唤醒另一个沉睡的线程 
            newCondition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // -1 的方法
    public void decr() {
        try {
            lock.lock();
            while (number != 1) {
                newCondition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            newCondition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock可重入锁

ReentrantLock是唯一实现了Lock接口的类,且提供了更多的方法。

可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会死锁。

public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("第1次获取锁,这个锁是:" + lock);
                    for (int i = 2;i<=11;i++){
                        try {
                            lock.lock();
                            System.out.println("第" + i + "次获取锁,这个锁是:" + lock);
                            try {
                                Thread.sleep(new Random().nextInt(200));
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } finally {
                           // 注意:一定要释放锁。如果把这里注释掉的话,那么程序就会陷入死锁当中
                           lock.unlock();
                        }
                    }

                } finally {
                    lock.unlock();
                }
            }
        }).start();
    }
}
/**
 * 第1次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第2次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第3次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * ...
 */
ReadWriteLock读写锁
public interface ReadWriteLock {
  
    // 获取读锁
    Lock readLock();

  // 获取写锁
    Lock writeLock();
}

读写分离,可以有多个线程进行读操作,提高效率。

ReentrantReadWriteLock实现了ReadWriteLock接口。提供了更丰富的方法,最重要的还是获取读锁和写锁。

案例——多个线程进行读操作

// synchronized加锁
public class SynchronizedDemo {

    public static void main(String[] args) {
        final SynchronizedDemo test = new SynchronizedDemo();
        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();

        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();
    }

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行读操作");
        }
        System.out.println(thread.getName()+"读操作完毕");
    }
}

/*
结果:
Thread-0正在进行读操作
Thread-0读操作完毕
Thread-1正在进行读操作
Thread-1正在进行读操作
......
......
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
*/

// 读锁
public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    public static void main(String[] args) {
        final ReentrantReadWriteLockDemo test = new ReentrantReadWriteLockDemo();
        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();

        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();
    }

    public void get2(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

/*
结果:
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
......
Thread-0正在进行读操作
Thread-1正在进行读操作
......
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
......
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-0读操作完毕
Thread-1读操作完毕
*/

结论:

使用读锁,线程1和线程2可以同时读,提高了效率。

注意

  1. 如果此时已经有线程持有了读锁,其他线程是可以申请读锁的,但是不能申请写锁,需要等待读锁释放,才能获得锁。
  2. 如果此时已经有线程持有了写锁,其他线程无论申请读锁还是写锁,都需要持有写锁的线程释放锁,才能成功获得。
synchronized和Lock的区别

你可能感兴趣的:(并发编程,java,开发语言)