线程安全及Synchronized关键字原理

文章介绍了线程安全的概念,通过多线程售票示例展示了线程不安全问题,如重复售票和负数售票。接着分析了问题的根本原因在于多个线程对共享数据的写操作。文章详细讲解了如何使用synchronized关键字解决线程安全问题,包括同步代码块和同步方法的语法与示例,并探讨了synchronized的实现原理,涉及Monitor监视锁和对象头内存结构。
摘要由CSDN通过智能技术生成

目录

内容介绍

线程安全

什么是线程安全

 线程安全问题演示

 第一步:创建售票线程类

第二步:创建测试类

线程安全问题分析

线程安全问题解决

同步代码块

语法

同步锁

示例代码

 同步方法

语法

同步锁

示例代码

Synchronized实现原理

字节码分析

查看字节码

静态代码块

非静态同步方法

静态同步方法

 Monitor监视锁

对象头内存结构

普通对象头

数组对象头

MarkWord结构

 Monitor原理

加锁原理

阻塞原理

等待唤醒

总结 


内容介绍

  1. 线程安全和不安全介绍
  2. 线程不安全问题演示和分析
  3. 线程不安全问题解决
  4. Synchronized关键字原理分析

线程安全

什么是线程安全

单线程售票示意图:

 多线程售票示意图:

结论:

如果多个线程操作一个资源得到的结果,和单个线程操作一个资源得到的结果是相同的,则线程安全;反之,则线程不安全。

 线程安全问题演示

为了演示线程安全问题,我们采用多线程模拟多个窗口同时售卖《两只老虎》电影票。

 第一步:创建售票线程类

package com.multithread.thread;

public class Ticket implements Runnable {
    private int ticktNum = 100;
    
    public void run() {
        while(ticktNum > 0){
            //1.模拟出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2.打印进程号和票号,票数减1
            String name = Thread.currentThread().getName();
            System.out.println("线程"+name+"售票:"+ticktNum--);
        }
    }
}

第二步:创建测试类

package com.multithread.demos;

import com.multithread.thread.Ticket;

public class TicketDemo {
    public static void main(String[] args){
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket, "窗口1");
        Thread thread2 = new Thread(ticket, "窗口2");
        Thread thread3 = new Thread(ticket, "窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果如下:

程序出现了两个问题:

1. 相同的票数,比如5这张票被卖了两回。

2. 不存在的票,比如0票与-1票,是不存在的。

线程安全问题分析

线程安全问题都是由全局变量及静态变量引起的。

若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

综上所述,线程安全问题根本原因:

  1. 多个线程在操作共享的数据;
  2. 操作共享数据的线程代码有多条;
  3. 多个线程对共享数据有写操作;

线程安全问题解决

        要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

        为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。本文重点介绍前两种,后续会出其他同步机制及原理。

  1. 同步代码块(synchronized)
  2. 同步方法(synchronized)
  3. 同步锁(ReenreantLock)
  4. 特殊域变量(volatile)
  5. 局部变量(ThreadLocal)
  6. 阻塞队列(LinkedBlockingQueue)
  7. 原子变量(Atomic*)

同步代码块

        synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法
synchronized(同步锁){
     需要同步操作的代码
}
同步锁

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象可以是任意类型。
  2. 多个线程要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

示例代码
package com.multithread.thread;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象
    Object obj = new Object();

    public void run() {
        while(true){
            synchronized (obj){
                if(ticktNum > 0){
                    //1.模拟出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程"+name+"售票:"+ticktNum--);
                }
            }
        }
    }
}

执行结果如下:线程的安全问题,解决了。

 同步方法

        使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

语法
public synchronized void method(){
   可能会产生线程安全问题的代码 
}
同步锁
  1. 对于非static方法,同步锁就是this。
  2. 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
示例代码
package com.multithread.thread;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象
    Object obj = new Object();

    public void run() {
        while(true){
            sellTicket();
        }
    }

    private synchronized void sellTicket(){
        if(ticktNum > 0){
            //1.模拟出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2.打印进程号和票号,票数减1
            String name = Thread.currentThread().getName();
            System.out.println("线程"+name+"售票:"+ticktNum--);
        }
    }
}

执行结果如下:线程的安全问题,解决了。

Synchronized实现原理

字节码分析

        Synchronized是基于jvm指令实现的,具体可参考字节码文件。

查看字节码

        使用javap -v xx.class命令查看字节码文件

        比如本文:

  javap -v Ticket.class

静态代码块

非静态同步方法

静态同步方法

结论:

通过字节码文件可以看到,synchronized关键字底层使用的是monitor指令

 Monitor监视锁

        那monitor底层又是怎么实现的呢?这就要从对象的内存结构开始了解了。下面我们一起来学习下。

对象头内存结构

以32位虚拟机为例

普通对象头

数组对象头

MarkWord结构

 Monitor原理

        Monitor被翻译为监视器或管程

        每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

加锁原理

  1. obj是java提供的对象,monitor是操作系统提供的
  2. 刚开始Monitor中的Owner为null
  3. 当Thread-2被加上重量级锁的时候,他Mark Word的内容发生改变,他就会找到一个monitor与之关联,并记录了指向monitor的地址,不再记录之前的信息。
  4. monitor中的owner则记录了谁是这把锁的主人,此时记录了Thread-2
  5. 此时线程2成功获取到了monitor。
阻塞原理

  • 1.新线程Thread-1,他会先检查obj是否关联到了一个monitor锁,此时发现已经关联
  • 2.然后检查这个monitor是否有一个主人Owner,发现他有Owner为Thread-2
  • 3.所以Thread-1无法获取锁,此时Thread-1会通过EntryList(等待队列或阻塞队列)与monitor发生关联,然后进行BLOCKED(阻塞)状态。
  • 4.如果此时Thread-3来了,再同上面过程检查一遍后进入EntryList与Thread-1一同等待
等待唤醒

Thread-2释放锁之后,由Thread-1和Thread-3进行竞争决定谁成为新Owner

注意

    synchronized必须是进入同一个对象的monitor才有上述的效果

  •     不加synchronized的对象不会关联监视器,不遵从上述规则

总结 

  • 描述什么是线程安全和不安全

        多线程操作一个共享资源得到的结果,和单线程操作一个共享资源得到的结果是相同的,则线程安全;反之,则线程不安全。

  • 说出导致线程不安全的原因
  1. 多线程间有共享变量
  2. 操作共享变量的代码有多条
  3. 多线程对共享变量有写操作
  • 说出保证线程安全的解决方案和原理

        Synchronized同步代码块、同步方法可以保证线程安全

        Synchronized底层原理是基于monitor监视锁实现

                Monitorenter:获取锁命令

                Moniterexit:释放锁指令

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值