Java多线程并发笔记(2)并发的关键字:Volatile、synchronized详解

前言

记录学习过程
前面实现了线程与多线程的创建及一些操作方法;
可以看出许多在单线程有用的方法到了多线程就会出各种错误;
为了解决错误,多线程并发就有很多知识
并发的关键字:Volatile、synchronized
这个大佬的解释很深入

目录

  1. 线程安全
    1.1 Java内存模型(JMM)
  2. Volatile
  3. synchronized
  4. 区别

线程安全

许多单线程的程序到了多线程就会线程不安全
例如最基础的 读取 - 修改 -写入 问题,对于多线程操作一个共享变量,执行两次++,可能就加了一个1

那么线程安全是什么呢?
线程安全:当一个类,不断被多个线程调用,仍能表现出正确的行为时,那它就是线程安全的

为了实现线程安全,要从两个方面去考虑:执行控制内存可见

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存

synchronized关键字解决的是执行控制的问题,synchronized是锁机制,当有线程使用方法时将当前对象、类锁住,使其他线程不能调用该方法,就杜绝了多线程同时调用导致程序实现错误的情况

volatile关键字解决的是内存可见性的问题,由于线程操作的对象是拷贝的副本,不是原对象,造成明明程序改动了变量,实际情况却没有改动的问题,volatile修改变量并同步到内存

Java内存模型(JMM)

Java内存模型(Java Memory Model )定义了 Java 虚拟机 ( JVM ) 在计算机内存 ( RAM ) 中的工作方式
这个解释很详细

扫描二维码关注公众号,回复: 9174570 查看本文章

多线程并发编程的两个问题:

  1. 线程间的通信
    即如果有共享数据,就可以通过共享数据通信
    没有共享就只能通过明确的发送信息方法通信
  2. 线程同步
    控制不同进程间操作发生的相对顺序的机制
    如锁机制互斥执行,消息传递就要顺序执行

JMM定义了线程的私有本地内存(抽象概念,并不真实存在)与内存的共享变量的关系
在这里插入图片描述

package com.company.Thread;

public class ThreadOfPool {
    private static int count=1;
     static class CreatThread1 extends Thread {
        public void run(){
            count++;
            System.out.println("线程1:"+count);
        }
    }
    static class CreatThread2 extends Thread {
        public void run() {
            count++;
            System.out.println("线程2:"+count);
        }
    }
    public int getCount(){
         return count;
    }
    public static void main(String[] args) throws InterruptedException {
         CreatThread1 creatThread1=new CreatThread1();
         CreatThread2 creatThread2=new CreatThread2();
         creatThread1.start();
         creatThread2.start();

    }
}

关于 读取 - 修改 -写入 问题,线程A creatThread1 读取了共享变量放在本地内存,然后count+1,在这时线程B creatThread2也读取了共享变量,这时count还是1,然后++,线程a是count++=2,线程b也是count++=2,这就不符合程序的原意

通过锁定共享变量,使得变量count具有可见性就可以解决这个问题

Volatile

这个案例挺符合的:

package com.company.Thread.Game;

public class GoalNotifier implements Runnable {
    public boolean goal = false;

    public boolean isGoal() {
        return goal;
    }

    public void setGoal(boolean goal) {
        this.goal = goal;
    }

    @Override
    public void run() {
        while (true) {
            if (isGoal()) {

                System.out.println("Goal !!!!!!");

                setGoal(false);
            }
        }
    }
}

当goal = true 时会执行run中的语句

测试:

package com.company.Thread.Game;

public class Game {
    public static void main(String[] args) throws InterruptedException {
        // Game begun! Init goalNotifier thread
        GoalNotifier goalNotifier = new GoalNotifier();
        Thread goalNotifierThread = new Thread(goalNotifier);
        goalNotifierThread.start();

        // After 3s
        goalNotifierThread.sleep(3000);
        // Goal !!!
        goalNotifier.setGoal(true);
    }
}

在这里插入图片描述
并不会输出,因为goalNotifier.setGoal(true)方法操作的是本地内存中的goal变量,所以方法没反应,因为他操作的是主存中的goal变量

使用Volatile修饰goal变量:

public volatile boolean goal = false;

在这里插入图片描述
就运行了run方法

Volatile详解

Volatile是一个标志,通知JVM在编译时应该从主存读取变量

