深入了解JUC并发(四)并发关键字

并发关键字

synchronized

可以理解为汽车的自动挡,对应的Lock就是手动挡~

概念

即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心现成话上下文切换

使用

只有在静态方法的时候锁的是类对象

修饰目标
方法当前实例对象
静态方法类对象
代码块当前实例对象
class对象类对象
任意Object对象任意示例对象
实例和讲解
实例
/**
 * @author qijian
 * @description
 * @date 2021/4/12 - 10:09
 */
public class Synchronized {
    //synchronized关键字可放于方法返回值前任意位置,本示例应当注意到sleep()不会释放对监视器的锁定
    //实例方法
    public synchronized void instanceMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("instanceMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //静态方法
    public synchronized static void staticMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("staticMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void thisMethod() {
        //this对象
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println("thisMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void classMethod() {
        //class对象
        synchronized (Synchronized.class) {
            for (int i = 0; i < 5; i++) {
                System.out.println("classMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void anyObject() {
        //任意对象
        synchronized ("anything") {
            for (int i = 0; i < 5; i++) {
                System.out.println("anyObject");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
讲解

1、普通方法和代码块中使用this是同一个监视器(锁),即某个具体调用该代码的对象

public static void main(String[] args) {
    Synchronized syn = new Synchronized();
    for (int i = 0; i < 10; i++) {
        new Thread() {
            @Override
            public void run() {
                syn.thisMethod();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                syn.instanceMethod();
            }
        }.start();
    }
}

在这里插入图片描述

2、静态方法和代码块中使用该类的class对象是同一个监视器,任何该类的对象调用该段代码时都是在争夺同一个监视器的锁定

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Synchronized syn = new Synchronized();
        new Thread() {
            @Override
            public void run() {
                Synchronized.staticMethod();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                syn.classMethod();
            }
        }.start();

    }
}

在这里插入图片描述

特点
  1. 可重入性:当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果
  2. 自动释放:当代码段执行结束或出现异常后会自动释放对监视器的锁定
  3. 非公平锁:在等待获取锁的过程中不可被中断
  4. 互斥性:被 synchronized 修饰的方法同时只能由一个线程执行
锁升级
  1. Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,这几个状态会随着竞争情况逐渐升级
  2. 锁可以升级但不能降级,意味着 偏向锁 升级成 轻量级锁 后不能降级成 偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
概念
  1. HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
  2. 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

运作:

在这里插入图片描述

关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活:

  1. 如有必要可以使用JVM参数来关闭延迟:

    -XX:BiasedLockingStartupDelay=0

  2. 如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁,那么程序默认会进入轻量级锁状态。:

    -XX:UseBiasedLocking=false

轻量级锁/重量级锁
概念

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

在这里插入图片描述

8锁现象

深刻理解我们的锁:如何判断锁的是谁?

1、两个同步方法(先发短信后打电话)

public class dome01 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(() -> { phone.sendMs(); }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone.call(); }).start();
    }
}

class Phone {
    public synchronized void sendMs() {
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

原因:顺序执行下去

2、我们让 发短信 延迟4s(先发短信后打电话)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();

        new Thread(() -> {
            try {
                phone.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone.call(); }).start();
    }
}

class Phone {
    public synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

原因:并不是顺序执行,而是synchronized 锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待

3、加一个普通方法(先hello再发短信)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();

        new Thread(() -> {
            try {
                phone.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone.hello(); }).start();
    }
}

class Phone {
    public synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
    public void hello() {
        System.out.println("hello");
    }
}

原因:hello是一个普通方法,不受synchronized锁的影响,不用等待锁的释放

4、如果我们使用的是两个对象,一个调用发短信,一个调用打电话(先打电话后发短信)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone2.call(); }).start();
    }
}

class Phone {
    public synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
    public void hello() {
        System.out.println("hello");
    }
}

原因:两个对象两把锁,不会出现等待的情况,发短信睡了4s,所以先执行打电话

5、如果我们把synchronized的方法加上static变成静态方法

(1)我们先来使用一个对象调用两个方法!(先短信后电话)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();

        new Thread(() -> {
            try {
                phone.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone.call(); }).start();
    }
}

class Phone {
    public static synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public static synchronized void call() {
        System.out.println("打电话");
    }
}

(2)如果我们使用两个对象调用两个方法!(还是先短信后电话)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone2.call(); }).start();
    }
}

class Phone {
    public static synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public static synchronized void call() {
        System.out.println("打电话");
    }
}

原因是:对于static静态方法来说,对于整个类Class来说只有一份,对于不同的对象使用的是同一份方法而已

6、如果我们使用一个静态同步方法、一个同步方法(先打电话后发短信)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
//        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone1.call(); }).start();
    }
}

