java中保证线程安全的机制

1.概念

1.1进程VS线程

进程:操作系统分配资源的最小单位
线程:操作系统调度最小单位

进程与线程关系:

  1. 有进程必然有线程(一个进程必然有一个线程、一个进程也可有多个线程)
  2. 从属关系
  3. 线程是轻量级进程(线程启动会比进程稍微轻量级一点)

1.2线程安全问题

1.2.1内存的共享/私有

共享:堆(方法区/常量池)
私有:栈/PC

1.2.2三个特性

原子性、内存的可见性、代码的重排序

  • 保证原子性:java的一组指令(天生原子) / 锁
  • 内存可见性:JMM(java Memory Model) java内存模型
    在这里插入图片描述
    针对共享数据——线程只能操作工作内存中的数据(读、写)
    读:把主内存的数据加载(LOAD)到工作内存;
    写:把工作内存的数据写(SAVE)到主内存
  • 代码重排序
    概念:CPU / javac编译器 / 运行时的JIT对代码进行的适度优化
    注意:java规定了优化必需保证单线程情况下的正确性(多线程下可能出出错)

1.2.3引发线程安全问题的因素

  1. 基础条件:出现共享数据(没有共享数据一定不会存在线程不安全的问题)
  2. 共享数据出现的情况(只读操作不会导致线程不安全)
  3. 三个特性(原子性、内存可见性、代码重排序)

2.synchronized—监视器锁(monitor lock)

2.1语法层面

2.1.1语法

1》作为方法的修饰符(定义方法)
2》作为代码块出现

public class SynchronizedDemo {
    public synchronized void method() {
        // 具体代码
    }
    public synchronized static void staticMethod() {
        // 具体代码
    }  
     public void block() {
        synchronized (this) { //synchronized (引用)
            // 具体代码
        }
    }     
}

2.1.2作用

  1. java中每个对象都有一个锁 —— 监视锁(monitor lock)
    SynchronizedDemo object = new SynchronizedDemo();
    object.method();
    在这里插入图片描述
  2. 执行带 synchronized 修饰的普通方法时,首先需要 lock 引用指向的对象中的锁
    1》如果可以锁,正常执行代码
    2》否则,需要等待其他进程把锁释放(unlock)
    解释: 如果一个线程 lock 到了这把锁,到方法执行结束时,就会 unlock 这把锁

2.2

2.2.1锁在什么地方

  1. 普通方法:锁在调用该方法的引用指向对象中(当前对象即this)
    eg: public synchronized void method() { }
  2. 静态方法:
    eg: public synchronized static void staticMethod() { }
  3. 代码块
public class SynchronizedDemo {   
   public void block() {
        ///
        synchronized (this) { //相当于普通锁
            // 具体代码
        }
        synchronized (SynchronizedDemo.class) { //相当于全局锁
            // 具体代码
        }
    }
}

2.2.2加锁/释放锁

1.原理:

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中。
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

2.分类:

A. 普通方法

  1. 锁的持有和释放——线程状态之间的关系
    eg: Runnable(就绪队列)里面有A、B、C三个线程;三者都在抢同一把锁;只有一个CPU
    I. 从三个进程中任意选择一个进程放在CPU上(假设A在CPU上),加锁
    II. 执行一段时间后,A被调度出CPU回到就绪队列中(A放弃CPU),此时锁依旧在
    III. 假设此时B抢占CPU(锁依旧锁着),抢占资格被剥夺,从就绪队列移到阻塞队列(Runnable状态——》Blocked状态);若C去抢占CPU,结果和B一样
    IV. A在CPU中执行完毕时,释放锁(A不一定会释放CPU),同时Blocked状态里的B、C线程重新变成Runable状态
    VI. A放弃CPU(A自主退出/时间到了),下一轮的抢锁开始(可能使A抢占成功,也可能是B或C)
  2. 每个锁都有自己的block队列(阻塞队列)
  3. 即使不是同一方法,但只要是指向同一对象,争抢的就是同一把锁

B.静态方法
eg :public synchronized static void staticMethod() { }

  1. 类里的锁有时候叫全局锁

2.2.3程序测试

1.程序测试_普通方法:

public class SynchronizedDemo {
    public synchronized void method() {
        // 具体代码
        for (int i = 0; i < 10; i++) {
            System.out.println( Thread.currentThread().getName() + ": " + i);
            //打印当前线程的名称
            if(i==9){
                System.out.println("________________");
            }
        }
    }
    
    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                object.method();
            }
        }
        private SynchronizedDemo object;
        MyThread(SynchronizedDemo object) {
            this.object = object; //同一个对象
            //this.object = new SynchronizedDemo(); //不同对象 争抢的是不同的锁 进程不断切换
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo object = new SynchronizedDemo();
        Thread t = new MyThread(object); //object指向同一个对象
        t.start();
        while (true) {
            object.method();
        }
    }
}

