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可以同时读,提高了效率。
注意:
- 如果此时已经有线程持有了读锁,其他线程是可以申请读锁的,但是不能申请写锁,需要等待读锁释放,才能获得锁。
- 如果此时已经有线程持有了写锁,其他线程无论申请读锁还是写锁,都需要持有写锁的线程释放锁,才能成功获得。