文章目录
为何要使用同步
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