1. 同步锁的引入
我们先来看看没有锁会有什么问题。
举一个卖票的例子,假设我们现在有10张票,有3个人来买票,但我们对每个人买票的个数不做限制。
class MyThread implements Runnable{
private int ticket = 10;
@Override
public void run(){
while(this.ticket > 0){
try{
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
}
}
}
public class Thread1{
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread th1 = new Thread(mt,"黄牛A");
Thread th2 = new Thread(mt,"黄牛B");
Thread th3 = new Thread(mt,"黄牛c");
th1.start();
th2.start();
th3.start();
}
}
我们看看运行结果有什么问题
为什么卖票的结果会出现负数呢。看看代码,当三个线程启动后,三个线程都可以进入run()方法。假如此时票只剩了2张了,但三个线程看到的ticket都是大于0的,都可以进入while循环内,将票的数量-1,此时A先减了,票数变成1了,B也将票数-1,票数现在就是0了,C此时再减时,就变成-1了。
显然这不是我们想看到的结果,我们希望在我进入run方法后,我没有买完票,别人不可以进来买票。就是让线程不要一起进入run方法,而是按顺序一个一个的来。这就是我们今天讲的锁的功能了。
2. synchronized实现同步问题(加锁操作)
我们有两种加锁方式,一种是同步代码块,一种是同步方法。
同步代码块:表示同一时刻只有一个线程能进入同步代码块,但是有多个线程可以同时进入方法
synchronized(this):表示锁当前对象this
synchronized(obj):表示锁任意对象
synchronized(类名.Class):锁对象的Class – 相当于全局锁
同步方法:任意时刻只能有一个线程进入方法
作用于普通方法:对方法所属对象加锁
作用于静态方法:对方法所属的类对象加锁,是全局锁
我们利用同步方法和同步代码块对刚才的代码加以改造,主方法不变,此处不再写主方法了。
同步代码块:
class myThread2 implements Runnable{
private int ticket = 10;
@Override
// 可以有多个线程同时进入run方法
public void run() {
//同步代码块,锁的是当前对象this
// 同一时刻只有一个线程能进入代码块(分隔线内的范围)
synchronized (this) {
System.out.println("====================================");
for (int i = 0; i < 10; i++) {
if (ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩下" + ticket-- + "张票");
}
System.out.println("===========================");
}
}
}
}
此时无论我们卖多少张票,都不会再出现负票数的情况了。
来看看同步方法的用法:
class myThread2 implements Runnable{
private int ticket = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 调用同步方法this
this.sale();
}
}
// 同步方法,任意时刻只有一个线程能进入该方法
public synchronized void sale(){
if (ticket > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "还剩下" + ticket-- + "张票");
}
}
}
3. Synchronized对象锁的概念
synchronized(this)以及普通的synchronized方法,都只能防止多个线程同时进入同一对象的同步段,锁的是synchronized括号内的对象,而不是代码段。
我们看看这样锁会有什么问题:
class Sync {
// 同步方法,同一时刻只有一个线程能进入
public synchronized void test() {
System.out.println(Thread.currentThread().getName()+"方法开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"方法结束");
}
}
class myThread3 implements Runnable{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class synchronizedTest {
public static void main(String[] args) {
myThread3 myThread3 = new myThread3();
for(int i = 0; i < 3; i++){
Thread threadi = new Thread(myThread3,"线程"+i);
threadi.start();
}
}
}
看一下运行结果:
这是为什么呢?我们明明给方法加锁了呀。
我们前面说过,synchronized用于普通方法上时,锁的是当前对象this,即调用该同步方法的对象。这段代码里,我们可以看到是sync对象调用的test()方法,即锁的对象就是sync,但是我们在run()方法里多了这么一行:Sync sync = new Sync();我们在run方法里创建对象了,run方法并没有加锁,此时三个线程可以同时进入run方法,它们各自new了三个sync对象,每个sync调用自己test方法,因此实际结果与我们预期结果不符合。
我们可以将上述代码做如下修改:
class Sync{
public synchronized void test(){
System.out.println(Thread.currentThread().getName()+"方法开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"方法结束");
}
}
class MyThread implements Runnable{
private Sync sync; // 只有一个对象
public MyThread(Sync sync){
this.sync = sync;
}
@Override
public void run(){
this.sync.test();
}
}
public class Thread1{
public static void main(String[] args) {
Sync sync = new Sync();
MyThread mt = new MyThread(sync);
for(int i = 0; i<3; i++){
new Thread(mt,"线程"+i).start();
}
}
}
4. 全局锁
以上我们看到了对象锁,下面看看什么是全局锁。全局锁有两种使用方式。
Ⅰ. static与synchronized共同作用于普通方法
当synchronized与static一同作用于普通方法时,锁的是当前类而非对象,就相当于是一个全局锁。看看用法:
// 全局锁
class Sync {
//static与synchronized连用,相当于锁住了该类,无论有多少对象都会被锁住
public static synchronized void test(){
System.out.println(Thread.currentThread().getName()+"方法开始");
System.out.println(Thread.currentThread().getName()+"方法结束");
}
}
class myThread3 implements Runnable{
@Override
public void run() {
Sync sync = new Sync();
Sync.test();
}
}
public class synchronizedTest {
public static void main(String[] args) {
myThread3 myThread3 = new myThread3();
for(int i = 0; i < 3; i++){
Thread threadi = new Thread(myThread3,"线程"+i);
threadi.start();
}
Ⅱ. 在代码块中锁当前的类对象
synchronized(类名称.class){ }
class Sync {
// 锁当前Sync类
public static void test(){
//在同步代码块中锁当前的Class对象
synchronized (Sync.class) {
System.out.println(Thread.currentThread().getName() + "方法开始");
System.out.println(Thread.currentThread().getName() + "方法结束");
}
}
}
class myThread3 implements Runnable{
@Override
public void run() {
Sync sync = new Sync();
Sync.test();
}
}
public class synchronizedTest {
public static void main(String[] args) {
myThread3 myThread3 = new myThread3();
for(int i = 0; i < 3; i++){
Thread threadi = new Thread(myThread3,"线程"+i);
threadi.start();
}
}
}
5. synchronized底层实现
5.1 同步代码块实现:
执行同步代码块首先要执行moniterenter指令,退出时执行moniterexit指令。使用synchronized实现同步,关键就是要获取对象的monitor对象。当线程获取到monitor对象后,才可以执行同步代码块,否则就只能等待。同一时刻只有一个线程可以获取到该对象的monitor监视器。
通常一个monitorenter指令会同时包含多个monitorexit指令,因为JVM要确保所获取的锁无论在正常执行路径或异常执行路径都能正确解锁。
5.2 同步方法实现:
当使用synchronized标记方法时,字节码会出现一个ACC_SYNCHRONIZED(进入同步方法)。该标记表示在进入该方法时,JVM需要进行monitorenter操作。退出该方法时,无论是否正常返回,JVM均需要进行monitorexit操作。