【并发编程八:线程安全问题分析及锁synchronized的介绍(1)】

【衔接上一章 【并发编程七:Java中的线程池(4)-线程池规范、监控、关闭】

2.1什么是线程安全?

  • 白话一点,线程安全是指多个线程同时运行的时候,会不会产生预期之外的结果?比如我要对i=0的变量进行+1操作,1个线程执行的时候,结果是1,两个线程执行的时候,结果应该是2,但是结果却是1,那就产生了线程安全问题;

  • 产生线程安全问题是因为多个线程访问内存中的共享资源,从而产生了冲突、产生了脏数据、产生了意外的结果;

  • 所谓共享资源就是多个线程都可以访问的资源;
    在这里插入图片描述

  • Java代码运行在jvm中,jvm的公共内存是堆、元空间(方法区),同一个jvm进程都能访问这个公共内存,正因为这块内存是大家都可以访问的,是公共的,这样就会导致内存不安全的问题;

  • 所以线程安全指的是,在共享内存中的公共数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险

  • 所以对于像堆、元空间(方法区)这种公共区域,多个线程去操作时,是存在安全问题的;

  • 但是注意:如果多个线程都只是读取共享资源而不去修改,那就不会存在线程安全,只有当至少有一个线程修改了共享资源时才会存在线程安全

2.2线程不安全原因剖析

导致线程不安全的底层核心原因主要有三个:

  • 1、原子性
  • 2、有序性
  • 3、可见性

2.2.1原子性

  • 我们经常听到数据库中事务的原子性,数据库事务的原子性是指一组对数据库的操作,要么最终全部成功执行,要么最终全部回滚;
  • 多线程并发编程的原子性是指一组指令被执行时,不受其他指令的干扰,比如我们说CAS(Compare & Set)是原子的,java.util.concurrent.atomic下的原子操作类等,这里的原子性其实指的是隔离性,也就是一组操作不能被别的线程干扰;

2.2.2线程上下文切换

  • 1、使用多线程的目的是为了充分利用多核CPU;
  • 2、当创建很多线程,CPU不够用了,此时就是一个CPU来应付多个线程;
  • 3、操作系统采用时间片切换的方式把每个线程分配给CPU去执行,线程在时间片内占用 CPU 执行任务,当线程使用完时间片后,就会处于就绪状态并让出 CPU从而让其它线程去使用,这就是上下文切换;

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.3synchronized解决线程安全

  • 前面分析的线程安全问题可以采用synchronized来解决;
  • synchronized是 Java 中的一个关键字,它的使用就是在代码上加一个- - -synchronized关键字即可;
  • synchronized是Java的内置锁;
  • 这把锁我们看不见,所以也被称为内部锁或监视器锁,它的作用就是在同一时刻只有一个线程能进入synchronized代码块;
  • 加锁:线程在进入 synchronized 代码块前会自动获取内部锁,此时其它线程访问该同步代码块时会被阻塞挂起;
  • 解锁:拿到内部锁的线程在正常退出代码块、抛出异常后、在同步代码块内调用了该内置锁资源的wait系列方法时 释放该内置锁;
    synchronized内置锁是排它锁,因为当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁;

2.4synchronized同步锁的使用

2.4.1synchronized使用语法

  • (1)作用在方法上;
 - 修饰非静态方法(普通方法)
 - 修饰静态方法
  • (2)作用在代码块上;(粒度更细,更加灵活)
    • synchronized(this|object) {…}
    • synchronized(类.class) {…}
/**
 * synchronized关键字使用语法
 * 1、方法上
 * 2、代码块上
 *
 */
public class Test00 {

    public static void main(String[] args) {
        Test00 test00 = new Test00();
        test00.test1();
        test00.test2();
    }

    public synchronized void test1() {
        System.out.println("test1......");
    }

    public void test2() {
        synchronized(this) {
            System.out.println("test2......");
        }
    }
}

2.4.2synchronized同步锁分类

(1)对象锁;

/**
 * 两个线程同时访问同一个对象的同步方法,会互斥;
 *
 */
public class Test01 {

    public static void main(String[] args) throws InterruptedException {
        Test01 test01 = new Test01();

        new Thread(() -> test01.eat(), "t1").start();
        new Thread(() -> test01.eat(), "t2").start();
    }

    //对象锁
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

/**
 * 两个线程同时访问同一个对象的同步方法和非同步方法,不互斥;
 */
public class Test02 {

    public static void main(String[] args) throws InterruptedException {
        Test02 t = new Test02();

        new Thread(() -> t.eat(), "t1").start();
        new Thread(() -> t.work(), "t2").start();
    }

    //同步方法
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }

    //非同步方法
    public void work() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 工作......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

(2)类锁;

public class Test05 {

    //new一个对象作为锁
    private Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> eat(), "t1").start();
        new Thread(() -> eat(), "t2").start();
    }

    /*
      两个线程同时访问同一个类的静态同步方法,会互斥;
    public synchronized static void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }*/

    public static void eat() {

        synchronized (Test05.class) { //类锁
            while (true) {
                System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
                ThreadUtils.sleepMilliSeconds(300);
            }
        }
    }

    public static void eat2() {
        synchronized (Test05.class.getClass()) { //类锁
            while (true) {
                System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
                ThreadUtils.sleepMilliSeconds(300);
            }
        }
    }

    public void eat3() {
        synchronized (lock) {//对象锁
            while (true) {
                System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
                ThreadUtils.sleepMilliSeconds(300);
            }
        }
    }
}

2.4.3对象锁

1、两个线程同时访问同一个对象的同步方法,会互斥;
/**
 * 两个线程同时访问同一个对象的同步方法,会互斥;
 *
 */
