JAVA语言之多线程编程之synchronized和Lock
小标 2019-03-04 来源 : 阅读 877 评论 0

摘要:本文主要向大家介绍了JAVA语言之多线程编程之synchronized和Lock,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

本文主要向大家介绍了JAVA语言之多线程编程之synchronized和Lock,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助。

JAVA语言之多线程编程之synchronized和Lock

前言


在高并发多线程应用场景中对于synchronized和Lock的使用是很普遍的,这篇文章我们就来进行这些知识点的学习,比如说:公平锁与非公平锁、乐观锁与悲观锁、线程间通信、读写锁、数据脏读等知识内容。
目录:
1.同步问题的产生与案例代码
2.synchronized解决同步问题
3.Lock解决同步代码问题
4.公平锁与非公平锁
5.乐观锁与悲观锁
6.synchronized与Lock比较


同步问题案例


这个问题在我们日常生活中非常常见,比如说:秒杀物品的库存数据、火车票剩余票等就是有同步问题,下面我们通过代码来解释这个问题产生的原理:


package com.ckmike.mutilthread;

import java.util.concurrent.TimeUnit;

/**
 * SynchronizedQuestionDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-21 下午1:34
 * @copyright ckmike
 **/
public class SynchronizedQuestionDemo {

    public static void main(String[] args) {
        // 只有10张票
        TicketService ticketService = new TicketService(10);

        Thread buy1 = new Thread(ticketService);
        buy1.setName("buy1");
        Thread buy2 = new Thread(ticketService);
        buy2.setName("buy2");
        Thread buy3 = new Thread(ticketService);
        buy3.setName("buy3");
        Thread buy4 = new Thread(ticketService);
        buy4.setName("buy4");

        buy1.start();
        buy2.start();
        buy3.start();
        buy4.start();
    }
}

class TicketService implements Runnable{
    private int ticket_store;

    public TicketService(int ticket_store) {
        this.ticket_store = ticket_store;
    }

    @Override
    public void run() {
        while (true) {
            if (ticket_store > 0) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 输出卖票信息
                System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
            }else{
                            break;
                        }
        }
    }
}


上面的执行结果明显是不符合我们的预期的,四个线程同时去买票,对余票数据的判断存在问题,这就是数据脏读的场景。


解决办法:在做售票这个操作时,对于ticket_store的操作同一时刻只能一个线程操作,那么我们这里就会用到锁这个概念了,对于共享数据java中解决数据脏读可以通过synchronized和Lock去解决。


现在我们带着这个问题来了解synchronized和Lock。


synchronized


JVM中每个对象都有一个监控器可以作为锁。当线程试图访问同步代码时,必须先获得对象锁(对象监视器),退出或抛出异常时必须释放锁。Synchronzied实现同步的表现形式分为:代码块同步和方法同步。


同步代码块


在编译后通过将Monitor Enter指令插入到同步代码块的开始处,将Monitor Exit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个Monitor(对象监控器)与之关联,线程执行Monitor Enter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象锁。


同步方法


从class文件结构中可知,synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的对象锁,实现方法同步。


虽然同步方法和同步代码块实现细节不同,但本质上都是对一个对象监视器(monitor)的获取(对象锁的获取)。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。


synchronized解决数据脏读问题


package com.ckmike.mutilthread;

import java.util.concurrent.TimeUnit;

/**
 * SynchronizedQuestionDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-21 下午1:34
 * @copyright ckmike
 **/
public class SynchronizedQuestionDemo {

    public static void main(String[] args) {
        // 只有10张票
        TicketService ticketService = new TicketService(10);

        Thread buy1 = new Thread(ticketService);
        buy1.setName("buy1");
        Thread buy2 = new Thread(ticketService);
        buy2.setName("buy2");
        Thread buy3 = new Thread(ticketService);
        buy3.setName("buy3");
        Thread buy4 = new Thread(ticketService);
        buy4.setName("buy4");

        buy1.start();
        buy2.start();
        buy3.start();
        buy4.start();
    }
}

class TicketService implements Runnable{
    private int ticket_store = 100;

    public TicketService(int ticket_store) {
        this.ticket_store = ticket_store;
    }

    @Override
    public void run() {
        while (true) {
            if (ticket_store > 0) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (this) {
                    if(ticket_store > 0) {
                        // 输出卖票信息
                        System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
                    }else {
                        break;
                    }
                }
            }else {
                break;
            }
        }
    }
}


上面的方式是使用同步代码块实现的同步,解决了数据脏读的问题。我们也可以通过同步方法来解决这个问题,相比较同步方法,同步代码块效率要高些。


