Java 学习之路 之 线程同步(七十六)

2024-03-05 16:48

本文主要是介绍Java 学习之路 之 线程同步(七十六),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

多线程编程是有趣的事情,它很容易突然出现 “错误情况”,这是由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易 “偶然” 出现线程安全问题。

1,线程安全问题

关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几个步骤。

(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。

(2)用户输入取款金额。

(3)系统判断账户余额是否大于取款金额。

(4)如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款失败。

乍一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定.也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!

按上面的流程去编写取款程序,而且我们便用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。我们不管检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个属性。

public class Account
{// 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;public Account(){}// 构造器public Account(String accountNo , double balance){this.accountNo = accountNo;this.balance = balance;}// 此处省略了accountNo和balance两个Field的setter和getter方法// accountNo的setter和getter方法public void setAccountNo(String accountNo){this.accountNo = accountNo;}public String getAccountNo(){return this.accountNo;}// balance的setter和getter方法public void setBalance(double balance){this.balance = balance;}public double getBalance(){return this.balance;}// 下面两个方法根据accountNo来重写hashCode()和equals()方法public int hashCode(){return accountNo.hashCode();}public boolean equals(Object obj){if(this == obj)return true;if (obj !=null&& obj.getClass() == Account.class){Account target = (Account)obj;return target.getAccountNo().equals(accountNo);}return false;}
}

接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。

public class DrawThread extends Thread
{// 模拟用户账户private Account account;// 当前取钱线程所希望取的钱数private double drawAmount;public DrawThread(String name , Account account , double drawAmount){super(name);this.account = account;this.drawAmount = drawAmount;}// 当多条线程修改同一个共享数据时,将涉及数据安全问题。public void run(){// 账户余额大于取钱数目if (account.getBalance() >= drawAmount){// 吐出钞票System.out.println(getName()+ "取钱成功!吐出钞票:" + drawAmount);/*try{Thread.sleep(1);}catch (InterruptedException ex){ex.printStackTrace();}*/// 修改余额account.setBalance(account.getBalance() - drawAmount);System.out.println("\t余额为: " + account.getBalance());			}else{System.out.println(getName() + "取钱失败!余额不足!");}}
}

读者先不要管程序中那段被注释掉的那段代码,上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱。程序如下:

public class DrawTest
{public static void main(String[] args) {// 创建一个账户Account acct = new Account("1234567" , 1000);// 模拟两个线程对同一个账户取钱new DrawThread("甲" , acct , 800).start();new DrawThread("乙" , acct , 800).start();}
}

多次运行上面程序,很有可能都会看到如图 16.9 所示的错误结果。

如图 16.9 所示的运行结果并不是我们所期望的结果(不过也有可能看到运行正确的效果),这正是多线程编程突然出现的 “偶然” 错误——因为线程调度的不确定性。假设系统线程调度器在刚才注释的那段代码处暂停,让另一个线程执行——为了强制暂停,只要取消上面程序中那段代码的注释即可。取消注释后再次编译 DrawThread.java,并再次运行 DrawTest 类,将总可以看到如图 16.9 所示的错误结果。

问题出现了:账户余额只有 1000 时取出了1600,而且账户余额出现了负值,这不是银行希望的结果。虽然上面程序是人为地使用 Thread.sleep(1) 来强制线程调度切换,但这种切换也是完全可能发生的—— 100000 次操作其要有 1 次出现了错误,那就是编程错误引起的。

2,同步代码块

之所以出现如图 16.9 所示的结果,是因为 run() 方法的方法体不具有同步安全性——程序中有两个并发线程在修改 Account 对象;而且系统恰好执行线程切换,切换给另一个修改 Account 对象的线程,所以就出现了问题。

就像前面介绍的文件并发访问,当有两个进程并发修改同一个文件时就有可能造成异常。

为了解决这个问题,Java 的多线程支持引入丁同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj)
{...//此处的代码就是同步代码块
}

上面语法格式中 synchronized 后括号里的 obj 就是同步监视器,上面代码的含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

虽然 Java 程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户(account)作为同步监视器。我们把程序修改成如下形式。

public class DrawThread extends Thread
{// 模拟用户账户private Account account;// 当前取钱线程所希望取的钱数private double drawAmount;public DrawThread(String name , Account account , double drawAmount){super(name);this.account = account;this.drawAmount = drawAmount;}// 当多条线程修改同一个共享数据时,将涉及数据安全问题。public void run(){// 使用account作为同步监视器,任何线程进入下面同步代码块之前,// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑synchronized (account){// 账户余额大于取钱数目if (account.getBalance() >= drawAmount){// 吐出钞票System.out.println(getName()+ "取钱成功!吐出钞票:" + drawAmount);try{Thread.sleep(1);}catch (InterruptedException ex){ex.printStackTrace();}// 修改余额account.setBalance(account.getBalance() - drawAmount);System.out.println("\t余额为: " + account.getBalance());			}else{System.out.println(getName() + "取钱失败!余额不足!");}}//同步代码块结束,该线程释放同步锁}
}

上面程序使用 synchronized 将 run() 方法里的方法体修改成同步代码块,该同步代码块的同步监视器是 account 对象,这样的做法符合 “加锁一修改一释放锁” 的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区);所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