测试条件:
public void method(){} //方法不加锁
this.object = object; //同一个对象

运行结果一(一部分):
method 方法中 i 不能顺次执行0~9,main 与 Thread-0 不停的抢占 CPU 。
一部分运行结果
测试条件:
public synchronized void method() //方法加锁
this.object = new SynchronizedDemo(); //不同对象 争抢的是不同的锁 进程不断切换

运行结果二(一部分):
因为不是同一个对象,所以抢的不是同一把锁, 虽然 method 方法加锁,但是 i 还是不能从0~9顺次执行,main 与 Thread-0 不停的抢占 CPU 。
在这里插入图片描述
测试条件:
public synchronized void method() //方法加锁
this.object = object; //同一个对象

运行结果三(一部分):
method 方法中 i 顺次执行0~9,必须等一个线程的 method 方法执行完毕,另一个线程才可以抢占CPU(并不意味着一定可以抢占成功,结果可能会出现:main 线程两次或者多次(main: 0 ~ 9)之后,Thread-0 线程抢占CPU成功出现(Thread-0:0 ~9))。
在这里插入图片描述
2.程序测试_静态方法

public class StaticMethod {
    public static synchronized void staticMethod() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":  " + i);
        }
    }

    private static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                StaticMethod.staticMethod(); //同一把锁
            }
        }
    }

    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while (true) {
            StaticMethod.staticMethod(); //同一把锁
        }
    }
}

2.2.4synchronized代码表现

表现 锁的对象 什么时期加 什么时候释放
修饰普通方法 this 进入方法 正常/异常退出方法
修饰静态方法 进入方法 正常/异常退出方法
修饰代码块 小括号内引用指向的对象 进入代码块 正常/异常退出代码块

补充:SynchronizedDemo.class就是类 SynchronizedDemo 的对象

3.synchronized和原子性/可见性/重排序的关系

3.1原子性

线程之间必须锁的是同一把锁,才可以保证原子性。

3.2可以保证一定限度的可见性

解释:加锁和释放锁会伴随着工作内存的刷新,在这个时机,保证了内存的可见性,但临界区(加锁-》释放锁之间的代码)的执行期间不做任何保证

public class SyncVisible {
    private static int n = 0;
    private static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                synchronized (SyncVisible.class) {
                    n++;
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyThread();
        thread.start();
        for (int i = 0; i < 100000; i++) {
            synchronized (SyncVisible.class) {
                n--;
            }
        }
        thread.join();
        System.out.println(n);
    }
}

结果:
在这里插入图片描述

class Demo{
    public static synchronized void m1(){}//静态同步方法
    public static void m2(){}//静态方法
    public synchronized void m3(){}//同步方法
    public void m4(){}//普通放法
}
//d1\d2指向同一对象
Demo d1 = new Demo(); Demo d2 = d1;
Demo d3 = new Demo();
//不互斥:允许穿插执行 但不能影响你代码的执行
线程A 线程B 是否互斥 补充
m1 m1 互斥 抢同一把锁
m1 m2 不互斥 B线程无与锁有关的东西
d1.m3() d3.m3() 不互斥 对象不同 两把锁
d1.m3() d1.m4() 不互斥 B线程无与锁有关的东西

3.3解决代码的重排序问题

锁之前的语句无法重排序到临界区(锁里面的代码),临界区内部的无法重排序到外部。

eg:A B C synchronized{D E F} G H
允许{D E F}穿插在其他代码中执行,不允许{D E F}顺序不可更改

3.4synchronized缺点

理论上所有问题都可以用synchronized解决,但成本非常大(线程调度的成本非常大)

4.volatile(稍轻量级)

4.1volatile—变量修饰符(修饰变量)

语法:用来修饰变量—变量的修饰符(属性/静态属性)
注意:修饰局部变量无意义,因为局部变量不是线程之间共享的。

  1. 可以保证该变量的可见性问题
  2. 可以保证部分代码的重排序问题
    eg:Object o = new Object();
    //可保证1.new() 2.初始化 3.对象到引用的赋值 —— 顺序不变

4.2赋值语句是否原子?

