synchronized和Lock
synchronized同步锁(JVM)
同步代码块
- 用synchronized将 操作共享数据的代码给锁起来
- 如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象:this.
synchronized(任意对象/类名.class) {
多条语句操作共享数据的代码
}
同步代码块锁对象–对象锁(对象名)
锁对象是对象时,synchronized可以实现基于同一个对象的线程同步。
- 基于单个实例对象开启的多线程代码
public class SellTickets_Sync implements Runnable {
private static int number = 200;
private Object obj = new Object();
@SneakyThrows
@Override
public void run() {
while (true) {
//同步代码块 对象锁(对象名),锁住单个实例对象的线程访问同一个共享数据
synchronized (obj) {
number--;
if (number > 0) {
Thread.sleep(5);
System.out.println("票还剩下:" + number + "张," + Thread.currentThread().getName());
}
if (number == 0) {
System.out.println("票还剩下:" + number + "张," + Thread.currentThread().getName() + "票卖完了");
break;
}
}
}
System.out.println(Thread.currentThread().getName()+"跳出循环了");
}
public static void main(String[] args) {
//基于单个实例对象开启的多线程
SellTickets_Sync sellTickets1 = new SellTickets_Sync();
Thread t1 = new Thread(sellTickets1, "窗口一");
Thread t2 = new Thread(sellTickets1, "窗口二");
t1.start();
t2.start();
}
}
同步代码块锁对象–全局锁(类名.class)
synchronized(类名.class) {
多条语句操作共享数据的代码
}
- 全局锁(类名.class),锁住多个实例对象开启的线程访问同一个共享数据
public class SellTickets_Sync implements Runnable {
private static int number = 200;
private Object obj = new Object();
@SneakyThrows
@Override
public void run() {
while (true) {
//同步代码块 全局锁(类名.class),锁住多个实例对象开启的线程访问同一个共享数据
synchronized (Object.class) {
number--;
if (number > 0) {
Thread.sleep(5);
System.out.println("票还剩下:" + number + "张," + Thread.currentThread().getName());
}
if (number == 0) {
System.out.println("票还剩下:" + number + "张," + Thread.currentThread().getName() + "票卖完了");
break;
}
}
}
}
public static void main(String[] args) {
//基于多个实例对象开启的多线程
SellTickets_Sync sellTickets1 = new SellTickets_Sync();
SellTickets_Sync sellTickets2 = new SellTickets_Sync();
Thread t1 = new Thread(sellTickets1, "窗口一");
Thread t2 = new Thread(sellTickets2, "窗口二");
t1.start();
t2.start();
}
}
全局锁和对象锁的区别
- 对象锁,我们锁对象是一个对象,一个类可以有多个对象,所以我们只能保证基于同一对象开启的线程实现线程同步。
- 全局锁,全局锁又叫类锁,我们锁对象是一个类,我们都知道一个类在jvm中只有一个Class文件,类是具有唯一性的,所以我们可以实现基于一个类的不同对象开启的多线程实现线程同步。
同步方法(静态和非静态)
-
同步方法:就是把synchronized关键字加到方法上
-
同步方法锁对象是当前对象 也就是this
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//同步方法
public synchronized int SyncMethod() throws InterruptedException {
Thread.sleep(5);
return Tickets--;
}
- 静态同步方法:就是把synchronized关键字加到静态方法上
- 静态同步方法锁对象是 类名.class (全局锁)
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
// 静态同步方法
public static synchronized int SyncMethod() throws InterruptedException {
Thread.sleep(5);
return Tickets--;
}
Lock锁(JDK)
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了 锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
官方API对Lock锁的解释:
Lock是一个接口,我们不能直接使用,需要使用它的实现类。
- Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。
- 锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读锁。
- 虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。 所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。
- 随着这种增加的灵活性,额外的责任。 没有块结构化锁定会删除使用synchronized方法和语句发生的锁的自动释放。 在大多数情况下,应使用以下惯用语:
Lock l = ...;
l.lock(); //加锁
try {
// access the resource protected by this lock
}
finally {
l.unlock(); //释放锁
}
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。 (防止死锁)
- Lock实现提供了使用synchronized方法和语句的附加功能, 提供非阻塞尝试来获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly(),以及尝试获取可以超tryLock(long, TimeUnit) )。
- 一个Lock类还可以提供与隐式监视锁定的行为和语义完全不同的行为和语义,例如保证排序,非重入使用或死锁检测。 如果一个实现提供了这样的专门的语义,那么实现必须记录这些语义。
- 请注意, Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。 获取Lock实例的监视器锁与调用该实例的任何lock()方法没有特定关系。 建议为避免混淆,您不要以这种方式使用Lock实例,除了在自己的实现中。
除非另有说明,传递任何参数的null值将导致NullPointerException被抛出。
通过查看API手册我们可以看到,Lock是一个接口,我们不能直接使用,需要使用其实现类,主要有以下两个实现类:
- 1、ReentrantLock (可重入锁)
public class ReentrantLock extends Object implements Lock, Serializable
- 2、ReentrantReadWriteLock(读写锁)
public class ReentrantReadWriteLock extends Object implements ReadWriteLock, Serializable
ReentrantLock (可重入锁)
官方API对ReentrantLock 的解释:
一个可重入互斥Lock具有与使用synchronized方法和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能。
A ReentrantLock由线程拥有 ,最后成功锁定,但尚未解锁。 调用lock的线程将返回,成功获取锁,当锁不是由另一个线程拥有。 如果当前线程已经拥有该锁,该方法将立即返回。 这可以使用方法isHeldByCurrentThread()和getHoldCount()进行检查。
该类的构造函数接受可选的公平参数。 当设置true ,在争用下,锁有利于授予访问最长等待的线程。 否则,该锁不保证任何特定的访问顺序。 使用许多线程访问的公平锁的程序可能会比使用默认设置的整体吞吐量(即,更慢,通常要慢得多),但是具有更小的差异来获得锁定并保证缺乏饥饿。 但是请注意,锁的公平性不能保证线程调度的公平性。 因此,使用公平锁的许多线程之一可以连续获得多次,而其他活动线程不进行而不是当前持有锁。 另请注意, 未定义的tryLock()方法不符合公平性设置。 如果锁可用,即使其他线程正在等待,它也会成功。
建议的做法是始终立即跟随lock与try块的通话,最常见的是在之前/之后的建设,如:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } } 除了实现Lock接口,这个类定义了许多public种protected方法用于检查锁的状态。 其中一些方法仅适用于仪器和监控。
此类的序列化与内置锁的操作方式相同:反序列化锁处于未锁定状态,无论其序列化时的状态如何。
此锁最多支持同一个线程的2147483647递归锁。 尝试超过此限制会导致Error从锁定方法中抛出。
- 在使用ReeentrantLock的时候,必须要使用try/finally{lock.unlock()}确保锁被释放
public class ReentrantLockTest implements Runnable {
//票数
private static int tickets = 200;
// 定义一个可重入锁对象
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
//加锁
reentrantLock.lock();
try {
while (true) {
if (tickets > 0) {
tickets--;
Thread.sleep(2);
System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下:" + tickets + "张票");
} else {
System.out.println(Thread.currentThread().getName() + "票已售完,还剩下:" + tickets + "张票");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//开锁
reentrantLock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest lockTest = new ReentrantLockTest();
Thread t1 = new Thread(lockTest, "线程一");
Thread t2 = new Thread(lockTest, "线程二");
t1.start();
t2.start();
}
}
死锁问题
- 死锁概述:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
- 产生死锁原因
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
死锁代码案例:
public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()‐>{
while(true){
//死锁:
synchronized (objA){
//线程一
synchronized (objB){
System.out.println("小康同学正在走路");
}}}}).start();
new Thread(()‐>{
while(true){ synchronized (objB){
//线程二
synchronized (objA){
System.out.println("小薇同学正在走路");
} } } }).start(); } }
解决死锁问题
- 按顺序加锁
上个例子线程间加锁的顺序各不一致,导致死锁,如果每个线程都按同一个的加锁顺序这样就不会出现死锁。 - 获取锁时限
每个获取锁的时候加上个时限,如果超过某个时间就放弃获取锁之类的。 - 死锁检测
按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。