Synchronize锁详解

1、多线程中出现的问题

我们在使用多线程中,发现代码有时候会和我们的预期结果不一致:

我们发现,在多线程中,我们开启两个线程对我们的i值进行增加,但是最后的结果不等于200000,他最后输出的结果是小于200000的,这是为啥呢?

原因很简单,因为我们的i++这个操作,并不是一个原子操作。

i++分解后其实是三个操作:

  1. 读取i的值
  2. 将i的值进行+1
  3. 将i的值写回内存

两个线程同时进程,可能A线程刚读取完i的值。B线程就进来,将i的值进行读取并对i+1写回内存。B线程结束后A线程并不知道i的值被修改,所以他再对i进行+1操作其实并不准确。

2、Synchronize锁的介绍

synchronize锁的作用:

就是保证同一时刻最多只有一个线程执行该段代码,以保证并发安全的问题。

3、Synchronize锁的用法

  • 对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
  • 类锁:Synchronize修饰静态方法或者锁对象为class对象

3.1、对象锁

用代码来演示我们的对象锁

public class MethodSynchronize implements Runnable {

    private static MethodSynchronize methodSynchronize = new MethodSynchronize();

    @Override
    public void run() {
        synchronized (this) {
            System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(methodSynchronize);
        Thread thread2 = new Thread(methodSynchronize);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){

        }
        System.out.println("game over");
    }
}
//执行结果
我是对象锁代码块,Thread-0开始执行
我是对象锁代码块,Thread-0执行结束
我是对象锁代码块,Thread-1开始执行
我是对象锁代码块,Thread-1执行结束
game over

我们发现,对方法加了锁后,就是顺序执行的。执行完了thread1之后才回去执行thread2。

如果我们不加对象锁,我们的执行效果是这样的:

public class MethodSynchronize implements Runnable {

    private static MethodSynchronize methodSynchronize = new MethodSynchronize();

    @Override
    public void run() {
        // synchronized (this) {
            System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        // }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(methodSynchronize);
        Thread thread2 = new Thread(methodSynchronize);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){

        }
        System.out.println("game over");
    }
}
//执行效果
我是对象锁代码块,Thread-0开始执行
我是对象锁代码块,Thread-1开始执行
我是对象锁代码块,Thread-0执行结束
我是对象锁代码块,Thread-1执行结束
game over

使用同步代码块,自己指定锁:

public class MethodSynchronize implements Runnable {

    private static MethodSynchronize methodSynchronize = new MethodSynchronize();
    Object lock1 = new Object();
    Object lock2 = new Object();

    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        }

        synchronized (lock2) {
            System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(methodSynchronize);
        Thread thread2 = new Thread(methodSynchronize);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){

        }
        System.out.println("game over");
    }
}
//执行结果
我是lock1对象锁代码块,Thread-0开始执行
我是lock1对象锁代码块,Thread-0执行结束
我是lock2对象锁代码块,Thread-0开始执行
我是lock1对象锁代码块,Thread-1开始执行
我是lock1对象锁代码块,Thread-1执行结束
我是lock2对象锁代码块,Thread-0执行结束
我是lock2对象锁代码块,Thread-1开始执行
我是lock2对象锁代码块,Thread-1执行结束
game over

Process finished with exit code 0

这里我们可以看到,因为是不同的锁,所以thread-0的lock2锁的代码执行时间和thread-1的lock1锁执行的时间是同步。因为锁对象不是同一个,所以是并行执行的。

如果将lock2的锁对象也替换成lock1,那么他们就还是顺序执行:

public void run() {
        synchronized (lock1) {
            System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        }

        synchronized (lock1) {
            System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "执行结束");
        }
    }
//执行效果
我是lock1对象锁代码块,Thread-0开始执行
我是lock1对象锁代码块,Thread-0执行结束
我是lock2对象锁代码块,Thread-0开始执行
我是lock2对象锁代码块,Thread-0执行结束
我是lock1对象锁代码块,Thread-1开始执行
我是lock1对象锁代码块,Thread-1执行结束
我是lock2对象锁代码块,Thread-1开始执行
我是lock2对象锁代码块,Thread-1执行结束
game over

 

方法锁代码示例:

public class MethodSychroize2 implements Runnable {
    private static MethodSychroize2 methodSychroize2= new MethodSychroize2();
    @Override
    public void run() {
        method();
    }

    public synchronized void method(){
        System.out.println("我是对象锁的方法修饰符,我叫"+Thread.currentThread().getName()+"开始");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我是对象锁的方法修饰符,我叫"+Thread.currentThread().getName()+"结束");
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(methodSychroize2);
        Thread thread2 = new Thread(methodSychroize2);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){

        }
        System.out.println("game over!");
    }
}
//执行效果
我是对象锁的方法修饰符,我叫Thread-0开始
我是对象锁的方法修饰符,我叫Thread-0结束
我是对象锁的方法修饰符,我叫Thread-1开始
我是对象锁的方法修饰符,我叫Thread-1结束
game over!

synchronize修饰普通方法,默认锁对象就是当前实例对象

同步代码块,锁对象需要自己指定

3.2、类锁

概念:一个类,可以有多个实例对象,但是只有一个class对象

类锁有两种方式:

  1. static静态方法上synchronize关键字
  2. 代码块中用.class作为锁对象

不适用类锁,而是使用对象锁出现的问题:

public class StaticMethodSynchronize implements Runnable {
    private static StaticMethodSynchronize instance1 = new StaticMethodSynchronize();
    private static StaticMethodSynchronize instance2 = new StaticMethodSynchronize();

    @Override
    public void run() {
        method();
    }