上面的代码我们是对于同一个TicketService实例进行多线程操作,所以可以达到同步效果,如果我们使用的是四个不同的实例,那么他们之间就不再是互斥的,因为Java中的锁是对象锁,不同实例对象锁是不一样的,可自行验证。


如果是静态同步方法,那么获取的应该是该类的锁,锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果。


结合上面思考:同步实例方法,同步类方法,synchronized(this),synchronized(ClassName.class)他们之间的一个关系就出来了,以及他们的应用场景也就出来了。


synchronized线程间通信问题


场景描述:现在我有三个线程分别为线程A,线程B,和线程C,三个线程之间有先后顺序的,A操作完了,B才可以操作,B操作完了,C才可以操作。那么这个时候就需要进行线程之间的通信,然线程知道什么时候该自己执行。


分析:在线程A执行期间,B线程一直等待A的通知,B执行期间,C一直等待B的通知。


package com.ckmike.mutilthread;

/**
 * BackupDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-20 下午1:51
 * @copyright ckmike
 **/
public class BackupDemo {

    public static void main(String[] args) {
            DataTool dataTool = new DataTool();
            BackUpA A = new BackUpA(dataTool);
            BackUpB B = new BackUpB(dataTool);
            BackUpC C = new BackUpC(dataTool);
            A.start();
            B.start();
            C.start();
    }
}

class BackUpA extends Thread{
    private DataTool dataTool;

    public BackUpA(DataTool dataTool) {
        this.dataTool = dataTool;
    }

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

class BackUpB extends Thread{
    private DataTool dataTool;

    public BackUpB(DataTool dataTool) {
        this.dataTool = dataTool;
    }

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

class BackUpC extends Thread{
    private DataTool dataTool;

    public BackUpC(DataTool dataTool){
        this.dataTool = dataTool;
    }

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

class DataTool{

    volatile public String prevA = "A";

