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);
}
}
}
获取锁的线程释放锁的情况:
synchronized的同步效率很低,如果某个代码块被其修饰,当一线程进入synchronized修饰的代码块,那么其余线程只能一直等待,等待持有锁的线程释放锁,才能进入同步代码块。
如果持有锁的线程由于要等待IO或其他原因(如调用sleep方法),被阻塞了,但是没有释放锁,其他线程就只能等待,非常影响程序性能。因此需要一种机制可以不让等待的线程一直无期限的等待下去(如只等待一定时间,或能够响应中断),通过Lock可以解决。如lock可以判断线程是否成功获取到锁,而synchronized无法做到。
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,必须主动释放锁,就算发生了异常,也需要手动释放,因为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("解锁了");
}
关键字synchronized与wait()/notify()一起使用可以实现等待/通知。Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知。使用notify()时,JVM会随机唤醒某个等待的线程,使用Condition类可以选择性通知,Condition常用的两个方法:
注意:
在调用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是唯一实现了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]
* ...
*/
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可以同时读,提高了效率。
注意: