Java - 使用synchronized关键字加锁

 首先来看一个加锁的具体案例 “使用两个线程对同一个变量相加”,大概了解加锁的意义...

代码如下: 

public class demo2{
    public static int count=0;
    public static Object locker1=new Object();  //???需不需要new Object()创建对象
    public static void main(String[] args) throws InterruptedException {
        Thread thread1=new Thread(()->{
            synchronized(locker1){
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        });

        Thread thread2=new Thread(()->{
            synchronized (locker1){
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(count);
    }
}

加锁前:

代码每次运行后的输出结果都不同,这是两个线程修改同一个变量造成的线程安全问题。

加锁后:

代码运行后的输出结果相同且正确

一、锁的使用方式

首先需要明确一点:Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个

我们通常使用synchronized关键字来给一段代码或一个方法上锁。它通常有以下三种形式:

1. 修饰普通方法

//1. 锁为当前实例 即SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
   
  }
}

2. 修饰静态方法 

//2. 锁为当前Class对象 即SynchronizedDemo类对象
public class SynchronizedDemo {
public synchronized static void method() {

  }
}

3. 修饰代码块 

//3. 锁为括号里的对象
//3.1 括号里的this即SynchronizedDemo对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {

        }
    }
}
//3.2 括号里的this即类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {

        }
    }
}
//3.3 括号里的是o对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        
    }
}

无论是哪种写法,使用synchronized的时候都要明确锁对象(明确是对哪个对象加锁)

由此我们会发现写法1等价于3.1,写法2等价于3.2

文章开头的代码案例,使用了3.2的加锁方式,锁对象是locker1

二、 锁的特性与作用

synchronized用的锁是存在Java对象头里的,synchronized 本质上要修改指定对象的 "对象头"。 

1. 锁的互斥性 

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象的 synchronized 就会阻塞等待。

注意:互斥的前提是两个线程拿到同一把锁  

当thread1释放锁后,thread2才有可能拿到锁。因为可能是与thread2竞争的线程先拿到锁。

注意:锁的竞争者也必须是尝试加锁失败后,进入BLOCKED阻塞状态的线程

来看一个不同线程对不同对象加锁的情况,这种情况下就不能起到互斥的作用!

代码示例:

public class demo2{
    public static int count=0;
    public static Object locker1=new Object();  //???需不需要new Object()创建对象
    public static Object locker2=new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1=new Thread(()->{
            synchronized(locker1){
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        });

        Thread thread2=new Thread(()->{
            synchronized (locker2){
                for(int i=0;i<50000;i++){
                    count++;
                }
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(count);
    }
}

2. 锁的可重入性

synchronized 同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的问题。

🔊 什么情况会锁死?

一个线程没有释放锁, 然后又尝试再次加锁。如果锁为不可重入锁,就会锁死。

public class demo3 {
    public class SynchronizedDemo {
        public void method() {
            //第一次加锁
            synchronized (this) {

            }
            //第二次加锁
            synchronized (this) {

            }
        }
    }
}

按照互斥性:第二次加锁时,进程需要阻塞等待第一次加的锁被释放才能进行第二次加锁。但是第一次的锁需要经过第二次的锁加上并释放之后才能释放...这个时候就进入了无底洞,没办法解锁再加锁,就是死锁状态 -- 这样的锁被称为“不可重入锁”

而Java的 synchronized锁是可重入锁,就不会有上述死锁问题

3. 锁的作用 - 解决原子性问题

拿开头的案例分析,为什么加锁前运行结果不正确,加锁后结果就正确了呢?

首先要明确:两个线程修改同一个变量,count++的指令实际有三个步骤:

  • 从内存把数据读到 CPU
  • 进行数据更新
  • 把数据写回到 CPU

多个线程参与修改可能造成指令的步骤执行有多种情况,下面列出4种(上一篇博客也提到过):

只有在可能性1、2情况下,运行结果正确,其他情况都不正确。即只有当:一个线程一次性执行完count指令的三个步骤后,另一个线程才对count进行下一次修改时结果才正确。这种特性也叫做“原子性”

加锁后,两个线程的指令执行情况可能性如下:

使用了synchronized加锁操作之后,可以实现指令的原子性,从而保证“一个线程在修改时另一个线程不能修改”。

即:加锁实际上是保证原子性,一定程度上保证了线程安全

注意:

加锁过程本身是非常消耗资源的。如果加锁过程太频繁,虽然能够保证线程安全,但是效率就会大大降低 ~

三、wait&notify机制

使用该方法可以在多线程并发执行的环境下,有效控制多线程的执行顺序,使线程之间可以通信。

1. wait()方法

观察wait方法的使用效果,代码示例:

public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("等待中");
            object.wait();
            System.out.println("等待结束");
        }
    }
}

代码结果:由于wait的存在,线程一直在等待,进程始终没有结束

wait 做的事情:

  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件时被唤醒, 重新尝试获取这个锁

注意:

  wait一定要搭配 synchronized 来使用

  调用 wait 的对象和 synchronized 里使用的锁对象必须是一个对象

wait结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

注意:调用wait方法的线程会一直处于阻塞状态,直到别的线程唤醒它

 

2. notify方法

使用wait方法后线程一直在等待,进程无法结束。那我们可否唤醒线程,让它从等待队列中出来呢?当然可以,我们使用到的就是notify方法。

notify 方法用来唤醒等待中的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(没有 "先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

代码案例:

public class demo4 {
    //创建一个locker作为锁对象
    public static Object locker=new Object();
    public static Object locker2=new Object();
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            System.out.println("线程1 - start");
            synchronized (locker){
                try {
                    locker.wait();//记住: wait需要搭配 synchronized 使用,否则会报错
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程1 - end");
        });

        Thread thread2=new Thread(()->{
            //让用户控制线程1的结束
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入任意内容: ");
            // next会阻塞等待用户输入内容
            scanner.next();

            //用户输入内容后,代码往下执行.此时便可以接触线程1的阻塞了
            synchronized (locker){
                locker.notify();//记住: 调用notify的对象需要和调用wait的对象是同一个
            }
            //测试: 调用notify的对象与调用wait的对象不相同时的运行结果
//            synchronized (locker2){
//                locker.notify();//记住: 调用notify的对象需要和调用wait的对象是同一个
//            }
        });

        thread1.start();
        thread2.start();
    }
}

注意:

  调用 wait 的对象和 synchronized 里使用的锁对象必须是一个对象,且需要和调用 notify 的    对象相同

  调用notify的时候会尝试进行通知,在有线程调度器随机挑选出个呈 wait 状态的线程唤醒。

  如果当前当前对象中没有正在wait的线程,也不会有副作用

  当多个线程等待的时候,notify是随机唤醒一个线程,notifyall是唤醒所有线程

3. 使用wait&notify机制的好处

① 线程之间能够通信

② 可以一定程度上避免线程饿死

🔊 什么是线程饿死?

     在并发执行情况下,各个线程间的执行顺序并不遵循"先来后到",而是"力争前线"。参与竞争的       线程中某些竞争力大的线程每次都能冲到前线,拿到系统的资源,而有些线程被排挤在外。这       些被排挤的线程一直拿不到系统的资源,就会"饿死"

使用wait后,参与竞争的线程虽然还是那么多,由于部分线程受到wait条件唤醒的限制,需要等待条件被唤醒后才能抢夺到cpu资源,这就给了另一些线程机会。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值