在编译过程中会多一行机器语言:lock add dword ptr

翻译:lock的作用是使得本CPU的Cache写入内存,同时使其他CPU的Cache无效

volatile不能保证原子性。但能保证可见性和有序性
而synchronized能保证原子性

Volatile修饰的变量保证了3点:

  1. 完成写入后,所以访问该变量的进程都会得到最新值(可见性)
  2. 在你写入前,会保证所有之前发生的事已经发生(这是Volatile修饰的前提)
  3. volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行的顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其他不可见的地方

多线程基础必备的知识点

volatile大多用于标志位上(判断操作),使用Volatile的前提:

  1. 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)
  2. 该变量不会纳入到不变性条件中(该变量是可变的)
  3. 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)

synchronized

前面的volatile只能作用于变量,而且不能保证原子性
synchronized更强,可以使用在变量、方法、和类级别的,而且可以保证变量修改的可见性(当执行完synchronized之后,修改后的变量对其他的线程是可见的)和原子性(被保护的代码块是一次被执行的,没有任何线程会同时访问)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全

package com.company.Thread.Synchronized;

public class synchronizedTest {
    public synchronized void test1(){}
    public void test2(){
        synchronized (this){}
    };
}

在这里插入图片描述锁机制:
同步代码块:
monitorenter:进入指令
monitorexit:退出指令
同步方法(JVM底层实现):方法修饰符上的ACC_SYNCHRONIZED实现

synchronized使用 (锁对象、类)

  1. 修饰普通方法
  2. 修饰代码块
  3. 修饰静态方法
package com.company.Thread.Synchronized;

public class synchronizedTest {
    private Object object=new Object();
    //修饰普通方法,锁synchronizedTest对象
    public synchronized void test1(){

    }
    public void test2(){
        //修饰代码块,也是锁对象,this是指本对象
        synchronized (this){

        }
        //也可以锁设置的对象object
        synchronized (object){
        
        }
    }
}

修饰普通方法、代码块(内置锁),本质上是锁对象,也就是堆内的对象实例,这些方式也称为客户端锁
客户端锁不赞成使用,会造成高耦合,建议使用设计模式里的装饰器模式

修饰静态方法有点不同
我们知道静态方法在类加载时也随之加载在方法区,它们不需要实例就可以使用,所以synchronized锁定的是类(类的字节码对象)

    public static synchronized void test3(){
        
    }

但是类锁和对象锁不会冲突

package com.company.Thread.Synchronized;

public class SynchronizedTest{
    private Object object=new Object();
    //修饰普通方法,锁synchronizedTest对象
    public synchronized void test1(){
        for (int i=0;i<5;i++){
            System.out.println("对象锁:"+i);
        }

    }
    public void test2(){
        //修饰代码块,也是锁对象,this是指本对象
        synchronized (this){

        }
        //也可以锁设置的对象object
        synchronized (object){

        }
    }
    public static synchronized void test3(){
        for (int i=0;i<5;i++){
            System.out.println("类锁:"+i);
        }
    }

    public static void main(String[] args){
        SynchronizedTest synchronizedTest=new SynchronizedTest();
        Thread thread1=new Thread(()->
                synchronizedTest.test1()
        );

        Thread thread2=new Thread(()->
                test3()
        );
        thread1.start();
        thread2.start();
    }

}

在这里插入图片描述

synchronized锁的一些特点

内置锁的可重入性
同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入

package com.company.Thread.Synchronized;

public class Father {
    public synchronized void test1(){
        System.out.println("father类调用");
    }

}
class Child extends Father{
    public synchronized void test2(){
        System.out.println("child类调用方法test2");
    }
    public synchronized void test3(){
        System.out.println("child类调用方法test3");
        test2();
        super.test1();
    }
}
class Test{
    public static void main(String[] args){
        Child child=new Child();
        child.test3();
    }
}

在这里插入图片描述
test3的对象锁可以重入

重入锁是怎么实现可重入性的,其实现方法是为每个锁关联一个线程持有者计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

锁的释放

很简单,就两种情况,运行完了,释放
运行出异常,报错释放(为了预防死锁)

volatile和synchronized的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
发布了49 篇原创文章 · 获赞 0 · 访问量 1226

猜你喜欢

转载自blog.csdn.net/key_768/article/details/104291649