将 DrawThread 修改为上面所示的情形之后,多次运行诙程序,总可以看到如图 16.10 所示的正确结果。


3,同步方法

与同步代码块对应,Java 的多线程安全支持还提供了同步方法,就是使用 synchronized 关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,同步方法的同步监视器是 this,也就是该对象本身。

通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征。

该类的对象可以被多个线程安全地访问。

每个线程调用该对象的任意方法之后都将得到正确结果。

每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。

前面我们介绍了可变类和不可变类,其中不可变类总是线程安全的,因为它的对象状态不可改变;但可变对象需要额外的方法来保证其线程安全。例如上面的 Account 就是一个可变类,它的 account 和 balance 两个 Field 都可变,当两个线程同时修改 Account 对象的 balance Field 时,程序就出现了异常。下面我们将 Account 类对 balance 的访问设置成线程安全的,那么只要把 balance 的方法修改成同步方法即可。程序如下所示。

public class Account
{// 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;public Account(){}// 构造器public Account(String accountNo , double balance){this.accountNo = accountNo;this.balance = balance;}// accountNo的setter和getter方法public void setAccountNo(String accountNo){this.accountNo = accountNo;}public String getAccountNo(){return this.accountNo;}// 因此账户余额不允许随便修改,所以只为balance提供getter方法,public double getBalance(){return this.balance;}// 提供一个线程安全draw()方法来完成取钱操作public synchronized void draw(double drawAmount){// 账户余额大于取钱数目if (balance >= drawAmount){// 吐出钞票System.out.println(Thread.currentThread().getName()+ "取钱成功!吐出钞票:" + drawAmount);try{Thread.sleep(1);}catch (InterruptedException ex){ex.printStackTrace();}// 修改余额balance -= drawAmount;System.out.println("\t余额为: " + balance);}else{System.out.println(Thread.currentThread().getName()+ "取钱失败!余额不足!");}}// 下面两个方法根据accountNo来重写hashCode()和equals()方法public int hashCode(){return accountNo.hashCode();}public boolean equals(Object obj){if(this == obj)return true;if (obj !=null&& obj.getClass() == Account.class){Account target = (Account)obj;return target.getAccountNo().equals(accountNo);}return false;}
}

上面程序中增加了一个代表取钱的 draw() 方法,并使用了 synchronized 关键字修饰该方法,把该方法变成同步方法。同步方法的同步监视器是 this.因此对于同一个 Account 账户而言,任意时刻只能有一个线程获得对 Account 对象的锁定,然后进入 draw() 方法执行取钱操作—— 这样也可以保证多个线程并发取钱的线程安全。

因为 Account 类中已经提供了 draw() 方法,而且取消了 setBalance() 方法,DrawThread 线程类需要改写,该线程类的 run() 方法只要调用 Account 对象的 draw() 方法即可执行取钱操作。run() 方法代码片段如下。

synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。

{// 直接调用account对象的draw方法来执行取钱// 同步方法的同步监视器是this,this代表调用draw()方法的对象。// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。account.draw(drawAmount);
}