    public synchronized void method(){
        System.out.println("我是类锁,我叫" + Thread.currentThread().getName() + "开始执行");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(instance1);
        Thread thread2 = new Thread(instance2);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()) {

        }
        System.out.println("game over");

    }
}
//执行结果
我是类锁,我叫Thread-1开始执行
我是类锁,我叫Thread-0开始执行
Thread-0执行结束
Thread-1执行结束
game over

我们发现这种情况下,使用同步代码块锁,线程为啥还是并行执行的呢?

原因是我们的同步代码块中,锁对象默认是当前实例,但是我们可以看到,thread1和thread2的传递进去的实例对象并不是同一个,所以无法锁住。

想要解决这个问题,我们就需要使用我们的类锁,使用静态方法上加synchronize

    public static synchronized void method(){
        System.out.println("我是类锁静态方法,我叫" + Thread.currentThread().getName() + "开始执行");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
//执行结果
我是类锁静态方法,我叫Thread-0开始执行
Thread-0执行结束
我是类锁静态方法,我叫Thread-1开始执行
Thread-1执行结束
game over

Synchronize(.class)这种形式的代码示例:

    public void method(){
        synchronized (StaticMethodSynchronize.class) {
            System.out.println("我是类锁静态方法,我叫" + Thread.currentThread().getName() + "开始执行");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行结束");
        }
    }
//执行结果
我是类锁静态方法,我叫Thread-0开始执行
Thread-0执行结束
我是类锁静态方法,我叫Thread-1开始执行
Thread-1执行结束
game over

4、核心思想

  1. 一把锁同时只能被一个线程持有,没有持有锁的线程必须等待
  2. 每个实例之间都有一把锁,不同实例之间互不影响;例外,只有锁对象是.class的时候和synchronize修饰static方法的时候,所有对象共用一把类锁
  3. synchronize无论是正常执行完毕还是抛出异常,都会释放锁对象

synchronize修饰的方法调用了其他的方法,不能保证其他方法是线程安全的。没有被synchronize修饰的方法,是可以同时被多个线程访问的

5、synchronize性质

  • 可重入:同一线程的外层函数获取了锁之后,内层函数不需要再去竞争锁,可以直接使用
  • 不可中断:一旦这个锁被人获得了,我还想获得锁对象,我只能选择等待或者阻塞,直到没得线程释放了这把锁

5.1可重入性

  • 证明同一个方法是可重入的:
public class SynchronizeRecusion  {
    int a = 0;
    public static void main(String[] args) {
        SynchronizeRecusion recusion = new SynchronizeRecusion();
        recusion.method();
    }

    private synchronized void method() {
        System.out.println("我是method,a="+a);
        if (a == 0){
            a++;
            method();
        }
    }
}
//执行结果
我是method,a=0
我是method,a=1
  • 证明可重入不要求是同一个方法:
public class MethodOtherSynchorize {

    public synchronized void method(){
        System.out.println("我是method");
        method1();
    }

    public synchronized void method1(){
        System.out.println("我是method1");
    }

    public static void main(String[] args) {
        MethodOtherSynchorize otherSynchorize = new MethodOtherSynchorize();
        otherSynchorize.method();
    }
}
//执行结果
我是method
我是method1
  • 证明可重入不要求是同一个类中
public class SuperClassSynchroize {

    public synchronized void doSomething(){
        System.out.println("我是父类方法"+this);
    }
}

class TestClass extends SuperClassSynchroize{
    public synchronized void doSomething(){
        System.out.println("我是子类方法"+this);
        super.doSomething();
    }

    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        testClass.doSomething();
    }
}
//执行结果
我是子类方法com.wx.mythread.thread.TestClass@7adf9f5f
我是父类方法com.wx.mythread.thread.TestClass@7adf9f5f

粒度:是线程中,只要是同一个线程中,需要的是同一把锁,就可以直接使用

5.2不可中断

一旦这个锁被人获得了,我还想获得锁对象,我只能选择等待或者阻塞,直到没得线程释放了这把锁。如果别人永远不释放,我将会永远等待下去。

相比之下,之后会介绍Lock锁,他有中断的能力。我等待的时间太久了,可以选择中断正在执行的锁对象;等待太久了,也可以选择不等待了,直接退出

6、原理

加锁和释放锁的等价代码:

public class LockSynchroize {
    Lock lock = new ReentrantLock();

    public synchronized void method(){
        System.out.println("我是synchronized形式的锁");
    }

    public void method1(){
        try {
            lock.lock();
            System.out.println("我是lock锁的形式");
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockSynchroize lockSynchroize = new LockSynchroize();
        lockSynchroize.method();
        lockSynchroize.method1();
    }
}

我们的synchronize关键字在方法内部也是做的这么下面这个操作。

将我们的同步代码块进行反编译:

monitorenter为0,说明可以获取到锁,获取到锁会+1。monitorexit操作会-1,减到0就会释放锁,否则就说明是重入进来的。 

6.1可重入原理

利用加锁计数器实现的。

  • JVM会负责跟踪对象呗加锁的次数
  • 线程第一次给对象加锁的时候,计数变为1,每当相同的线程在对象上再次获得锁时,计数会递增
  • 每当任务离开时,计数递减,当计数为0的时候,锁会被完全释放

6.2可见性原理

Java内存模型

加锁后

加锁后,线程也是无法直接操作主内存的,只能通过本地内存去同步主内存中的数据。

7、缺陷

  • 效率低:锁释放的情况少,不能设置超时时间,没有获取到锁只能一直等待
  • 不够灵活(读写锁更灵活):加锁释放锁的时机单一
  • 无法知道是够成功获取到锁

8、面试

  1. 使用synchronize关键字注意点:锁对象不能为空,作用域不宜过大,避免死锁
  2. 如何选择synchronize和lock:尽量选择juc提供的工具类,其次选择synchronize,因为写的代码少,减少出错的概率
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值