处理多线程问题时,多个线程访问一个对象并修改数据库时,可能破坏事务的四大特性(原子性、一致性、隔离性、持久性),因此我们要采取队列和锁(缺一不可),就好像上图厕所排队,请问你怎么才能安全和安心的上一个厕所?这时候首先得有序排队(队列)避免插队冲突,第二 人进厕所得上锁(加锁)避免在你未完成的情况下别人进去干扰你
线程同步(保证线程安全)
当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用完成后释放锁即可,但会引起以下问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致性能慢
同步方法
- 我们通过Private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提供机制,也就是synchronized 关键字,它包括两种用法:synchronized方法和synchronized块
- synchronized方法控制对 对象 的访问,每一个对象对应一把锁,每一个synchronized方法都必须获得调用该方法对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
不安全案例代码
public class TestLock {
public static void main(String[] args) {
User user = new User(1000,"小明");
Bank bank = new Bank(user,300);
//四个人去银行取钱
new Thread(bank,"小明").start();
new Thread(bank,"小明老婆").start();
new Thread(bank,"小明妈妈").start();
new Thread(bank,"小明爸爸").start();
}
}
//个人账户
class User{
//账户的钱和名字
int totalMoney;
String name;
public User(int totalMoney, String name){
this.totalMoney = totalMoney;
this.name = name;
}
}
//银行取款
class Bank implements Runnable{
User user;
//要取的钱和个人拥有的现金
int getMoney;
int money;
public Bank(User user,int getMoney){
this.user = user;
this.getMoney = getMoney;
}
@Override
public void run() {
if(getMoney> user.totalMoney){
System.out.println("卡里余额不足");
return;
}
//设置延时是为了满足四个人在知道卡里有钱的情况下同时取钱
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = getMoney;
user.totalMoney = user.totalMoney-getMoney;
System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
}
}
运行结果:
小明老婆取了300
小明的卡余额为:400
小明取了300
小明的卡余额为:400
小明妈妈取了300
小明的卡余额为:400
小明爸爸取了300
小明的卡余额为:400
!结果很明显是银行亏大了
1.synchronized方法保证线程安全
- 同步方法无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,也可以或者是class(反射)
@Override
public synchronized void run() {
if(getMoney> user.totalMoney){
System.out.println("卡里余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = getMoney;
user.totalMoney = user.totalMoney-getMoney;
System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
}
2.synchronized块保证线程安全
- 同步块 synchronized(Obj){…}
- Obj可以称为同步监视器
1.Obj可以是任何对象,推荐使用共享资源作为同步监视器- 同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码
2.第二线程访问,发现同步监视器被锁定,无法访问
3.第一个线程访问结束,解锁同步监视器
4.第二个线程访问,发现同步监视器没有锁,然后锁定访问
@Override
public void run() {
synchronized (user){
if(getMoney> user.totalMoney){
System.out.println("卡里余额不足");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money = getMoney;
user.totalMoney = user.totalMoney-getMoney;
System.out.println(Thread.currentThread().getName()+"取了"+getMoney+"\n"+user.name+"的卡余额为:"+user.totalMoney);
}
}
如果Bank继承的是Thread类,采用synchronized方法(在修饰run方法加synchronized)是不能保证线程安全的,因为创建多线程时,synchronized方法锁的是Bank,而操作对象增删改的是User,synchronized方法锁的是当前实例(this)。而对于上述实现Runnable接口的Bank类,采取synchronized方法锁的虽然是Bank,但不会出现问题,因为代理对象操作的同一资源(进同一银行得排队),没有代理对象的话是多个银行取同一资源(账户的钱),锁Bank是解决不了问题的,因为其他银行操作不需要另一个银行的锁,只需要User的锁
死锁问题的导致
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
案例代码
public class TestDeadLock {
public static void main(String[] args) {
PlayGame playGame = new PlayGame();
new Thread(playGame,"a").start();
new Thread(playGame,"b").start();
}
}
class Lol{}
class Dnf{}
class PlayGame implements Runnable{
//可以理解为资源只有一份(只有一个游戏账户)
static Lol lol = new Lol();
static Dnf dnf = new Dnf();
@Override
public void run() {
try {
playGame();
}catch (InterruptedException e){
e.printStackTrace();
}
}
private void playGame() throws InterruptedException {
if(Thread.currentThread().getName()=="a"){
synchronized (lol){
System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
Thread.sleep(1000);
synchronized (dnf){
System.out.println(Thread.currentThread().getName()+"同时获得DNF的锁");
}
}
}else {
synchronized (dnf){
System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
Thread.sleep(2000);
synchronized (lol){
System.out.println(Thread.currentThread().getName()+"同时获得LOL的锁");
}
}
}
}
}
a线程访问锁定 lol 同步监视器,b线程访问锁定 dnf 同步监视器,接下来a没执行完synchronized代码块(相当于lol没下线还玩着)还想同时玩 dnf,但dnf 同步监视器已经被 b 线程锁定了(导致无法登录),于是等待b释放锁。但b也没执行完synchronized代码块(相当于dnf没下线还玩着)还想同时玩 lol,但lol同步监视器已经被 a 线程锁定了(导致无法登录),于是双方等待对方释放锁资源(但对方都很倔强,如果a没等到b释放dnf的锁死都不会释放lol的,b也和a有着一样的想法),最终宇宙毁灭那时候双方都只玩过其中一个游戏,这就是死锁问题的发生。
死锁问题的解决
private void playGame() throws InterruptedException {
if(Thread.currentThread().getName()=="a"){
synchronized (lol){
System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
Thread.sleep(1000);
}
synchronized (dnf){
System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
}
}else {
synchronized (dnf){
System.out.println(Thread.currentThread().getName()+"获得DNF的锁");
Thread.sleep(2000);
}
synchronized (lol){
System.out.println(Thread.currentThread().getName()+"获得LOL的锁");
}
}
}
不要锁中加锁,a释放 lol 同步资源器的条件是你已经玩完了,而不是要等 dnf 的同步资源器可以访问才释放lol的锁,b的思想也一样。
ReentrantLock(可重入锁)
ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReentrantLock,可以显式加锁、释放锁。
不安全案例代码
public class TestReentrantLock {
public static void main(String[] args) {
SaleTicket saleTicket = new SaleTicket();
new Thread(saleTicket,"小明").start();
new Thread(saleTicket,"小胖").start();
}
}
class SaleTicket implements Runnable{
private int ticket = 10;
@Override
public void run() {
try {
while (true) {
if(ticket>0){
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"买了第"+ticket--+"票");
}else{
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ReentrantLock(可重入锁)解决方法
class SaleTicket implements Runnable {
private int ticket = 10;
private final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
try {
while (true) {
reentrantLock.lock();//加锁
if (ticket > 0) {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "买了第" + ticket-- + "票");
} else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();//解锁
}
}
}
这时候就是小明买票没完成,;另外一个是不能买票的,在实际的业务代码里面,小明不可能一直买票直到票没有,买了一张就释放锁了,有兴趣的朋友可以自己写代码测试