Synchronized关键字
一、线程同步
线程同步:当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程使用;
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候就需要用到线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用;
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制Synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;
- 在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题;
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题;
可以通过private关键字来保证数据对象只能被访问,所以只需要针对方法提出一套机制,这套机制就是Synchronized关键字,
synchronized方法控制对成员变量或者类变量对象的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获取该锁,重新进入可执行状态;
二、Synchronized
Synchronized关键字是线程同步使用的关键字,在多线程并发编程中synchronized一直是元老级角色,很多人会称呼它为重量级锁,但是随着JavaSE1.6对synchronized进行了各种优化之后,有些情况它就并不那么重了。
1. 从语法上讲,Synchronized总共有三种用法:
1) 修饰同步代码块
-
作用范围:代码块 ;
-
锁是Synchonized括号里配置的对象
obj
;
public static void SynObj() {
Object obj = new Object();
synchronized (obj) {
//todo
}
}
2) 修饰普通同步方法
-
作用于对象;
-
锁是当前实例对象;
public synchronized void comMethod() {
//todo
}
3)修饰静态方法
-
作用于当前类;
-
锁是当前类的Class对象;
public synchronized static void staMethod() {
//todo
}
2. 对象监视器
synchronized允许使用任何一个对象作为同步的内容,因此任意一个对象都应该拥有自己的监视器(Monitor),同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,未获得监视器的线程将会被阻塞在同步块或同步方法的入口处,进入阻塞状态,
- 推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,即对象本身;
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码;
- 第二个线程访问,发现同步监视器被锁定,无法访问;
- 第一个线程访问完毕,解锁同步监视器;
- 第二个线程访问,发现同步监视器未锁,锁定并访问;
3. synchronized实现原理
synchronized与其他锁不同,它是内置在JVM中的,从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
- Synchronize修饰代码块,底层提供monitorEnter和monitorExit来达到获取锁和释放锁的过程;
- Synchronize修饰普通方法、静态方法,不是通过monitorEnter和monitorExit机制来处理,而是flags上有ACC_SYNCHRONIZED的标识;
- 底层都是通过获取monitor对象来获取锁,monitor对象是由操作系统提供的mutex锁机制来完成线程获取对象和释放对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
方法级的同步是隐式的, 即无须通过字节码指令来控制, 它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时, 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置, 如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
下图是对象、对象监视器、同步队列和执行线程之间的关系
三、举例
在前面用多线程简单实现网上购票的例子:
class UnSafeWeb12306 implements Runnable {
private int ticketNums = 10; //给十张票
private boolean flag = true; //设置标识位
@Override
public void run() {
while (flag) {
test();
}
}
public void test() {
if (ticketNums < 0) { //在票数小于0时,将标识位置为false,return
flag = false;
return;
}
try {
Thread.sleep(200); //模拟网络延时
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印线程名及票号
System.out.println(Thread.currentThread().getName() + "-->" +ticketNums--);
}
}
public class Test12306 {
public static void main(String[] args) {
//一份资源
UnSafeWeb12306 uw = new UnSafeWeb12306();
//多个代理
new Thread(uw,"张三").start();
new Thread(uw,"李四").start();
new Thread(uw,"王五").start();
}
}
运行结果跟预期是不符的,在代码中可以看到,当票数ticketNums小于0时将标识位置为false后程序应该结束,但运行结果中出现了负数;
出现负数的原因是:在只剩下一张票的情况下,假设ABC都在sleep,而A先获得时间片,拿到了最后一张票,此时ticketNums为0,然后C获得时间片,拿到0,最后B拿到的就是-1;
其实除了出现负数还可能会出现相同的情况,这里的原因就是一个线程对ticketNums做了修改之后没有立刻将修改后的值写回主内存,而其他线程的工作内存中还是为改变之前的副本,所以会出现这样的情况;
优化:
- 同步方法
public synchronized void test1() {
if (ticketNums < 0) {
flag = false;
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}
- 同步块
public void test2() { //锁定范围较大则效率底下
synchronized (this) {
if (ticketNums < 0) {
flag = false;
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}
}
- 对ticketNums加锁,经过测试这种方法依然不安全,因为ticketNums对象一直在改变,而synchronized需要锁一个不变的地址;
public void test3() { //线程不安全
synchronized ((Integer)ticketNums) {
if (ticketNums < 0) {
flag = false;
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}
}
- 只对局部加锁也是可以的;
public void test4() {
synchronized (this) {
if (ticketNums < 0) {
flag = false;
return;
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}
- 尽可能锁定合理的范围,不是指代码,是指数据的完整性;
//双重检查
public void test5() {
if (ticketNums < 0) { //考虑最后一张票
flag = false;
return;
}
synchronized (this) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + ticketNums--);
}
}