例如,下面模拟这样一个场景:一个售票处有3个售票员,出售20张票。
public class SellTickets {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
Thread t1 = new Thread(seller, "窗口1");
Thread t2 = new Thread(seller, "窗口2");
Thread t3 = new Thread(seller, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketSeller extends Thread {
private static int tickets = 20;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
运行后,发现会出现多个售票员出售同一张票的现象:
为了解决线程安全的问题,Java提供了多种同步锁。
synchronized的底层是使用操作系统的mutex lock实现的。下面先了解一些相关的概念。
锁的内存语义:
锁释放和锁获取的内存语义:
Mutex Lock
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中。
synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述。
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode 或锁信息 |
32/64bit | Class Metadata Address | 存储对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Mark Word
Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
Monitor
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
synchronized是Java中的关键字,是一种同步锁,它修饰的对象有以下几种:
序号 | 类别 | 作用范围 | 作用对象 |
---|---|---|---|
1 | 同步代码块 | 被synchronized修饰的代码块 | 调用这个代码块的单个对象 |
2 | 同步方法 | 被synchronized修饰的方法 | 调用该方法的单个对象 |
3 | 同步静态方法 | 被synchronized修饰的静态方法 | 静态方法所属类的所有对象 |
4 | 同步类 | 被synchronized修饰的代码块 | 该类的所有对象 |
同步代码块就是将需要的同步的代码使用同步锁包裹起来,这样能减少阻塞,提高程序效率。
同步代码块格式如下:
synchronized(对象){
同步代码;
}
同样对于文章开头卖票的例子,进行线程安全改造,代码如下:
public class SellTickets {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
Thread t1 = new Thread(seller, "窗口1");
Thread t2 = new Thread(seller, "窗口2");
Thread t3 = new Thread(seller, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketSeller implements Runnable {
private static int tickets = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
try {
Thread.sleep(10);
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
同步代码块的关键在于锁对象,多个线程必须持有同一把锁,才会实现互斥性。
将上面代码中的 synchronized (this) 改为 synchronized (new Objcet()) 的话,线程安全将得不到保证,因为两个线程的持锁对象不再是同一个。
又比如下面这个例子:
public class SyncTest implements Runnable {
// 共享资源变量
int count = 0;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// test1();
test2();
}
public static void test1() {
SyncTest syncTest1 = new SyncTest();
Thread thread1 = new Thread(syncTest1, "thread-1");
Thread thread2 = new Thread(syncTest1, "thread-2");
thread1.start();
thread2.start();
}
public static void test2() {
SyncTest syncTest1 = new SyncTest();
SyncTest syncTest2 = new SyncTest();
Thread thread1 = new Thread(syncTest1, "thread-1");
Thread thread2 = new Thread(syncTest2, "thread-2");
thread1.start();
thread2.start();
}
}
从输出结果可以看出,test2() 方法无法实现线程安全,原因在于我们指定锁为this,指的就是调用这个方法的实例对象,然而 test2() 实例化了两个不同的实例对象 syncTest1,syncTest2,所以会有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行 run() 方法,造成线程不安全。
如果要使用这个经济实惠的锁并保证线程安全,那就不能创建出多个不同实例对象。如果非要想 new 两个不同对象出来,又想保证线程同步的话,那么 synchronized 后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步了。
synchronized(xxxx.class){
//todo
}
一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞。
例如下面的例子:
public class SyncTest {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(counter, "线程-1");
Thread thread2 = new Thread(counter, "线程-2");
thread1.start();
thread2.start();
}
}
class Counter implements Runnable {
private int count = 0;
public void countAdd() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + " 同步计数:" + (count++));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void printCount() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + " 非同步输出:" + count);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("线程-1")) {
countAdd();
} else if (threadName.equals("线程-2")) {
printCount();
}
}
}
我们也可以用synchronized 给对象加锁。这时,当一个线程访问该对象时,其他试图访问此对象的线程将会阻塞,直到该线程访问对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码,,例如下例:
public class SyncTest {
public static void main(String args[]) {
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threads[i] = new Thread(accountOperator, "Thread-" + i);
threads[i].start();
}
}
}
class Account {
String name;
double amount;
public Account(String name, double amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(double amt) {
amount += amt;
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(double amt) {
amount -= amt;
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public double getBalance() {
return amount;
}
}
class AccountOperator implements Runnable {
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
public void run() {
synchronized (account) {
String name = Thread.currentThread().getName();
account.deposit(500);
System.out.println(name + "存入500,最新余额:" + account.getBalance());
account.withdraw(400);
System.out.println(name + "取出400,最新余额:" + account.getBalance());
System.out.println(name + "最终余额:" + account.getBalance());
}
}
}
同步锁可以使用任意对象作为锁,当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
class Test implements Runnable {
private byte[] lock = new byte[0]; // 特殊的instance变量
public void method() {
synchronized(lock) {
// todo 同步代码块
}
}
public void run() {
}
}
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
public synchronized void method(){
// todo
}
下面用同步函数的方式解决售票场景的线程安全问题,代码如下:
public class SellTickets {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
Thread t1 = new Thread(seller, "窗口1");
Thread t2 = new Thread(seller, "窗口2");
Thread t3 = new Thread(seller, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class TicketSeller implements Runnable {
private static int tickets = 100;
@Override
public void run() {
while (true) {
sellTickets();
}
}
public synchronized void sellTickets() {
try {
Thread.sleep(10);
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同步方法有以下特征:
Synchronized也可修饰一个静态方法,静态方法是不属于当前实例的,而是属性类的,那么这个锁就是类的class对象锁。同步静态方法可以解决同步方法和同步代码块中的一个问题:new 两个对象的话,等于有两把锁,无法保证线程安全。
public class SyncTest {
public static void main(String args[]) {
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "Thread-1");
Thread thread2 = new Thread(syncThread2, "Thread-2");
thread1.start();
thread2.start();
}
}
class SyncThread implements Runnable {
private static int count = 0;
public synchronized static void method() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void run() {
method();
}
}
syncThread1 和 syncThread2 是 SyncThread 的两个对象,但在 thread1 和 thread2 并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。
Synchronized还可作用于一个类,用法如下:
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
同步类与同步静态方法有相同的效果,该类的所有对象都是持有同一把锁:
public class SyncTest {
public static void main(String args[]) {
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "Thread-1");
Thread thread2 = new Thread(syncThread2, "Thread-2");
thread1.start();
thread2.start();
}
}
class SyncThread implements Runnable {
private static int count = 0;
public void method() {
synchronized (SyncThread.class) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void run() {
method();
}
}