class Phone {
    public static synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

原因:因为一个锁的是Class类的模板,一个锁的是对象的调用者。所以不存在等待,直接运行。

7、如果我们使用一个静态同步方法、一个同步方法,两个对象调用(先打电话后发短信)

public class dome01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone1.sendMs();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> { phone2.call(); }).start();
    }
}

class Phone {
    public static synchronized void sendMs() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

原因:两把锁锁的不是同一个东西

小结

要看清 synchronized 锁的到底是什么,才能准确判断

lock

手动挡

概念
  1. 在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能
  2. 它提供了与synchronized关键字类似的同步功能。虽然它缺少了(通过synchronized块或者方法)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性

在这里插入图片描述

使用
package com.marchsoft.juctest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Description:
 *
 * @author jiaoqianjin
 * Date: 2020/8/10 22:05
 **/

public class LockDemo {
    public static void main(String[] args) {
        final Ticket2 ticket = new Ticket2();

        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticket.sale();
            }
        }, "C").start();
    }
}
//lock三部曲
//1、    Lock lock=new ReentrantLock();
//2、    lock.lock() 加锁
//3、    finally=> 解锁:lock.unlock();
class Ticket2 {
    private int number = 30;
	
    // 创建锁
    Lock lock = new ReentrantLock();
    //卖票的方式
    public synchronized void sale() {
        lock.lock(); // 开启锁
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "张票剩余" + number + "张票");
            }
        }finally {
            lock.unlock(); // 关闭锁
        }

    }
}
方法

在这里插入图片描述

从上,可以看出 Synchronized 与 Lock 的区别:

  1. Synchronized 内置的Java关键字,Lock是一个Java类;

  2. Synchronized 无法判断获取锁的状态,Lock可以判断锁的状态

  3. Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

  4. Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待;

  5. Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁

  6. Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码

缺点
  1. 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了
  2. 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些

ReadWriteLock

读写锁

不加锁

所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题:

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        int num = 6;
        for (int i = 1; i <= num; i++) {
            int finalI = i;
            new Thread(() -> {

                myCache.write(String.valueOf(finalI), String.valueOf(finalI));

            },String.valueOf(i)).start();
        }

        for (int i = 1; i <= num; i++) {
            int finalI = i;
            new Thread(() -> {

                myCache.read(String.valueOf(finalI));

            },String.valueOf(i)).start();
        }
    }
}

/**
 *  方法未加锁,导致写的时候被插队
 */
class MyCache {
    private volatile Map<String, String> map = new HashMap<>();

    public void write(String key, String value) {
        System.out.println(Thread.currentThread().getName() + "线程开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程写入ok");
    }

    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "线程开始读取");
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程写读取ok");
    }
}

在这里插入图片描述

加锁
  1. 可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
  2. 但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache2 myCache = new MyCache2();
        int num = 6;
        for (int i = 1; i <= num; i++) {
            int finalI = i;
            new Thread(() -> {

                myCache.write(String.valueOf(finalI), String.valueOf(finalI));

            },String.valueOf(i)).start();
        }

        for (int i = 1; i <= num; i++) {
            int finalI = i;
            new Thread(() -> {

                myCache.read(String.valueOf(finalI));

            },String.valueOf(i)).start();
        }
    }

}
class MyCache2 {
    private volatile Map<String, String> map = new HashMap<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void write(String key, String value) {
        lock.writeLock().lock(); // 写锁
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程写入ok");

        }finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }

    public void read(String key) {
        lock.readLock().lock(); // 读锁
        try {
            System.out.println(Thread.currentThread().getName() + "线程开始读取");
            map.get(key);
            System.out.println(Thread.currentThread().getName() + "线程写读取ok");
        }finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }
}

在这里插入图片描述

volatile

volatile是Java提供的一种轻量级的同步机制

  • 相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错
  • 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象
保证可见性

当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去

public class JMMDemo01 {
    // 不加volatile:程序会死循环
    // 加了volatile:是可以保证可见性的
    private volatile static Integer number = 0;

    public static void main(String[] args) {
        //main线程
        //子线程1
        new Thread(()->{
            while (number==0){
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //子线程2
        new Thread(()->{
            while (number==0){
            }

        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number=1;
        System.out.println(number);
    }
}
不保证原子性

原子性:线程A在执行任务的时候,不能被打扰/分割,要么同时成功,要么同时失败。

这个写会操作会导致其他线程中的volatile变量缓存无效(原子性:同时成功/失败)

public class VDemo02 {

    private static volatile int number = 0;

    public static void add(){
        number++; //++ 不是一个原子性操作,是两个~3个操作
    }

    public static void main(String[] args) {
        //理论上number 为 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){
            //main  gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+number);
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值