认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。
在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等。
判断规则:
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized
关键字和Lock
的实现都是悲观锁。
适合写操作多的场景,先加锁保证写操作时数据的正确。
创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()
方法,另一个线程调用sendMessage()
方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
这个地方,锁的是对象,也就是Phone实例,只要实例不释放锁,其他方法调用就得等着。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail).start();
Thread.sleep(1000);
new Thread(phone::sendMessage).start();
}
}
class Phone {
public synchronized void sendEmail() {
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()
方法(sendEmail()
方法里有Thread.sleep
操作),另一个线程调用sendMessage()
方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
一个对象里有多个synchronized方法,某一时刻,一个线程调用其中一个synchronized方法,其他线程是拿不到这个对象的锁的,就要等待,而且sleep操作不会释放锁,所以sendMessage()
方法只能等待sendEmail()
释放锁之后才能执行。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail).start();
Thread.sleep(1000);
new Thread(phone::sendMessage).start();
}
}
class Phone {
public synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建一个Phone类的实例,分别启动两个线程,一个线程调用sendEmail()
方法,另一个线程调用sendHello()
方法(sendHello()
方法是普通方法,没有synchronized修饰),打印顺序是什么?
先输出Phone.sendHello,后输出Phone.sendEmail。
sendHello()
方法没有synchronized修饰,也就不需要争抢资源,所以先输出了sendHello。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail).start();
Thread.sleep(1000);
new Thread(phone::sendHello).start();
}
}
class Phone {
public synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
public void sendHello() {
System.out.println("Phone.sendHello");
}
}
创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()
方法,另一个线程调用实例2的sendMessage()
方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
因为sendEmail()
和sendMessage()
是两个对象,synchronized锁的是对象,也就是两个锁,各锁各的,这两个对象在执行方法的时候,不存在竞争。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(phone1::sendEmail).start();
Thread.sleep(1000);
new Thread(phone2::sendMessage).start();
}
}
class Phone {
public synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()
静态方法,另一个线程调用sendMessage()
静态方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
注意,这里方法上加了static修饰,所以synchronized锁的就不是对象了,锁的是类,那么这个类的所有实例都会受到影响。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> phone.sendEmail()).start();
Thread.sleep(1000);
new Thread(() -> phone.sendMessage()).start();
}
}
class Phone {
public static synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public static synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()
静态方法,另一个线程调用实例2的sendMessage()
静态方法,打印顺序是什么?
先输出Phone.sendEmail,后输出Phone.sendMessage。
对于普通的同步方法,锁的是实例对象,通常指this,具体的某个对象,所有普通同步方法用的都是同一把锁,即实例对象本身,对于静态同步方法,所的是当前类的Class对象,当前类创建的不同实例共享这一个锁,对于tongue方法块,锁定的是synchronized括号内的对象。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendEmail()).start();
Thread.sleep(1000);
new Thread(() -> phone2.sendMessage()).start();
}
}
class Phone {
public static synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public static synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建一个Phone类的实例,启动两个线程,一个线程调用sendEmail()
静态方法,另一个线程调用sendMessage()
方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
这里的锁,一个加在了类上,一个加在了对象上,锁的是两个东西,两者不冲突,所以先输出的Phone.sendMessage。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> phone.sendEmail()).start();
Thread.sleep(1000);
new Thread(() -> phone.sendMessage()).start();
}
}
class Phone {
public static synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
创建两个Phone类的实例,启动两个线程,一个线程调用实例1的sendEmail()
静态方法,另一个线程调用实例2的sendMessage()
方法,打印顺序是什么?
先输出Phone.sendMessage,后输出Phone.sendEmail。
同理,一个是对象锁,一个是类锁,他俩并没有资源的竞争。
当一个线程试图访问同步代码的时候,它必须先获取到锁,正常退出、抛出异常的时候都会释放锁。
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendEmail()).start();
Thread.sleep(1000);
new Thread(() -> phone2.sendMessage()).start();
}
}
class Phone {
public static synchronized void sendEmail() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Phone.sendEmail");
}
public synchronized void sendMessage() {
System.out.println("Phone.sendMessage");
}
}
synchronized同步代码块、synchronized普通同步方法、synchronized静态同步方法。
javap -c ***.class
:文件反编译
javap -v ***.class
:文件反编译(更详细,v:verbose)
public class LockDemo {
final Object object = new Object();
public void fun() {
synchronized (object) {
System.out.println("LockDemo.fun");
// throw new RuntimeException("xxx");// 如果手动抛出异常,那么monitorenter和monitorexit就是一对一了
}
}
public static void main(String[] args) {
}
}
通过对class文件反编译javap -c ***.class
,可以看出synchronized同步代码块的原理是:使用monitorenter
和monitorexit
指令来完成,但是这里会发现两个monitorexit
,分别对应正常退出和异常退出两种情况的释放锁。
public class LockDemo {
public synchronized void fun() {
System.out.println("LockDemo.fun");
}
public static void main(String[] args) {
}
}
通过对class文件反编译javap -v ***.class
,可以看到fun上会有一个ACC_SYNCHRONIZED
标识。Java虚拟机会检查方法上有没有ACC_SYNCHRONIZED
标识,如果有,执行线程会先持有monitor锁,然后再执行方法,最后在方法完成(正常完成或异常退出)时候释放锁。
public class LockDemo {
public static synchronized void fun() {
System.out.println("LockDemo.fun");
}
public static void main(String[] args) {
}
}
通过对class文件反编译javap -v ***.class
,可以看到fun上会有两个标识:ACC_STATIC
, ACC_SYNCHRONIZED
,由此就可以确定方法是普通同步方法还是静态同步方法了。
Java里的每一个对象都可以持有锁,我们知道所有类的父类是Object,根据Java底层源码是C++来看,Java里的Object对应的C++里的ObjectMonitor,然而ObjectMonitor就带着一个对象监视器,也就是monitor,所以每个Java对象都可以持有锁。
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "thread1").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "thread2").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "thread3").start();
}
}
class Ticket {
private int number = 50;
ReentrantLock reentrantLock = new ReentrantLock(true);// 不传参时候,默认非公平锁
public void sale() {
reentrantLock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第" + number-- + "张,还剩" + number + "张");
}
} finally {
reentrantLock.unlock();
}
}
}
公平锁:多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买,后来的人在队尾排着。
非公平锁:多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,可能造成优先级翻转或者饥饿状态。
为什么会有公平锁/非公平锁?为什么默认非公平锁?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平锁?什么时候用非公平锁?
如果为了更高的吞吐量,采用非公平锁更合适,因为节省了多线程切换的时间,吞吐量自然就高了,否则就采用公平锁。
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是一个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可:可以
重:再次
入:进入
锁:同步锁
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入,自己可以获取自己的内部锁。
隐式锁默认是可重入锁,在一个synchronized修饰的方法或者代码块内部,调用本类的其他synchronized修饰的方法或者代码块时候,是永远可以得到锁的。
synchronized同步代码块
public class LockDemo {
public static void main(String[] args) {
final Object object = new Object();
new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + ":外层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + ":中层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + ":内层调用");
}
}
}
}).start();
}
}
synchronized同步方法
public class LockDemo {
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
new Thread(lockDemo::fun1, "threadName").start();
}
public synchronized void fun1() {
System.out.println("LockDemo.fun1:" + Thread.currentThread().getName());
fun2();
}
public synchronized void fun2() {
System.out.println("LockDemo.fun2:" + Thread.currentThread().getName());
fun3();
}
public synchronized void fun3() {
System.out.println("LockDemo.fun3:" + Thread.currentThread().getName());
}
}
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":外层");
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":内层");
} finally {
reentrantLock.unlock();
}
} finally {
reentrantLock.unlock();
}
}, "threadName").start();
}
}
要保证lock()
和unlock()
是配对出现的,如果不配对,就有可能出现死锁的情况,第一个线程使用完之后,不释放,第二个线程迟迟拿不到锁,程序就卡住了。
在ObjectMonitor.cpp里,有几个关键属性,可以记录锁相关的数据。
属性 | 作用 |
---|---|
_owner | 指向持有ObjectMonitor对象的线程 |
_WaitSet | 存放处于wait状态的线程队列 |
_EntryList | 存放处于等待锁block状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 用来记录该线程获取锁的次数 |
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。当执行monitorenterl时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生的原因:
public class LockDemo {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
new Thread(() -> {
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + "持有object1的锁,尝试获取object2的锁");
try {
Thread.sleep(1);// 让thread2可以start起来
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + "成功获取object2的锁");
}
}
}, "thraed1").start();
new Thread(() -> {
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + "持有object2的锁,尝试获取object1的锁");
try {
Thread.sleep(1);// 让thread1可以start起来
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + "成功获取object1的锁");
}
}
}, "thraed2").start();
}
}
在Terminal里输入jps -l
查看当前运行的Java线程,输入jstack 进程编号
查看具体信息,可以看到最后一行有一个Found 1 deadlock
,说明发生了死锁。
还有一种方式:jconsole,在控制台输入jconsole
,回车,选择自己的进程,点击连接,选择线程标签,点击左下角“检测死锁”,会出现一个新的“死锁”标签,里面就有具体的信息了。
详见JUC并发编程与源码分析笔记14-ReentrantLock、ReentrantReadWriteLock、StampedLock讲解。
详见JUC并发编程与源码分析笔记14-ReentrantLock、ReentrantReadWriteLock、StampedLock讲解。