    // 备份到A数据源
    synchronized public void backup2A(){
        try {
            while ("C".equals(prevA)) {
                wait();
            }

            for(int i=0; i<2;i++){
                System.out.println("backup2A数据源");
            }
            prevA = "B";
            notifyAll();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 备份到B数据源
    synchronized public void backup2B(){
        try{
            while ("A".equals(prevA)){
                wait();
            }
            for (int i=0; i<2; i++){
                System.out.println("backup2B数据源");
            }
            prevA = "C";
            notifyAll();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    // 备份到c数据源
    synchronized public void backup2C(){
        try{
            while ("B".equals(prevA)){
                wait();
            }
            for (int i=0; i<2; i++){
                System.out.println("backup2C数据源");
            }
            prevA = "C";
            notifyAll();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}


通过volatile和synchronized和wait()\notifyAll()结合实现线程间通信,借助标识进行线程顺序执行。


ReentrantLock


在Java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁,后面我们会分析)。在Lock接口出现之前,Java程序是靠synchronized关键字(后面分析)实现锁功能的,而JAVA SE5.0之后并发包中新增了Lock接口用来实现锁的功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性


ReentrantLock解决同步问题


package com.ckmike.mutilthread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * SynchronizedQuestionDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-21 下午1:34
 * @copyright ckmike
 **/
public class SynchronizedQuestionDemo {

    public static void main(String[] args) {
        // 只有10张票
        TicketService ticketService = new TicketService(10);

        Thread buy1 = new Thread(ticketService);
        buy1.setName("buy1");
        Thread buy2 = new Thread(ticketService);
        buy2.setName("buy2");
        Thread buy3 = new Thread(ticketService);
        buy3.setName("buy3");
        Thread buy4 = new Thread(ticketService);
        buy4.setName("buy4");

        buy1.start();
        buy2.start();
        buy3.start();
        buy4.start();
    }
}

class TicketService implements Runnable{
    private int ticket_store = 100;

    // 默认是非公平锁
    private Lock lock = new ReentrantLock();

    public TicketService(int ticket_store) {
        this.ticket_store = ticket_store;
    }

    @Override
    public void run() {
        while (true) {
            if (ticket_store > 0) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
                if(ticket_store > 0) {
                    // 输出卖票信息
                    System.out.println(Thread.currentThread().getName() + ".....sale...." + ticket_store--);
                }else {
                    break;
                }
                lock.unlock();
            }else {
                break;
            }
        }
    }
}


使用Lock同样可以解决数据多线程同步问题。
关于ReentrantLock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。


ReentrantLock线程间通信


package com.ckmike.mutilthread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * BackupDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-20 下午1:51
 * @copyright ckmike
 **/
public class BackupDemo {

    public static void main(String[] args) {
            DataTool dataTool = new DataTool();
            BackUpA A = new BackUpA(dataTool);
            BackUpB B = new BackUpB(dataTool);
            BackUpC C = new BackUpC(dataTool);
            A.start();
            B.start();
            C.start();
    }
}

class BackUpA extends Thread{
    private DataTool dataTool;

    public BackUpA(DataTool dataTool) {
        this.dataTool = dataTool;
    }

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

class BackUpB extends Thread{
    private DataTool dataTool;

    public BackUpB(DataTool dataTool) {
        this.dataTool = dataTool;
    }

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

class BackUpC extends Thread{
    private DataTool dataTool;

    public BackUpC(DataTool dataTool){
        this.dataTool = dataTool;
    }

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

class DataTool{

    volatile public String prevA = "A";

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    // 备份到A数据源
    public void backup2A(){
        try {
            lock.lock();
            while ("C".equals(prevA)) {
                condition.await();
            }
            for(int i=0; i<2;i++){
                System.out.println("backup2A数据源");
            }
            prevA = "B";
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    // 备份到B数据源
    public void backup2B(){
        try{
            lock.lock();
            while ("A".equals(prevA)){
                condition.await();
            }
            for (int i=0; i<2; i++){
                System.out.println("backup2B数据源");
            }
            prevA = "C";
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    // 备份到c数据源
    public void backup2C(){
        try{
            lock.lock();
            while ("B".equals(prevA)){
                condition.await();
            }
            for (int i=0; i<2; i++){
                System.out.println("backup2C数据源");
            }
            prevA = "C";
            condition.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}


利用Condition可以实现与wait()\notifyAll()一样的功能。Condition.await()等同于wait(),condition.signalAll()等同与notifyAll()。


公平锁与非公平锁


公平锁:按照线程获取锁的顺序来分配,FIFO。
非公平锁:是一种获取锁的抢占机制,是随机获取锁。可能造成某些线程一直拿不到锁。
synchronized是非公平锁,ReentrantLock可以通过isFair设置为公平锁,默认是非公平锁。


乐观锁与悲观锁


乐观锁与悲观锁的概念不是JAVA的概念,而是针对关系型数据库数据更新时的一种解决方案。
乐观锁:就是认为数据冲突的可能性比较小,只有当事物提交时才回去判断是否在读取数据后,是否有其他事务修改了该数据,如果有,则当前事务进行回滚。可以这样简单理解:一条数据其中有一个字段是version,每次的更新操作都会自动+1,当你读出这条数据后,如果有其他事务修改了,那么version就与你提交的version不相等,那么这个事务就不会被提交。


悲观锁:则认为数据冲突是大概率事件,所以每次进行修改之前都会先获取该数据的锁,类似于synchronized,所以花费时间较多,效率就会比较低。悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。而悲观锁又分为共享锁和排他锁。


共享锁:就是对于多个不同的事务,对同一个资源共享同一个锁,类似于一个门多把钥匙。
排它锁:排它锁与共享锁相对应,类似于一个门只有一把钥匙。
行锁:这个就是字面上的意思,给数据行加上锁。比如:SELECT * from user where id = 1  lock in share mode; 就是对id=1的数据行加了锁。这个锁就是行锁。
表锁:给表加上锁。


这一部分应该是数据库中的概念,我放到这里就是因为曾经因为面试问得我一脸懵逼,所以就在这里简单的介绍一下,我后面还会写关于数据库关于锁,索引、事务隔离等相关的文章。


ReentrantReadWriteLock


关于ReentrantLock进行更细粒度的锁,就是这个ReentrantReadWriteLock读写锁,可以针对读和写进行加锁。特别要注意:只有读读是不用加锁,属于读读共享;但是只要有写就一定要加锁互斥,比如读写互斥,写读互斥,写写互斥。


读写案例:


package com.ckmike.mutilthread;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * ReadWriteDemo 简要描述
 * <p> TODO:描述该类职责 </p>
 *
 * @author ckmike
 * @version 1.0
 * @date 18-12-21 下午4:48
 * @copyright ckmike
 **/
public class ReadWriteDemo {

    public static void main(String[] args) {
        ReadWriteService readWriteService = new ReadWriteService();

        ReadThread read1 = new ReadThread(readWriteService);
        ReadThread read2 = new ReadThread(readWriteService);
        read1.setName("A");
        read2.setName("B");
        read1.start();
        read2.start();
        WriteThread write1 = new WriteThread(readWriteService);
        WriteThread write2 = new WriteThread(readWriteService);
        write1.setName("C");
        writ    

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注编程语言JAVA频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 1 不喜欢 | 0
看完这篇文章有何感觉?已经有1人表态,100%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程