关于原子性:
int a = 100;        int n;
boolean b = true;   boolean m;
long c = 1ooL;      long o;
是原子吗?
字面量              变量
n = 0; 是原子的     n = a; 不是原子的
m = true; 是       m = b; 不是
o = 1000L; 不是    o = c; 不是 
//long64位的 Java有可能运行在64/32位机器上 若运行在32位机器上则不是原子的(分为高32位和低32位)
//long、double64位
//float是原子的
基本数据类型变量被赋值 基本数据类型变量被赋值 是不是 原子的
boolean、byte、short、int、char、float 字面量 原子
boolean、byte、short、int、char、float 变量 不是原子
long、double 任何情况 不是原子

注意

只有可见性问题,没有原子性问题,可以使 volatile 发挥作用。

volatitle long a = 10; //是原子的

5.单例模式

  • 单例模式:程序运行中,类(Person)只会生成一个(Person)对象(实例),大家共用的是同一个实例。
  • 场景:配置项 —— 对象
  • 实现:1.饿汉模式 2.懒汉模式

5.1饿汉模式

// 饿汉模式的单例
public class SingletonHungry {
    // 不允许外边调用构造方法
    private SingletonHungry() {}
    private static final SingletonHungry instance = new SingletonHungry();
    public static SingletonHungry getInstance() {
        return instance;
    }
}

5.2懒汉模式

5.2.1懒汉模式-单线程版

/**
 * 懒汉模式的单例:单线程环境下正确
 *                  多线程环境下有线程安全问题 
 *                  (可见性和原子性的问题)
 */
public class SingletonLazyVersion1 {
    private SingletonLazyVersion1() {}

    // 多线程下instance可见性不能保证
    private static SingletonLazyVersion1 instance = null;
    // getInstance 被第一次调用时,意味着有人需要 instance
    // 再进行初始化
    public static SingletonLazyVersion1 getInstance() {
        //多线程情况下原子性不能保证
        // 原子开始
        if (instance == null) {   
            instance = new SingletonLazyVersion1(); 
        }
        // 原子结束
        return instance;
    }
}

多线程环境下有线程安全问题:
1.可见性: private static SingletonLazyVersion1 instance = null;
2.原子性:if (instance == null) { instance = new SingletonLazyVersion1(); }

5.2.2懒汉模式-多线程版-性能低

// 线程安全版本的懒汉单例
public class SingletonLazyVersion2 {
    private SingletonLazyVersion2() {}
    //不用给instance专门加锁 已经保证了instance的可见性了
    //因为getInstance方法释放锁时内存都是可见的(清理工作内存的缓存 读到最新的i) 
    private static SingletonLazyVersion2 instance = null;
    //整体用synchronized加锁 保证原子性
    //getInstance()方法是static 对其加锁相当于全局锁(大家用的都是同一把锁) 
    public synchronized static SingletonLazyVersion2 getInstance() {
        if (instance == null) {
            instance = new SingletonLazyVersion2();
        }
        return instance;
    }
}

缺点:虽保证线程安全,但锁的粒度过大(开始锁住 结束才释放 每次都在竞争锁)

5.2.3懒汉模式-多线程版-二次判断-性能高

public class SingletonLazyVersion3 {
    private SingletonLazyVersion3() {}
    //注意:instance必需加volatile  才能防止synchronized里面代码的重排序带来的问题
    private volatile static SingletonLazyVersion3 instance = null; 
    private static SingletonLazyVersion3 getInstance() {
        if (instance == null) {
            //只有在初始化时才需要抢锁 保证只有一个在线程初始化
            synchronized (SingletonLazyVersion3.class) {
                if (instance == null) { //二次判断法 确保instance没有被初始化
                    instance = new SingletonLazyVersion3();
                }
            }            
            /* 直接写 错误 必须二次判断
            //A B C 都为null return
            //假设A抢到锁  A去初始化
            //A初始完后  B、C接着枪锁  B抢到锁B初始化 
            //二次初始化发生错误
            //注意:开始抢锁到抢到锁有时间间隔  有可能期间其他对像已经初始化过了
            synchronized (SingletonLazyVersion3.class) {
                instance = new SingletonLazyVersion3();
            }
            */
        }
        return instance; //不为空 return
    }
}
/*
//假设A、B两个线程  
//A进去发现instance = null  加锁  
//二次判断instance = null 
//假设执行初始化时发生重排序:new——赋值——初始化  A被new——赋值,然后A被切出去 
//B进来发现 instance 已经被初始化 退出去 
//但此时 instance 不可用 发生错错误
//注意:synchronized只能保证外面的程序不会重排序到里面 不能保证里面的代码的顺序
*/

5.3锁的粒度问题

在这里插入图片描述

6.github链接

点击查看相关代码

发布了71 篇原创文章 · 获赞 3 · 访问量 1251

猜你喜欢

转载自blog.csdn.net/qq_43361209/article/details/103000818
今日推荐