抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

线程安全

一个方法或者一个实例可以在多线程环境中使用而不会出现问题

线程安全实现方式

悲观锁

synchronized

使用synchronized是通过互斥的方式保证同步的,它对于同一条线程来说是可重入的;其次它是阻塞的。synchronized会将线程阻塞,当获得锁时会唤醒线程,将线程从用户状态转为内核状态,该操作会消耗大量的资源,顾synchronized是一个重量级锁。

ReentrantLock

ReentrantLock是一个可重入锁。它与synchronized的区别是,它需要显示的去调动lockunlock方法,手动去加锁并释放锁,一般释放锁的方法写在finally语句块中。

乐观锁

CAS(Compare And Swap)

CAS是原子操作,保证并发安全,不能保证并发同步

CAS是CPU的一个指令

CAS是非阻塞的、轻量级的乐观锁

原理:CAS比较并替换,就是将内存值更新为需要的值,但是有个条件,内存值必须与期望值相同。

最佳应用:java.util.concurrent.atomic包下的原子操作类

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新字段

CAS优点:

  • 乐观锁,通过CPU指令实现,性能高

CAS缺点:

  • 自旋时间长,消耗CPU资源
  • 非公平锁

synchronized使用方法

synchronized修饰的对象主要为四种:

  • 修饰代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  • 修饰方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 修饰一个静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

修饰代码块

被修饰的代码块被称为同步语句块,起作用范围是{}中的代码,作用的对象是这个代码块的对象。

public class UserService {

    public void addUser(User user){
        synchronized(this){
           System.out.println("add user");
        }
    }
}

多个线程访问同一个对象的代码块时,线程被阻塞;

多个线程访问不同对象的代码块,线程不会被阻塞。

即synchronized修饰的是一个对象,每个访问这个对象的线程都将被阻塞,直到该对象的锁被释放。

如果没有明确的对象加锁,可以在类中定义一个常量来实现:

public class UserService {

    private String lock = "lock";

    public void addUser(User user){
        synchronized(lock){
           System.out.println("add user");
        }
    }
}

修饰方法

public class UserService {
    public synchronized void updateUser(User user){
         System.out.println("update user");
    }
}

synchronized修饰方法时,它锁定的是调用这个同步方法的对象。即一个对象在不同的线程中调用该方法将被阻塞。

synchronized修饰一个静态的方法

public class UserService {
    public synchronized static void updateUser(User user){
         System.out.println("update user");
    }
}

synchronized作用的对象是一个静态方法,则它去得是类的所,该类素有的对象同一把锁,所有对象都会被阻塞。

synchronized作用于一个类

public class UserService {
    public void updateUser(User user){
        synchronized(UserService.class){
             System.out.println("update user");
        }
    }
}

synchronized作用于一个类时,是给这个类加锁,这个类的所有对象用的是同一把锁。

小结

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

线程安全的集合

最原始的集合

原始的线程安全集合都是通过在方法上添加synchronized关键字,该方法效率较低。

Vector:长度可变的数组

HashTable:线程安全的HashMap

concurrent包中的集合

ConcurrentHashMap:线程安全的HashMap,jdk1.8之前采用Segment分段锁,jdk1.8取消了分段锁,直接在table元素上加锁,实现对每一行进行加锁。

CopyOnWriteArrayList:具有写锁

CopyOnWritArraySet:具有写锁

ConcurrentSkipListMap:利用跳表实现的有序的、支持高并发的Map

ConcurrentSkipListSet:利用跳表实现的有序的、支持高并发的Set

ConcurrentLinkedQueue:高并发场景下的队列

ConcurrentLinkedDeque:高并发场景下的双端队列

线程安全Queue

在Java多线程应用中,Queue主要分为两种:

  • BlockingQueue:阻塞队列
  • ConcurrentLinkedQueue:高性能队列

BlockingQueue

BlockingQueue是一个接口,定义了一套阻塞队列的规则。它的常用方法为:

  • add(e) remove() element() 方法不会阻塞线程。当不满足约束条件时,会抛出IllegalStateException 异常。例如:当队列被元素填满后,再调用add(e),则会抛出异常。
  • offer(e) poll() peek() 方法即不会阻塞线程,也不会抛出异常。例如:当队列被元素填满后,再调用offer(e),则不会插入元素,函数返回false。
  • 要想要实现阻塞功能,需要调用put(e) take() 方法。当不满足约束条件时,会阻塞线程。
ArrayBlockingQueue

ArrayBlockingQueue是基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多场合非常适合使用。

LinkedBlockingQueue

LinkedBlockingQueue是基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列〈该队列由一个链表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行,他是一个无界队列。

SynchronousQueue

SynchronousQueue是一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并消费。

PriorityBlockingQueue

PriorityBlockingQueue是基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,他也是一个无界的队列。

DelayQueue

DelayQueue:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等等。

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueueo它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。

其核心方法为:

  • addoffer:将元素加入队列,两个方法没有区别
  • poll:取头元素节点,该方法会删除元素
  • Peek:取头元素节点

java.utils.cuncurrent下的锁

ReentrantLock

  • 一个可重入的互斥锁 Lock
  • ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有
  • 此类的构造方法接受一个可选的公平参数。当设置为true时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。采用默认设置(使用不公平锁)。
  • 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),优点是在获得锁和保证锁分配的均衡性时差异较小。

ReadWriteLock

ReadWriteLock维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

  • 与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。
  • 一次只有一个线程(writer 线程)可以修改共享数据;在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
  • 与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高

ReentrantReadWriteLock

ReentrantReadWriteLockReadWriteLock的实现类。它具有以下属性:

  • 获取顺序:ReentrantReadWriteLock不会将读取者优先或写入者优先强加给锁访问的排序
  • 公平性:默认为非公平锁,可以在构造函数中设置为公平锁
  • 重复性:可重入。
  • 锁降级:重入还允许从写入锁降为读取锁。实现方式是先获取写入所,然后获取读取锁,最后释放写入锁。即写入线程可以获取读取锁,读取锁中不能获取写入锁。

Stampedlock

StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了三种模式的读写控制,三种模式分别如下

写锁writeLock:排它锁(独占锁),同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当目前没有线程持有读锁或者写锁的时候才可以获取到该锁,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockWrite并传递参数stamp。

悲观读锁readLock:共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁,如果已经有线程持有写锁,其他线程请求获取该读锁会被阻塞。

乐观读锁tryOptimisticRead:乐观锁,运用CAS原理。适用于读多写少的场景,因为获取读锁只是使用与或操作进行检验,不涉及CAS操作,所以效率会高很多。

评论