java 中线程同步在多线程使用比较常见,一般也是用 synchronized 关键字来做同步,简单实用,下面直接给个例子说明一下。
一般大家也会用这个来举例,很常见也便于理解。
1. 首先创建一个Ticket 类用于表述车票,如下:
/**
* 车票类
*/
public class Ticket {
private Integer ticketNum;
Ticket(int ticketNum) {
this.ticketNum = ticketNum;
}
// synchronized 这里先注销这个同步关键字
void buyTicket(int amount) {
System.out.println("当前可售票数: " + ticketNum);
if (ticketNum < amount) {
System.out.println("目前剩余票数" + ticketNum + ", 购票" + amount + ", 可售票数不足");
} else {
ticketNum = ticketNum - amount;
System.out.println("购票" + amount + ", 剩余票数" + ticketNum);
if (ticketNum <= 0) {
System.out.println("票售罄");
}
}
}
}
2. 创建一个客户端类 BuyTicketClient 用于提供买票服务
/**
* 购票服务端
*/
public class BuyTicketClient implements Runnable {
Ticket ticket;
int amount;
BuyTicketClient(Ticket ticket, int amount) {
this.ticket = ticket;
this.amount = amount;
}
public void run() {
ticket.buyTicket(amount);
}
}
3. 模拟多人购票
/**
* 模拟多人购票
*/
public class Buy {
public static void main(String[] args) {
Ticket ticket = new Ticket(5);
BuyTicketClient buyTicketClient_1 = new BuyTicketClient(ticket, 2);
BuyTicketClient buyTicketClient_2 = new BuyTicketClient(ticket, 1);
BuyTicketClient buyTicketClient_3 = new BuyTicketClient(ticket, 3);
Thread thread_3 = new Thread(buyTicketClient_3);
Thread thread_2 = new Thread(buyTicketClient_2);
Thread thread_1 = new Thread(buyTicketClient_1);
thread_3.start();
thread_2.start();
thread_1.start();
}
}
运行看一下结果(每次运行结果可能都不同):
当前可售票数: 5
当前可售票数: 5
当前可售票数: 2
目前剩余票数0, 购票1, 可售票数不足
购票2, 剩余票数0
购票3, 剩余票数2
票售罄
票售罄
会发现输出结果完全对不上,主要原因是多线程执行顺序不可控,票数量(ticketNum字段)修改是混乱的。
4. 加 synchronized 字段做同步
步骤1代码中buyTicket方法加 synchronized字段,如下
/**
* 车票类
*/
public class Ticket {
private Integer ticketNum;
Ticket(int ticketNum) {
this.ticketNum = ticketNum;
}
synchronized void buyTicket(int amount) {
System.out.println("当前可售票数: " + ticketNum);
if (ticketNum < amount) {
System.out.println("目前剩余票数" + ticketNum + ", 购票" + amount + ", 可售票数不足");
} else {
ticketNum = ticketNum - amount;
System.out.println("购票" + amount + ", 剩余票数" + ticketNum);
if (ticketNum <= 0) {
System.out.println("票售罄");
}
}
}
}
再次运行步骤3的代码,结果如下(每次运行结果可能不同):
当前可售票数: 5
购票3, 剩余票数2
当前可售票数: 2
购票1, 剩余票数1
当前可售票数: 1
目前剩余票数1, 购票2, 可售票数不足
显示结果是合理的。
synchronized 一般涉及三种使用场景
1. 修饰实例方法,作用于当前对象实例,进入同步代码前要获得当前实例的锁(上述买票例子就是对实例方法 buyTicket() 加锁)
另外:一个线程正在访问一个对象实例的 synchronized 实例方法,那么其他线程不能访问该对象实例的其他 synchronized 方法,毕竟一个对象实例只有一把锁,当一个线程获取了该对象实例的锁之后,其他线程无法获取该对象实例的锁,所以无法访问该对象实例的其他synchronized实例方法
2. 修饰代码块,作用于对象实例,进入同步代码前要获得当前实例的锁,上述 Ticket 类改一下就是,如下:
public class Ticket {
private Integer ticketNum;
Ticket(int ticketNum) {
this.ticketNum = ticketNum;
}
void buyTicket(int amount) {
synchronized (ticketNum) {
System.out.println("当前可售票数: " + ticketNum);
if (ticketNum < amount) {
System.out.println("目前剩余票数" + ticketNum + ", 购票" + amount + ", 可售票数不足");
} else {
ticketNum = ticketNum - amount;
System.out.println("购票" + amount + ", 剩余票数" + ticketNum);
if (ticketNum <= 0) {
System.out.println("票售罄");
}
}
}
}
}
上述代码是对 Integer 类型的 ticketNum对象实例加锁,大多时候推荐使用 this 替换 ticketNum,this 表示对当前实例加锁(到这里和实例方法加锁是一样的含义)
同时,还可以把 ticketNum 换成 Ticket.class(类锁),这个在下面细说。
3. 修饰静态方法,作用当前类,进入同步代码库前要获得给定类的锁(就是2中说的类锁)
这个很有意思,举个例子说明一下和实例方法加锁的区别
public class SynTwoMethod implements Runnable {
static int i = 0;
public synchronized void increase() {
i++;
}
public static synchronized void increaseStatic() {
i++;
}
public synchronized void increaseClass() {
synchronized (SynTwoMethod.class) {
i++;
}
}
public void run() {
for (int j = 0; j < 100000; j++) {
increase();
// increaseStatic();
// increaseClass();
}
}
}
//测试
public class SynTwoMethodTest {
public static void main(String[] args) throws InterruptedException {
SynTwoMethod synTwoMethod_1 = new SynTwoMethod();
SynTwoMethod synTwoMethod_2 = new SynTwoMethod();
Thread thread_1 = new Thread(synTwoMethod_1);
Thread thread_2 = new Thread(synTwoMethod_2);
thread_1.start();
thread_2.start();
thread_1.join();
thread_2.join();
System.out.println(synTwoMethod_1.i);
}
}
increase() 方法是实例方法加锁,increaseStatic() 是静态方法加锁,increaseClass() 是通过代码块对对象加锁;测试方法生成了同一对象的不同实例;在run() 方法里分别执行三种方法会发现:
increase 得到的结果是小于 200000,increaseStatic 和 increaseClass 得到的结果都是 200000。
结论:存在类锁的类,无论生成多少个实例,在执行加锁方法时都会单一执行;而实例加锁只对同一个实例有效。