上面的 DrawThread 类无须自己实现取钱搡作,而是直接调用 account 的 draw() 方法来执行取钱操作。由于已经使用 synchronized 关键字修饰了 draw() 方法,同步方法的同步监视器是 this,而 this 总代表调用该方法的对象—— 在上面示例中,调用 draw() 方法的对象是 account,因此多个线程并发修改同一份 account 之前,必须先对 account 对象加锁。这也符合了 “加锁一修改一释放锁” 的逻辑。

在 Account 里定义 draw() 方法,而不是直接在 run() 方法中实现取钱逻辑,这种做法更符合面向对象规则。在面向对象里有一种流行的设计方式:Domain Driven Design(领域驱动设计,DDD),这种方式认为每个类都应该是完备的领域对象,例如 Account 代表用户账户,应该提供用户账户的相关方法;通过 draw() 方法来执行取钱操作(实际上海应该提供 transfer() 等方法来完成转账等操作),而不是直接将 setBalance() 方法暴露出来任人操作,这样才可以更好地保证 Account 对象的完整性和一致性。

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来昀负面影响,程序可以采用如下策略。

不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步。例如上面 Account 类中的 accountNo 属性就无须同步,所以程序只对 draw() 方法进行了同步控制。

如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。

JDK 所提供的 StringBuilder、StringBuffer 就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使 StringBuilder 来保证较好的性能;当需要保证多线程安全时,就应该使用 StringBuffer。

4,释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监枧器的锁定。

当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。

当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器。

当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。

当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的 wait() 方法,则当前线程暂停,并释放同步监视器。

在如下所示的情况下,线程不会释放同步监视器。

线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法来暂停当前线程的执行,当前线程会释放同步监视器。

线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该尽量避免使用 suspend() 和 resume() 方法来控制线程。

5,同步锁(Lock)

从 Java 5 开始。Java 提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁使用 Lock 对象充当。

Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作,Lock 实现允许更灵活的结构,可以具有差别很大的属性,并且支持多个相关的 Condition 对象。

Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源附独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁),Lock、ReadWriteLock 是 Java 5 新提供的两个根接口,并为 Lock 提供了 ReentrantLock(可重入锁)实现类:为 ReadWriteLock 提供了 ReentrantReadWriteLock 实现类。

在实现线程安全的控制中,比较常用的是 ReentrantLock(可重入锁)。使用该 Lock 对象可以显地加锁、释放锁,通常使用 ReentrantLock 的代码格式如下:

class X
{// 定义锁对象private final ReetrantLock lock = new ReetrantLock();// ...// 定义需要保证线程安全的方法public void m(){// 加载lock.lock();try{// 需要保证线程安全的代码// ... method body}finally{lock.unlock();}}
}

使用 ReentrantLock 对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用 finally 块来确保在必要时释放锁。通过使用 ReentrantLock 对象,我们可以把 Account 类改为如下形式,它依然是线程安全的。

public class Account
{// 定义锁对象private final ReentrantLock lock = new ReentrantLock();// 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;public Account(){}// 构造器public Account(String accountNo , double balance){this.accountNo = accountNo;this.balance = balance;}// accountNo的setter和getter方法public void setAccountNo(String accountNo){this.accountNo = accountNo;}public String getAccountNo(){return this.accountNo;}// 因此账户余额不允许随便修改,所以只为balance提供getter方法,public double getBalance(){return this.balance;}// 提供一个线程安全draw()方法来完成取钱操作public void draw(double drawAmount){// 加锁lock.lock();try{// 账户余额大于取钱数目if (balance >= drawAmount){// 吐出钞票System.out.println(Thread.currentThread().getName()+ "取钱成功!吐出钞票:" + drawAmount);try{Thread.sleep(1);}catch (InterruptedException ex){ex.printStackTrace();}// 修改余额balance -= drawAmount;System.out.println("\t余额为: " + balance);}else{System.out.println(Thread.currentThread().getName()+ "取钱失败!余额不足!");}}finally{// 修改完成,释放锁lock.unlock();}		}// 下面两个方法根据accountNo来重写hashCode()和equals()方法public int hashCode(){return accountNo.hashCode();}public boolean equals(Object obj){if(this == obj)return true;if (obj !=null&& obj.getClass() == Account.class){Account target = (Account)obj;return target.getAccountNo().equals(accountNo);}return false;}
}

