Java线程的同步机制

线程同步机制

一、锁

线程安全问题的产生前提:多个线程并发访问共享数据。
解决方案:将并发访问转换为串行访问,就是按照这种思路保证线程安全。

  • 一个线程只有先持有锁,才能对共享数据进行访问。
  • 一个锁一次只能被一个线程持有
  • 线程对共享数据结束访问后,必须要归还锁。

JVM把锁分为内部锁和显示锁

  • 内部锁通过synchronized关键字实现
  • 显示锁通过java.concurrent.locks.Lock接口的实现类实现。

二、锁的作用

锁可以实现对共享数据的安全访问,保障线程的原子性,可见性与有序性。

  • 锁通过互斥保护原子性,一个锁只能被一个线程持有,使得临界区代码一次只能被一个线程执行
  • 可见性的保障是通过写线程冲刷处理器缓存和读线程刷新处理器缓存这两个动作实现的。在Java平台中,锁的获得隐含着刷新处理器缓存动作,锁的释放隐含着冲刷处理器缓存的动作
  • 锁能够保障有序性。

注意:使用锁保障线程的安全性,必须满足下面条件

  • 必须使用同一个锁
  • 即使是读取共享数据的线程,也需要上同步锁

三、锁相关的概念

1.可重入性

一个线程持有该锁的时候,能够再次申请该锁,为可重入,否则为不可重入

2.锁的争用与调度

java中内部锁属于非公平锁,显示锁及支持公平锁,又支持非公平锁

3.锁的粒度

一个锁可以保护的共享数据的数量大小,称为锁的粒度。
粒度粗,共享数据量大;粒度细,反之

  • 粒度过粗会导致线程进行不必要的等待
  • 粒度过细会增加锁调度的开销

四、内部锁:synchronized

java中的每个对象都有一个与之关联的内部锁,也称为监视器(Monitor),是一种排它锁,可以保障原子性,可见性,有序性。
内部锁是通过synchronized关键字实现

  • 可以修饰代码块
package com.day04;

/**
 * @author: Xu TaoSong
 * @date: 2021/2/3 20:37
 * @description: TODO
 * @modifiedBy:
 */
public class Test01 {
     
    public static void main(String[] args) {
     
        Test01 test = new Test01();
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                test.mm();
            }
        }).start();
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                test.mm();
            }
        }).start();
    }
    public void mm(){
     
        synchronized (this) {
     
            for (int i = 0; i < 99; i++) {
     
                System.out.println(Thread.currentThread().getName() + "->" + i);
            }
        }
    }
}

在代码块中,可以使用一个常量作为锁对象

    public static final Object o = new Object();
    public void mm(){
     
        synchronized (o) {
     
            for (int i = 0; i < 99; i++) {
     
                System.out.println(Thread.currentThread().getName() + "->" + i);
            }
        }
    }
  • 可以修饰方法
package com.day04;

/**
 * @author: Xu TaoSong
 * @date: 2021/2/3 20:37
 * @description: TODO
 * @modifiedBy:
 */
public class Test01 {
     
    public static void main(String[] args) {
     
        Test01 test = new Test01();
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                test.mm();
            }
        }).start();
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                test.mm();
            }
        }).start();
    }
    synchronized public void mm(){
     
        for(int i = 0;i<99;i++){
     
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }
}

synchronized关键字的锁对象

修饰代码块的锁可以自定义指定
修饰实例方法的锁是当前对象this
修饰静态方法的锁是当前类的class,称为“类锁”。

执行效率比较

对方法上加synchronized比较笼统,使用代码块可以更加灵活的配置资源的并发。
同步方法,锁的粒度粗,执行效率比较低
同步代码块,锁的粒度细,执行效率比较高

根据实际情况来配置


在同步执行期间发生了异常,将会释放同步锁

五、volatile关键字

作用:

使得变量在多个线程之间可见,解决变量的可见性
将会强制线程在公共内存中读取变量的值,而不是从工作内存中读取。

volatile的非原子性

package com.volitale;

/**
 * @author: Xu TaoSong
 * @date: 2021/2/5 14:53
 * @description: TODO
 * @modifiedBy:
 */
public class Test02 {
     
    public static void main(String[] args) {
     
        for(int i = 0;i<10;i++){
     
            new Thread(new MyThread()).start();
        }
    }
    static class MyThread extends Thread{
     
        volatile public static int count = 0;
        public static void add(){
     
            for(int i = 0;i<1000;i++){
     
                count++;
            }
            System.out.println(Thread.currentThread().getName()+"count= " +count);
        }

        @Override
        public void run() {
     
            super.run();
            add();
        }
    }
}

六、Volatile和synchronized的不同:

  • volatile是线程同步的轻量级实现,性能比synchronized要好。volatile是修饰变量的,synchronized是修饰代码块或者方法的。
  • 多线程访问volatile变量不会发生阻塞,synchronized可能会发生阻塞
  • volatile能保证数据的可见性,synchronized不仅可以保证原子性,还可以保证可见性。

七、CAS的简介

CAS(Compare And Swap)是由硬件实现的。
CAS可以将read-modify-write这些的操作转换为原子操作。

实现方式:

在把数据更新到主内存之前,再次读取主内存中的值,如果当前值和期望值一致,才进行更新操作。否则,就撤销该次操作,或者重新提交。

CAS的优缺点

优点:提高了并发性能。
缺点:

  • 只能保证一个变量的原子操作
  • 长时间自旋导致cpu开销大

自旋:表示CAS更新失败,重复提交操作

  • ABA问题,比如:一个变量从1改到2,在从2改到1,CAS会认为此时内存没有发生改变,但实际上是发生了改变的。解决方法是在变量钱加上一个版本号标识。

你可能感兴趣的:(java多线程与并发,java,多线程,并发编程)