public class Test01 {

    public static void main(String[] args) throws InterruptedException {
        Test01 test01 = new Test01();
        new Thread(() -> test01.eat(), "t1").start();
        new Thread(() -> test01.eat(), "t2").start();
    }
    //对象锁
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

2、两个线程同时访问同一个对象的同步方法和非同步方法,不互斥;
/**
 * 两个线程同时访问同一个对象的同步方法和非同步方法,不互斥;
 */
public class Test02 {

    public static void main(String[] args) throws InterruptedException {
        Test02 t = new Test02();
        new Thread(() -> t.eat(), "t1").start();
        new Thread(() -> t.work(), "t2").start();
    }
    //同步方法
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
    //非同步方法
    public void work() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 工作......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

3、两个线程同时访问两个对象的同步方法,不互斥;
/**
 * 两个线程同时访问两个对象的同步方法,不互斥;
 *
 */
public class Test03 {

    public static void main(String[] args) throws InterruptedException {
        Test03 t1 = new Test03();
        Test03 t2 = new Test03();
        new Thread(() -> t1.eat(), "t1").start();
        new Thread(() -> t2.eat(), "t2").start();
    }
    //同步方法
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}
4、两个线程同时访问两个对象的同步方法和非同步方法,不互斥;
/**
 * 两个线程同时访问两个对象的同步方法和非同步方法,不互斥;
 */
public class Test04 {

    public static void main(String[] args) throws InterruptedException {
        Test04 t1 = new Test04();
        Test04 t2 = new Test04();
        new Thread(() -> t1.eat(), "t1").start();
        new Thread(() -> t1.work(), "t1").start();

        new Thread(() -> t2.eat(), "t2").start();
        new Thread(() -> t2.work(), "t2").start();
    }
    public synchronized void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
    public void work() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 工作......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

2.4.4类锁

1、两个线程同时访问同一个类的静态同步方法,会互斥;
/**
 * 两个线程同时访问同一个类的静态同步方法,会互斥;
 *
 */
public class Test05 {

    //Test05.class --> Class

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> eat(), "t1").start();
        new Thread(() -> eat(), "t2").start();
    }
    public synchronized static void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}
2、两个线程同时访问不同类的静态同步方法,不互斥;

/**
 * 两个线程同时访问不同类的静态同步方法,不互斥;
 *
 */
public class Test06 {

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> eat(), "t1").start();
        new Thread(() -> Cat.eat(), "t2").start();
    }
    
    public synchronized static void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}
3、两个线程同时访问同一个类的静态同步方法和静态非同步方法,不互斥;
/**
 * 两个线程同时访问同一个类的静态同步方法和静态非同步方法,不互斥;
 *
 */
public class Test07 {

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> eat(), "t1").start();
        new Thread(() -> work(), "t2").start();
    }

    public synchronized static void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }

    public static void work() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 工作......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}
4、两个线程同时访问同一个类的静态同步方法和非静态同步方法,不互斥;
/**
 * 两个线程同时访问同一个类的静态同步方法和非静态同步方法,不互斥;
 *
 */
public class Test08 {

    public static void main(String[] args) throws InterruptedException {
        Test08 t = new Test08();

        new Thread(() -> eat(), "t1").start();
        new Thread(() -> t.work(), "t2").start();
    }

    public synchronized static void eat() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 吃饭......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }

    public synchronized void work() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + " 工作......" + System.currentTimeMillis());
            ThreadUtils.sleepMilliSeconds(300);
        }
    }
}

- 总结:使用的是同一把锁就互斥,不是同一把锁,要不会互斥!

2.5synchronized底层实现原理剖析

synchronized锁的类型分为:偏向锁、轻量级锁、重量级锁;
JDK1.6之前,synchronized只有重量级锁,JDK1.6之后新增了偏向锁、轻量级锁,这是JDK的一种锁优化提升;

2.5.1JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:

  • (1)对象头(Header)
  • (2)实例数据(Instance Data)
  • (3)对齐填充(Padding);
    在这里插入图片描述
对象头(Header):

第一部分是Mark Word,用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32位虚拟机占32 bit,64位虚拟机占 64 bit,下图分别是32位和64位的Mark Word存储结构:
在这里插入图片描述

  • 通过2bit表示锁状态,通过1bit表示无锁和偏向锁;
  • 第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例;
  • (第三部分) 如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;
实例数据(Instance Data):

程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

对齐填充(Padding):

不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍,可以通过-XX:ObjectAlignmentInBytes=16调整默认的对齐大小为16 bytes的倍数;

2.5.2JOL查看Java对象内存布局

JOL (Java?Object Layout),通过JOL输出对象内存布局:

<!-- 查看Java对象内存布局 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.14</version>
</dependency>

在这里插入图片描述

  • OFFSET:偏移地址,单位:字节;
  • SIZE:占用内存大小,单位:字节;
  • TYPE DESCRIPTION:类型描述,其中(object header)表示对象头,单位:字节,一共占用12字节,前面8字节对应对象头中的Mark Word,最后4字节对应类型指针(类型指针默认进行了压缩是4字节,通过-XX:-- - - UseCompressedOops关闭指针压缩,若没有压缩就会变成8字节);
    (loss due to the next object alignment) 表示对齐填充,对齐填充使用了4字节,从而保证整个大小是8字节的整数倍;
  • Instance size: 32 bytes表示当前对象占32字节;
  • Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 内部及外部的空间损失;
  • VALUE:对应内存中当前存储的值;

【衔接下一章 【并发编程九:线程安全问题分析及锁的介绍(2)】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值