上面程序中的第 4 行代码定义了一个 ReentrantLock 对象,程序中实现 draw() 方法时,进入方法开始执行后立即请求对 ReentrantLock 对象进行加锁,当执行完 draw() 方法的取钱逻辑之后,程序使用 finally 块来确保释放锁。

使用 Lock 与使用同步方法有点相似,只是使用 Lock 时显式使用 Lock 对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合 “加锁-修改-释放锁” 的操作模式,而且使用 Lock 对象时每个Lock 对象对应一个 Account 对象,这样可以保证对于同一个 Account 对象,同一时刻只能有一个线程能进入临界区。

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock 提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的 tryLock() 方法,以及试图获取可中断锁的 locklnterruptibly() 方法,还有获取超时失效锁的 tryLock(long, TimeUnit) 方法。

ReentrantLock 锁具有可重入性,也就是说,一个线程可以对已被加锁的 ReentrantLock 锁再次加锁,ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

6,死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java 虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一日出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。

class A
{public synchronized void foo( B b ){System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了A实例的foo方法" );     //①try{Thread.sleep(200);}catch (InterruptedException ex){ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 企图调用B实例的last方法");    //③b.last();}public synchronized void last(){System.out.println("进入了A类的last方法内部");}
}
class B
{public synchronized void bar( A a ){System.out.println("当前线程名: " + Thread.currentThread().getName()+ " 进入了B实例的bar方法" );   //②try{Thread.sleep(200);}catch (InterruptedException ex){ex.printStackTrace();}System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法");  //④a.last();}public synchronized void last(){System.out.println("进入了B类的last方法内部");}
}
public class DeadLock implements Runnable
{A a = new A();B b = new B();public void init(){Thread.currentThread().setName("主线程");// 调用a对象的foo方法a.foo(b);System.out.println("进入了主线程之后");}public void run(){Thread.currentThread().setName("副线程");// 调用b对象的bar方法b.bar(a);System.out.println("进入了副线程之后");}public static void main(String[] args){DeadLock dl = new DeadLock();// 以dl为target启动新线程new Thread(dl).start();// 调用init()方法dl.init();}
}

运行上面程序,将会看到如图16.11所示的效果。

从图 16.11 中可以看出,程序既无法向下执行,也不会抛出任何异常,就一直 “僵持” 着。究其原因,是因为:上面程序中 A 对象和 B 对象的方法都是同步方法,也就是 A 对象和 B 对象都是同步锁。程序中两个线程执行,一个线程的线程执行体是 DeadLock 类的 run() 方法,另一个线程的线程执行体是 DeadLock 的 init() 方法(主线程调用了 init() 方法)。其中 run() 方法中让 B 对象调用 bar()方法,而 init() 方法让 A 对象调用 foo() 方法。图 16.11 显示 init() 方法先执行,调用了 A 对象的 foo() 方法,进入 foo() 方法之前,该线程对 A 对象加锁——当程序执行到①号代码时,主线程暂停 200 ms;CPU 切换到执行另一个线程,让 B 对象执行 bar() 方法,所以看到副线程开始执行 B 实例的 bar() 方法,进入 bar() 方法之前.该线程对 B 对象加锁—— 当程序执行到②号代码时,副线程也暂停200 ms;接下来主线程会先醒过来,继续向下执行,直到③号代码处希望调用 B 对象的 last() 方法——执行该方法之前必须先对 B 对象加锁,但此时副线程正保持着 B 对象的锁,所以主线程阻塞;接下来副线程应该也醒过来了,继续向下执行,直到④号代码处希望调用 A 对象的 last() 方法——执行该方法之前必须先对 A 对象加锁,但此时主线程没有释放对 A 对象的锁——至此,就出现了主线程保持着 A 对象的锁,等待对 B 对象加锁,而副线程保持着 B 对象的锁,等待对 A 对象加锁,两个线程互相等待对方先释放,所以就出现了死锁。

由于 Thread 类的 suspend() 方法也很容易导致死锁,所以 Java 不再推荐使用该方法来暂停线程的执行。

这篇关于Java 学习之路 之 线程同步(七十六)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/777157

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M