一、线程同步
1、问题
线程同步,解决多线程访问临界资源时的数据安全问题 。
我们使用多线程实现一个需求:四个窗口共同卖100张票。
线程类(票类Ticket_1):
package basis.StuThread.Ticket_1;
import java.util.concurrent.TimeUnit;
public class Ticket_1 implements Runnable{
private static int tickets = 100;
@Override
public void run() {
while (true){
if(tickets>0){
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出第"+tickets+"张票");
tickets--;
}else {
break;
}
}
}
}
每要卖一张票,线程休眠100毫秒。
主类(测试类):
package basis.StuThread.Ticket_1;
public class TestTicket_1 {
public static void main(String[] args) {
Ticket_1 ticket = new Ticket_1();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
测试结果(部分):
窗口1卖出第100张票
窗口2卖出第99张票
窗口4卖出第99张票
窗口3卖出第99张票
窗口1卖出第96张票
窗口4卖出第95张票
窗口3卖出第94张票
窗口2卖出第95张票
问题:
我们发现很多张票被不同的窗口重复卖了多次,这就是多线程访问共享资源(又叫临界资源,这里指100张票)可能带来的安全问题。那么如何解决?为访问共享资源的代码块添加互斥锁。为访问共享资源的代码添加互斥锁有两种方式:Synchronized 关键字 和 Lock 接口。
2、Synchronized 关键字的使用
synchronized 关键字最主要有以下三种应用方式,下面分别介绍
- 修饰实例方法:作用于当前实例加锁,进入同步代码前需要获取当前实例的锁;
- 修饰静态方法:作用于当前类对象加锁,进入同步代码前需要获得当前类对象的锁;
- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前需要获得给定对象的锁;
(1)synchronized作用于实例方法:
所谓的实例对象锁就是用 synchronized 修饰实例对象中的实例方法,注意是实例方法不包括静态方法。改造卖票程序的Ticket_1类如下:
package basis.StuThread.Ticket_1;
import java.util.concurrent.TimeUnit;
public class Ticket_1 implements Runnable{
private static int tickets = 100;
@Override
public void run() {
while (sellTicket()){
}
}
//买票方法
public synchronized boolean sellTicket(){
if (tickets > 0) {
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
tickets--;
return true;
} else {
return false;
}
}
}
主类不变:
package basis.StuThread.Ticket_1;
public class TestTicket_1 {
public static void main(String[] args) {
Ticket_1 ticket = new Ticket_1();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
运行结果:
窗口1卖了第100张票。
窗口1卖了第99张票。
窗口4卖了第98张票。
窗口4卖了第97张票。
...
窗口4卖了第58张票。
窗口4卖了第57张票。
窗口3卖了第56张票。
窗口3卖了第55张票。
窗口3卖了第54张票。
上述代码中:
我们把买票的具体过程抽出来,写进一个实例方法,该方法使用synchronized 关键字进行修饰,即为当前的实例方法添加一把互斥锁,保证同时只能一个线程进入该方法进行卖票。这样就解决了同一张票被多个线程卖了多次的情况,即解决了线程安全问题。
注意:
synchronized 修饰的是实例方法 sellTicket,这样情况下当前的线程锁便是实例对象 ticket,而程序中Ticket_1的实例对象只有一个 即 ticket,当一个线程获取到该锁时,其他线程就不能再获取到该锁,也就是不能访问被synchronized 关键字修饰的方法(但是可以访问其他的 非synchronized 修饰的方法),所以保证了线程安全。
Java中线程同步锁可以是任意对象。
当一个线程类拥有两个或以上个实例对象时,两个或多个线程持有不同的 实例对象(即锁),同时访问 synchronized 修饰的实例方法这是允许的,因为多个线程持有的是不同的锁。演示如下:
Ticket_1代码不变。
主类代码修改如下:
package basis.StuThread.Ticket_1;
public class TestTicket_1 {
public static void main(String[] args) {
//Ticket_1 ticket = new Ticket_1();
new Thread(new Ticket_1(),"窗口1").start();
new Thread(new Ticket_1(),"窗口2").start();
new Thread(new Ticket_1(),"窗口3").start();
new Thread(new Ticket_1(),"窗口4").start();
}
}
测试结果:
窗口4卖了第100张票。
窗口2卖了第100张票。
窗口3卖了第99张票。
窗口2卖了第99张票。
窗口4卖了第99张票。
窗口1卖了第99张票。
窗口3卖了第98张票。
窗口1卖了第98张票。
我们发现此时又出现了线程安全问题。原因就是多个线程持有的是不同的锁,线程之间不构成互斥关系。
解决办法,将 synchronized 作用于静态的 increase 方法。这样对象锁就是当前类对象(Ticker_1.class),这样无论创建多少个实例对象,但由于类对象只有一个,所以对象锁也就只有唯一的一个,下面就看看如何使用 synchronized 修饰静态方法。
(2)synchronized作用于静态方法
当 synchronized 作用于静态方法时,其锁就是当前类的 class 对象锁。由于静态成员是类成员,不属于任何一个实例对象,因此通过 class 对象锁可以控制静态成员的并发操作。演示如下:
我们修改Ticket_1类,为卖票方法 sellTicket() 添加 static 关键字
主类不变:
package basis.StuThread.Ticket_1;
public class TestTicket_1 {
public static void main(String[] args) {
//Ticket_1 ticket = new Ticket_1();
new Thread(new Ticket_1(),"窗口1").start();
new Thread(new Ticket_1(),"窗口2").start();
new Thread(new Ticket_1(),"窗口3").start();
new Thread(new Ticket_1(),"窗口4").start();
}
}
结果:
窗口1卖了第100张票。
窗口4卖了第99张票。
窗口4卖了第98张票。
窗口4卖了第97张票。
窗口4卖了第96张票。
...
窗口4卖了第42张票。
窗口4卖了第41张票。
窗口3卖了第40张票。
窗口3卖了第39张票。
窗口3卖了第38张票
由于静态方法是类持有的,不属于任何对象,所以无论我们new 出来多少个对象,所有线程都用的是同一把锁(即 Ticket_1.class),所以不会出现线程安全问题。
需要注意的是:
一个线程 A 访问一个对象的非static 的 synchronized 方法,而线程 B 需要访问这个实例对象的静态 synchronized 方法,这样式允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的是当前类的 class 对象锁,而访问非静态 synchronized 方法占用的是实例对象锁。此时,当两个方法都操作共享资源时,会出现线程安全问题。
(3)synchronized作用于同步代码块
除了使用关键字 synchronized 修饰实例方法和静态方法外,还可以修饰同步代码块。在某些情况下,方法体较大,操作比较耗时,而需要同步的代码又只有一小部分,此时,可以使用同步代码块的方式对需要同步的代码进行包裹。示例如下:
修改票类(Ticket_1):
package basis.StuThread.Ticket_1;
import java.util.concurrent.TimeUnit;
public class Ticket_1 implements Runnable{
private static int tickets = 100;
//创建一个对象(可以是任意类型),作为同步代码块的锁。
Object lock = new Object();
@Override
public void run() {
while (true){
//同步代码块
synchronized(lock) {
if (tickets > 0) {
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
tickets--;
} else {
break;
}
}
}
}
}
测试类:
package basis.StuThread.Ticket_1;
public class TestTicket_1 {
public static void main(String[] args) {
Ticket_1 ticket = new Ticket_1();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
从代码上看,synchronized 作用于一个给定的实例对象 lock ,即当前实例对象就是锁对象,每次当线程进入 synchronized 包裹的代码块时就会请求当前线程持有的 lock 实例对象锁,如果当前有其他线程正在持有该对象锁,那么新到的线程就必须等待,从而保证线程安全。
当然,除了 lock 作为对象锁外,我们还可以使用 this 对象(代表当前实例)或者当前类的 class 对象作为锁,代码如下:
//this,当前实例对象锁
synchronized(this){
}
//class对象锁
synchronized(AccountingSync.class){
}
3、Lock接口
在Java多线程编程中,我们经常使用synchronized关键字来实现同步,控制多线程对变量的访问,来避免并发问题。但是有的时候,synchronized关键字会显得过于沉重,不够灵活。synchronized
方法或代码块的使用,提供了对与每个对象相关的隐式监视器锁(monitor)的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
这个时候Lock出现。
Lock不是Java中的关键字而是 java.util.concurrent.locks 包中的一个接口。下面我们简单介绍一下Lock接口
(1)Lock接口简介
Lock
实现 提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition
对象。Lock相对于synchronized关键字而言更加灵活,你可以自由得选择我你想要加锁的地方。
(2)接口声明:
package java.util.concurrent.locks;
public interface Lock {}
Lock接口位于JUC包的中的locks包。
(3)Lock接口中定义的方法
方法 | 描述 |
void lock(): | 获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。 |
void lockInterruptibly(): | 如果当前线程未被中断,则获取锁。 |
Condition newCondition(): | 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean tryLock(): | 仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true 。如果锁不可用,则此方法将立即返回值 false 。 |
boolean tryLock(long time, TimeUnit unit): | 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 |
void unlock(): | 释放锁。在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前重新获取锁。 |
注意:我们通常在 try…catch 模块中使用锁,在 finally 模块中释放锁。
(4)Lock接口的实现类
Lock接口有三个实现类,分别是 ReentrantLock 、ReentrantReadWriteLock.ReadLock 、ReentrantReadWriteLock.WriteLock。后面两个是内部类。
接下来我们重点介绍使用最多的 ReentrantLock。
4、ReentrantLock
ReentrantLock是一种可重入且独占式的锁,它具有与使用 synchronized 监视器锁相同的基本行为和语义,但与 synchronized 关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。
ReentrantLock,顾名思义,它是支持可重入的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。
(1)继承关系和类声明
java.lang.Object
|____java.util.concurrent.locks.ReentrantLock
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
}
(2)构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock 类有两个构造方法,一个是无参的构造,默认使用非公平锁;一个是接受一个boolean 类型的参数,如果为 true 就实例化一个公平锁,如果为 false 就实例化非公平锁。
(3)使用方法
还使用上面讲到的多个窗口共同卖一百张票的程序:
Ticket_1类:
package basis.StuThread.Ticket_2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket_1 implements Runnable{
private static int tickets = 100;
//实例化一个可重入锁对象
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//同步代码块
try {
//线程休眠100毫秒
TimeUnit.MICROSECONDS.sleep(100);
//加锁
lock.lock();
if (tickets <1) {
break;
}
System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
tickets--;
}catch (Exception e){
e.printStackTrace();
}
finally {
//解锁
lock.unlock();
}
}
System.out.println("票以卖完,线程"+Thread.currentThread().getName()+"退出。");
}
}
注意:
此处解锁语句 lock.unlock(); 一定要放在同步代码块中执行。否者会造成一个线程获取锁后,通过break语句退出循环,不经过释放锁就直接结束线程,导致其他线程不能获取锁,从而结束自己的运行。
或者不使用try-catch语句,在break;之前再释放一次锁:
if (tickets <1) {
lock.unlock();
break;
}
测试类:
package basis.StuThread.Ticket_2;
public class TestTicket_1 {
public static void main(String[] args) {
Ticket_1 ticket = new Ticket_1();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}