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()方法时, 由于本身已经具有该锁,所以可以再次获取。