Java学习笔记-synchronized&lock
举例:卖票行为,当我们不使用锁来进行卖票行为时,我们会发现得到得结果并不是我们想要得结果
public class SellTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"A").start();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"B").start();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"C").start();
}
}
class Ticket{
private int num = 30;
public void sell(){
if (num > 0)
System.out.println(Thread.currentThread().getName() + "买到了第" + (num --) + "张");
}
这个时候我们会发现出现了有多个线程同时操作一个对象的情况,那么这种情况并不是我们所希望出现的情况,我们希望在进行卖票时,这张票只能卖一次,而且卖出去就只属于一个人,而不能出现两个人同时购买到了一张票,那么这时候我们就需要对这个卖票的行为进行一些操作
public class SellTicketSyn {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sell();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sell();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sell();
}
},"C").start();
}
}
class Ticket{
private int num = 30;
public synchronized void sell(){
if (num > 0){
System.out.println(Thread.currentThread().getName() + "买到了第" + (num --) + "张");
}
}
}
在上述的卖票中,我们对Ticket类卖票的这个方法加上了synchronized关键字,此时,这个方法就已经被上了锁,也即当某个线程正在进行这个操作时,即获取了这个对象的锁,这个对象一般都是共享资源变量,为了防止多个线程同时操作一个共享资源变量引起冲突而有的操作,当某个线程获取到这个对象的锁之后,其他竞争锁的线程就会进入等待队列中,直至该线程释放锁,则所有处于等待队列的线程继续竞争该锁。
synchronized
那么,synchronized到底是什么呢?
- synchronized是Java提供的原子性内置锁,这种内置的而且使用者看不到的锁也被称为监视器锁
- 使用synchronized后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,依赖操作系统底层互斥锁实现
- 主要实现原子性操作和解决共享变量的内存可见性问题
当执行monitorenter指令时会尝试获取对象锁,如果该对象没有被锁定或是已经获得了锁,那么锁的计数器就会+1,此时,所有竞争锁的线程就会进入等待队列中。
而当执行monitorexit指令时会将计数器-1,当计数器为0时,则锁释放,此时处于等待队列中的线程则重新开始竞争锁。
synchronized是排它锁,也即如果当一个线程获得锁时,其他线程必须等待该线程释放锁才能获得锁,而又因为Java线程与操作系统中原生线程一一对应,线程被唤醒或是阻塞时都会从用户态进到内核态,这种转换是十分消耗性能的。
那么,什么是内存可见性呢?
从内存语义上看,加锁的过程会清除工作内存中的共享变量,并从主内存中读入共享变量,释放锁的过程就是将工作内存中的共享变量写回进主内存中。所以,线程对所有变量的操作都是在工作内存中进行,线程间无法直接互相访问,变量传递需要通过主存完成。那么很有可能就会出现一个问题:线程A从主存中获取了共享变量n并将其修改为1放回主存,此时线程B也从主存获取了共享变量n将其修改为2,但线程A再去获取时,共享变量n仍然是1,这就是共享变量的内存不可见问题,也即线程B写入的值对线程A不可见
synchronized应用方式
- 修饰实例方法,也就是对该实例对象加锁,进入同步代码块前要获取该实例对象的锁
- 修饰静态方法,也就是对当前类对象加锁,进入同步代码块前要获取该类对象的锁
- 修饰代码块,给指定对象加锁,进入同步代码块前要获取该对象的锁
实现上述方式对卖票加锁还有另一种方式:
public class SellTicketLock {
public static void main(String[] args) {
Ticket2 ticket2 = new Ticket2();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"A").start();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"B").start();
new Thread(()->{for(int i = 0; i < 40; i ++) ticket2.sell();},"C").start();
}
}
class Ticket2{
private int num = 30;
private Lock lock = new ReentrantLock();
public void sell(){
// 显示上锁
lock.lock();
try {
if (num > 0)
System.out.println(Thread.currentThread().getName() + "买到了第" + (num --) + "张");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 必须要手动释放锁,否则死锁
lock.unlock();
}
}
}
synchronized与lock区别
- synchronized是内置的Java关键字,而lock是一个类
- synchronized无法判断获取锁的状态,但lock可以判断是否获取了锁
- synchronized可以自动释放锁,lock必须手动释放锁,不然就会造成死锁
- synchronized不可中断,lock中的trylock是可以被中断的
- synchronized和lock都是可重入锁,synchronized是非公平锁,而lock可以通过修改构造方法中参数设置为公平锁
- synchronized适合锁少量代码同步问题,lock适合锁大量的代码