多线程几种同步方法

为何要使用同步

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

1.同步方法

即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如下:

Bank.java类如下:

/**
 * @description: 银行
 * @Date : 2019/12/24 下午4:05
 * @Author : 石冬冬-Seig Heil
 */
public class Bank {

    /**
     * 总额
     */
    private int total = 0;

    /**
     * 存钱
     * @param money
     */
    public synchronized void save(int money){
        this.total += money;
        System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
    }

    /**
     * 取钱
     * @param money
     */
    public synchronized void withdraw(int money){
        this.total -= money;
        System.out.println(MessageFormat.format("线程={0},取钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
    }

    /**
     * 查询
     */
    public void query(){
        System.out.println(MessageFormat.format("线程={0},账号剩余={1}",Thread.currentThread().getName(),this.total));
    }

    /**
     * 业务类型
     */
    enum Buz{
        SAVE,WITHDRAW
    }
}

Customer.java类如下:

/**
 * @description: 客户
 * @Date : 2019/12/24 下午4:10
 * @Author : 石冬冬-Seig Heil
 */
public class Customer extends Thread{
    /**
     * 姓名
     */
    private String name;
    /**
     * 额度
     */
    private int money;
    /**
     * 银行
     */
    private Bank bank;
    /**
     * 业务类型
     */
    private Bank.Buz buz;

    public Customer(String name, int money, Bank bank, Bank.Buz buz) {
        super(name);
        this.name = name;
        this.money = money;
        this.bank = bank;
        this.buz = buz;
    }

    @Override
    public void run() {
        System.out.println(name + "准备" + buz.name() + "操作......"+this.money);
        switch (buz){
            case SAVE:
                bank.save(money);
                break;
            case WITHDRAW:
                bank.withdraw(money);
                break;
            default:
                break;
        }
        bank.query();
    }
}

BankBuzTest.java类如下:

/**
 * @description: 银行业务测试
 * @Date : 2019/12/24 下午4:09
 * @Author : 石冬冬-Seig Heil
 */
public class BankBuzTest {

    public static void main(String[] args) {
        Bank bank = new Bank();
        new Customer("小明",200,bank, Bank.Buz.SAVE).start();
        new Customer("小花",300,bank, Bank.Buz.SAVE).start();
    }
}

测试输出结果入下:

小明准备SAVE操作......200
小花准备SAVE操作......300
线程=小明,存钱=200,账号剩余=200
线程=小明,账号剩余=200
线程=小花,存钱=300,账号剩余=500
线程=小花,账号剩余=500

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2.同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

修改Bank类如下

/**
 * @description: 银行
 * @Date : 2019/12/24 下午4:05
 * @Author : 石冬冬-Seig Heil
 */
public class Bank {

    /**
     * 总额
     */
    private int total = 0;

    /**
     * 存钱
     * @param money
     */
    public /*synchronized*/ void save(int money){
        synchronized (this){
            this.total += money;
            System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
        }
    }

    /**
     * 取钱
     * @param money
     */
    public /*synchronized*/ void withdraw(int money){
        synchronized (this){
            this.total -= money;
            System.out.println(MessageFormat.format("线程={0},取钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
        }
    }

    /**
     * 查询
     */
    public void query(){
        System.out.println(MessageFormat.format("线程={0},账号剩余={1}",Thread.currentThread().getName(),this.total));
    }

    /**
     * 业务类型
     */
    enum Buz{
        SAVE,WITHDRAW
    }
}

测试类代码不变,测试结果输出

小明准备SAVE操作......200
小花准备SAVE操作......300
线程=小明,存钱=200,账号剩余=200
线程=小明,账号剩余=500
线程=小花,存钱=300,账号剩余=500
线程=小花,账号剩余=500

如果两个线程共享一个bank实例的时候,由于两个线程共享一个实例锁,而save()方法和query()方法又不是同一个原子事务操作,这就意味着total共享变量将会被线程-小花篡改,所以看到小花存钱后的500。

那怎么保障调用query方法,就是各个线程操作后正确的结果呢?
方法1:不共享同一个bank实例,修改代码如下:

/**
 * @description: 银行业务测试
 * @Date : 2019/12/24 下午4:09
 * @Author : 石冬冬-Seig Heil
 */
public class BankBuzTest {

    public static void main(String[] args) {
        //Bank bank = new Bank();
        new Customer("小明",200,new Bank(), Bank.Buz.SAVE).start();
        new Customer("小花",300,new Bank(), Bank.Buz.SAVE).start();
    }
}

输出结果

小明准备SAVE操作......200
小花准备SAVE操作......300
线程=小明,存钱=200,账号剩余=200
线程=小明,账号剩余=200
线程=小花,存钱=300,账号剩余=300
线程=小花,账号剩余=300

方法2:共享同一个实例,但保持query方法和save方法是原子操作,修改代码如下:

 /**
     * 存钱
     * @param money
     */
    public /*synchronized*/ void save(int money){
        synchronized (this){
            this.total += money;
            System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
            query();
        }
    }

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

3.使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。

ReentrantLock类的常用方法有:ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁

修改Customer类如下

/**
 * @description: 客户
 * @Date : 2019/12/24 下午4:10
 * @Author : 石冬冬-Seig Heil
 */
public class Customer extends Thread{
    /**
     * 锁
     */
    private Lock lock;
    /**
     * 姓名
     */
    private String name;
    /**
     * 额度
     */
    private int money;
    /**
     * 银行
     */
    private Bank bank;
    /**
     * 业务类型
     */
    private Bank.Buz buz;

    public Customer(Lock lock,String name, int money, Bank bank, Bank.Buz buz) {
        super(name);
        this.lock = lock;
        this.name = name;
        this.money = money;
        this.bank = bank;
        this.buz = buz;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            System.out.println(name + "准备" + buz.name() + "操作......"+this.money);
            switch (buz){
                case SAVE:
                    bank.save(money);
                    break;
                case WITHDRAW:
                    bank.withdraw(money);
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

修改Customer类如下

/**
 * @description: 银行业务测试
 * @Date : 2019/12/24 下午4:09
 * @Author : 石冬冬-Seig Heil
 */
public class BankBuzTest {

    public static void main(String[] args) {
        Bank bank = new Bank();
        Lock lock = new ReentrantLock();
        new Customer(lock,"小明",200,bank, Bank.Buz.SAVE).start();
        new Customer(lock,"小花",300,bank, Bank.Buz.SAVE).start();
    }
}

输出结果如下

小明准备SAVE操作......200
线程=小明,存钱=200,账号剩余=200
线程=小明,账号剩余=200
小花准备SAVE操作......300
线程=小花,存钱=300,账号剩余=500
线程=小花,账号剩余=500

注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用。

注:关于Lock对象和synchronized关键字的选择:

  • 最好两个都不用,使用一种java.util.concurrent包提供的机制,能够帮助用户处理所有与锁相关的代码。
  • 如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码。
  • 如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。

4.使用特殊域变量(volatile)实现线程同步

  • volatile关键字为域变量的访问提供了一种免锁机制,
  • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,
  • 因此每次使用该域就要重新计算,而不是使用寄存器中的值
  • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

Bank类修改如下:

package java_current.sync;

import java.text.MessageFormat;

/**
 * @description: 银行
 * @Date : 2019/12/24 下午4:05
 * @Author : 石冬冬-Seig Heil
 */
public class Bank {

    /**
     * 总额
     */
    private volatile int total = 0;

    /**
     * 存钱
     * @param money
     */
    public /*synchronized*/ void save(int money){
        this.total += money;
        System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
        query();
    }

    /**
     * 取钱
     * @param money
     */
    public /*synchronized*/ void withdraw(int money){
        this.total -= money;
        System.out.println(MessageFormat.format("线程={0},取钱={1},账号剩余={2}",Thread.currentThread().getName(),money,this.total));
        query();
    }

    /**
     * 查询
     */
    public void query(){
        System.out.println(MessageFormat.format("线程={0},账号剩余={1}",Thread.currentThread().getName(),this.total));
    }

    /**
     * 业务类型
     */
    enum Buz{
        SAVE,WITHDRAW
    }
}

测试类如下:

public static void main(String[] args) {
        Bank bank = new Bank();
        new Customer("小明",200,bank, Bank.Buz.SAVE).start();
        new Customer("小花",300,bank, Bank.Buz.SAVE).start();
    }

输出结果:

小花准备SAVE操作......300
小明准备SAVE操作......200
线程=小花,存钱=300,账号剩余=500
线程=小明,存钱=200,账号剩余=500
线程=小花,账号剩余=500
线程=小明,账号剩余=500

结果变成如上,为什么呢?volitile只能保障可见性,但不能保持原子性,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不使用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是从线程持有的工作内存当中读取,因此每个线程访问到的变量值都是一样的,这样就保证了同步。

5.使用局部变量实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。变量局部化。

Bank类修改如下:

package java_current.sync;

import java.text.MessageFormat;

/**
 * @description: 银行
 * @Date : 2019/12/24 下午4:05
 * @Author : 石冬冬-Seig Heil
 */
public class Bank {

    /**
     * 总额
     */
    private final ThreadLocal<Integer> LOCAL = ThreadLocal.withInitial(() -> 0);

    /**
     * 存钱
     * @param money
     */
    public /*synchronized*/ void save(int money){
        LOCAL.set(LOCAL.get() + money);
        System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,LOCAL.get()));
        query();
    }

    /**
     * 取钱
     * @param money
     */
    public /*synchronized*/ void withdraw(int money){
        LOCAL.set(LOCAL.get() + money);
        System.out.println(MessageFormat.format("线程={0},取钱={1},账号剩余={2}",Thread.currentThread().getName(),money,LOCAL.get()));
        query();
    }

    /**
     * 查询
     */
    public void query(){
        System.out.println(MessageFormat.format("线程={0},账号剩余={1}",Thread.currentThread().getName(),LOCAL.get()));
    }

    /**
     * 业务类型
     */
    enum Buz{
        SAVE,WITHDRAW
    }
}

输出结果:

小花准备SAVE操作......300
小明准备SAVE操作......200
线程=小花,存钱=300,账号剩余=300
线程=小明,存钱=200,账号剩余=200
线程=小花,账号剩余=300
线程=小明,账号剩余=200

注:ThreadLocal与同步机制

  • ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。
  • 前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式

6.使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。

在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。小工具包,支持在单个变量上解除锁的线程安全编程。

类摘要
AtomicBoolean可以用原子方式更新的 boolean 值。
AtomicInteger可以用原子方式更新的 int 值。
AtomicIntegerArray可以用原子方式更新其元素的 int 数组。
AtomicIntegerFieldUpdater<T>基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong可以用原子方式更新的 long 值。
AtomicLongArray可以用原子方式更新其元素的 long 数组。
AtomicLongFieldUpdater<T>基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReference<V>维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference<V>可以用原子方式更新的对象引用。
AtomicReferenceArray<E>可以用原子方式更新其元素的对象引用数组。
AtomicReferenceFieldUpdater<T,V>基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
AtomicStampedReference维护带有整数"标志"的对象引用,可以用原子方式对其进行更新。

其中AtomicInteger(乐观锁)为例 :
Bank类修改如下:

package java_current.sync;

import java.text.MessageFormat;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 银行
 * @Date : 2019/12/24 下午4:05
 * @Author : 石冬冬-Seig Heil
 */
public class Bank {

    /**
     * 总额
     */
    private final AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 存钱
     * @param money
     */
    public /*synchronized*/ void save(int money){
        System.out.println(MessageFormat.format("线程={0},存钱={1},账号剩余={2}",Thread.currentThread().getName(),money,atomicInteger.addAndGet(money)));
        query();
    }

    /**
     * 取钱
     * @param money
     */
    public /*synchronized*/ void withdraw(int money){
        System.out.println(MessageFormat.format("线程={0},取钱={1},账号剩余={2}",Thread.currentThread().getName(),money,atomicInteger.addAndGet(-money)));
        query();
    }

    /**
     * 查询
     */
    public void query(){
        System.out.println(MessageFormat.format("线程={0},账号剩余={1}",Thread.currentThread().getName(),atomicInteger.get()));
    }

    /**
     * 业务类型
     */
    enum Buz{
        SAVE,WITHDRAW
    }
}

输出结果:

小明准备SAVE操作......200
小花准备SAVE操作......300
线程=小明,存钱=200,账号剩余=200
线程=小花,存钱=300,账号剩余=500
线程=小明,账号剩余=500
线程=小花,账号剩余=500

7.使用阻塞队列实现线程同步

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,同时,阻塞队列里面的put、take方法是被加:synchronized 同步限制。

BlockingQueueProduct类

/**
 * @description: 生产者
 * @Date : 2019/12/24 下午4:09
 * @Author : 石冬冬-Seig Heil
 */
public class BlockingQueueProduct implements Runnable{

    @Override
    public void run() {
        Bank bank = new Bank();
        try {
            while (true){
                Random random = new Random();
                int c = random.nextInt(100);
                BlockingQueueConsumer.QUEUE.put(new Customer("小" + c,c,bank, Bank.Buz.SAVE));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

BlockingQueueConsumer类

/**
 * @description: 消费者
 * @Date : 2019/12/24 下午4:09
 * @Author : 石冬冬-Seig Heil
 */
public class BlockingQueueConsumer implements Runnable{

    public final static BlockingQueue<Customer> QUEUE = new ArrayBlockingQueue(2);

    @Override
    public void run() {
        try {
            while (true){
                QUEUE.take().start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

BlockingQueueTest类

/**
 * @description: 测试
 * @Date : 2019/12/24 下午6:14
 * @Author : 石冬冬-Seig Heil
 */
public class BlockingQueueTest {

    public static void main(String[] args) {
        new Thread(new BlockingQueueConsumer()).start();
        new Thread(new BlockingQueueProduct()).start();
    }
}
几种常见阻塞队列
1、BlockingQueue (常用)

获取元素的时候等待队列里有元素,否则阻塞
保存元素的时候等待队列里有空间,否则阻塞
用来简化生产者消费者在多线程环境下的开发

2、ArrayBlockingQueue (数组阻塞队列)

FIFO、数组实现有界阻塞队列,一旦指定了队列的长度,则队列的大小不能被改变在生产者消费者例子中,如果生产者生产实体放入队列超过了队列的长度,则在offer(或者put,add)的时候会被阻塞,直到队列的实体数量< 队列的初始size为止。不过可以设置超时时间,超时后队列还未空出位置,则offer失败。

如果消费者发现队列里没有可被消费的实体时也会被阻塞,直到有实体被生产出来放入队列位置,不过可以设置等待的超时时间,超过时间后会返回null

3、DelayQueue (延迟队列)

有界阻塞延时队列,当队列里的元素延时期未到是,通过take方法不能获取,会被阻塞,直到有元素延时到期为止

如:
1.obj 5s 延时到期
2.obj 6s 延时到期
3.obj 9s 延时到期

那么在take的时候,需要等待5秒钟才能获取第一个obj,再过1s后可以获取第二个obj,再过3s后可以获得第三个obj

这个队列可以用来处理session过期失效的场景,比如session在创建的时候设置延时到期时间为30分钟,放入延时队列里,然后通过一个线程来获 取这个队列元素,只要能被获取到的,表示已经是过期的session,被获取的session可以肯定超过30分钟了,这时对session进行失效。

4、LinkedBlockingQueue (链表阻塞队列)

FIFO、Node链表结构可以通过构造方法设置capacity来使得阻塞队列是有界的,也可以不设置,则为无界队列其他功能类似ArrayBlockingQueue

5、PriorityBlockingQueue (优先级阻塞队列)

无界限队列,相当于PriorityQueue + BlockingQueue插入的对象必须是可比较的,或者通过构造方法实现插入对象的比较器Comparator<? super E> 队列里的元素按Comparator<? super E> comparator比较结果排序,PriorityBlockingQueue可以用来处理一些有优先级的事物。比如短信发送优先级队列,队列里已经有某企业的100000条短信,这时候又来了一个100条紧急短信,优先级别比较高,可以通过PriorityBlockingQueue来轻松实现这样的功能。这样这个100条可以被优先发送

前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。
使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。
本小节主要是使用LinkedBlockingQueue来实现线程的同步LinkedBlockingQueue是一个基于已连接节点的,范围任意的blocking queue。
队列是先进先出的顺序(FIFO),关于队列以后会详细讲解~

LinkedBlockingQueue 类常用方法
LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue

  • put(E e) : 在队尾添加一个元素,如果队列满则阻塞
  • size() : 返回队列中的元素个数
  • take() : 移除并返回队头元素,如果队列空则阻塞

注:BlockingQueue定义了阻塞队列的常用方法,尤其是三种添加元素的方法,我们要多加注意,当队列满时:

  • add()方法会抛出异常
  • offer()方法返回false
  • put()方法会阻塞

参考文章:https://www.cnblogs.com/cxxjohnson/p/8536257.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值