Java——Synchronized/ReentrantLock

1. 引入

Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突。

举个例子:假设有一个卖票系统,一共有100张票,有4个窗口同时卖。

public class Ticket implements Runnable {
	// 当前拥有的票数
	private int num = 100;
	public void run() {
		while (true) {
			if (num > 0) {
				try {	
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
				// 输出卖票信息
				System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
			}
		}
	}
}
public class Nothing {
	public static void main(String[] args) {
		Ticket t = new Ticket();//创建一个线程任务对象。
		//创建4个线程同时卖票
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

结果:

Thread-1.....sale....2
Thread-0.....sale....3
Thread-2.....sale....1
Thread-0.....sale....0
Thread-1.....sale....0
Thread-3.....sale....1

这就是多线程情况下,出现了数据“脏读”情况。即多个线程访问余票num时,当一个线程获得余票的数量,要在此基础上进行-1的操作之前,其他线程可能已经卖出多张票,导致获得的num不是最新的,然后-1后更新的数据就会有误。这就需要线程同步的实现了。

解决方案

加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。一共有两种锁,来实现线程同步问题,分别是: synchronized 和 ReentrantLock

2. synchronized关键字

  • synchronized实现同步的基础:Java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。
  • Synchronzied实现同步的表现形式分为:代码块同步 和 方法同步。

synchronized原理

JVM基于进入和退出 Monitor 对象来实现 代码块同步 和 方法同步 ,两者实现细节不同。

  • 代码块同步: 在编译后通过将 monitorenter 指令插入到同步代码块的开始处,将 monitorexit 指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个 monitor 与之关联,线程执行 monitorenter 指令时,会尝试获取对象对应的 monitor 的所有权,即尝试获得对象的锁。
  • 方法同步: synchronized方法在 method_info结构 有 ACC_synchronized 标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。
    在这里插入图片描述
    synchronized的使用场景
  • 1.方法同步:
    锁住的是该对象,类的其中一个实例,当该对象(仅仅是这一个对象)在不同线程中执行这个同步方法时,线程之间会形成互斥。达到同步效果,但如果不同线程同时对该类的不同对象执行这个同步方法时,则线程之间不会形成互斥,因为他们拥有的是不同的锁。
	public synchronized void method1
  • 2.静态方法同步:
    锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果。
	public synchronized static void method3
  • 3.代码块同步:描述同1
	synchronized(this){ //TODO }
  • 4.代码块同步:描述同3
	synchronized(Test.class){ //TODO}
  • 5.代码块同步:
    这里面的o可以是一个任何Object对象或数组,并不一定是它本身对象或者类,谁拥有o这个锁,谁就能够操作该块程序代码。
	synchronized(o) {}

解决线程同步的实例:

public class Ticket implements Runnable {
	// 当前拥有的票数
	private int num = 100;
	public void run() {
		while (true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
			}
			synchronized (this) {
				// 输出卖票信息
				if (num > 0) {
					System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
				}
			}
		}
	}
}

结果:

Thread-2.....sale....10
Thread-1.....sale....9
Thread-3.....sale....8
Thread-0.....sale....7
Thread-2.....sale....6
Thread-1.....sale....5
Thread-2.....sale....4
Thread-1.....sale....3
Thread-3.....sale....2
Thread-0.....sale....1

实现了线程同步。同时改了一下逻辑,在进入到同步代码块时,先判断现在是否有没有票,然后再买票,防止出现没票还要售出的情况。通过同步代码块实现了线程同步,其他方法也一样可以实现该效果。

3. ReentrantLock锁

ReentrantLock,一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。(重入锁后面介绍)

Lock接口:
Lock,锁对象,在Java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁,后面我们会析)。

在Lock接口出现之前,Java程序是靠 synchronized 关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了 Lock 接口用来实现锁的功能,它提供了与 synchronized 关键字类似的同步功能只是在使用时需要显式地获取和释放锁,缺点就是缺少像 synchronized 那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性.

Lock接口的主要方法:

  • void lock(): 执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
  • boolean tryLock(): 如果锁可用,则获取锁,并立即返回true,否则返回false. 该方法和lock()的区别在于,tryLock()只是"试图"获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行.
  • void unlock(): 执行此方法时,当前线程将释放持有的锁. 锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.
  • Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。

ReentrantLock的使用
关于ReentrantLock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。

	ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
	.....................
	lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
	try {
		//操作
	} finally {
		lock.unlock(); //释放锁
	}

解决线程同步的实例:

public class Ticket implements Runnable {
	// 当前拥有的票数
	private int num = 100;
	ReentrantLock lock = new ReentrantLock();
	public void run() {
		while (true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
			}
			lock.lock();
			// 输出卖票信息
			if (num > 0) {
				System.out.println(Thread.currentThread().getName() + ".....sale...." + num--);
			}
			lock.unlock();
			}
	}
}

4. 重入锁

就是一个线程在获取了锁之后,再次去获取了同一个锁。

具体概念就是:自己可以再次获取自己的内部锁。

Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。

public class SynchronizedTest {
	public void method1() {
		synchronized (SynchronizedTest.class) {
			System.out.println("方法1获得ReentrantTest的锁运行了");
			method2();
		}
	}
	public void method2() {
		synchronized (SynchronizedTest.class) {
			System.out.println("方法1里面调用的方法2重入锁,也正常运行了");
		}
	}
	public static void main(String[] args) {
		new SynchronizedTest().method1();
	}
}

上面便是synchronized的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。

public class ReentrantLockTest {
	private Lock lock = new ReentrantLock();
	public void method1() {
		lock.lock();
		try {
			System.out.println("方法1获得ReentrantLock锁运行了");
			method2();
		} finally {
			lock.unlock();
		}
	}
	public void method2() {
		lock.lock();
		try {
			System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了");
		} finally {
			lock.unlock();
		}
	}
	public static void main(String[] args) {
		new ReentrantLockTest().method1();
	}
}

上面便是ReentrantLock的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时, 由于本身已经具有该锁,所以可以再次获取。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值