多线程学习(一):http://t.csdnimg.cn/o3ygn
目录
一、线程安全
二、线程同步
三、加锁的实现方式一:同步代码块
四、加锁的实现方式二:同步方法
五、同步方法和同步代码块的比较
六、加锁的实现方式三:Lock锁
一、线程安全
(1)什么是线程安全
多个线程,在操作同一共享资源时,可能会出现的业务问题
例如:取钱的线程安全问题
A和B两人同时去银行取钱一万元,对同一个账户进行操作,A使用银行卡,B使用存折,卡内余额一万元。
取钱流程:
1.判断余额是否足够
2.余额足够取钱
3.更新余额
多线程执行时,当A执行到第一步,判断余额足够取钱,还未执行到第三步。这时候B也进行取钱,这时候判断余额也是足够的,因此两人都可以进行取钱操作。在取钱之后更新时余额就成了负一万元。
(2)用代码模拟线程安全问题
(1)提供一个账户类,创建一个对象代表两人的共享账户
(2)定义一个线程类(创建两个线程,分别代表两个人A和B)
(3)创建两个线程,传入同一个账户对象给两个线程处理
package com.txd.demo4;
//账户类
public class Account {
private double money;
//A和B取钱时传过来的取钱金额
public void money(double money){
//搞清楚谁取钱(获取线程名)
String name = Thread.currentThread().getName();
//判断余额是否足够
if(this.money >= money){
System.out.println(name+"来取钱"+money+"成功");
this.money = this.money - money;
System.out.println(name+"来取钱,余额剩余"+this.money);
}else {
System.out.println(name+"来取钱,余额不足");
}
}
public Account() {
}
public Account(double money) {
this.money = money;
}
/**
* 获取
* @return money
*/
public double getMoney() {
return money;
}
/**
* 设置
* @param money
*/
public void setMoney(double money) {
this.money = money;
}
public String toString() {
return "Account{money = " + money + "}";
}
}
package com.txd.demo4;
//代表取钱的线程类
public class MoneyThread extends Thread{
//接受账户信息 和 取款人
private Account account;
public MoneyThread(Account account, String name){
super(name);
this.account = account;
}
@Override
public void run() {
//取钱操作(A和B)
account.money(10000);
}
}
执行结果
二、线程同步
线程同步就是解决线程安全问题的方案
线程同步的思想:
让多个线程实现先后依次访问共享资源,这样就解决了安全问题
线程同步的常见方案:
加锁:每次只允许一个线程加锁,加锁后才能访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
三、加锁的实现方式一:同步代码块
作用:
把共享资源的核心代码给上锁,以此保证线程的安全
synchronized(同步锁){
访问共享资源的核心代码
}
原理:
每次只允许一个线程加锁后进入,执行完毕自动解锁,其他线程才可以进来执行
同步锁的注意事项:
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
取钱案例改造:
选择核心代码,在该案例中核心代码就是取钱的代码,也就是
//判断余额是否足够
if(this.money >= money){
System.out.println(name+"来取钱"+money+"成功");
this.money = this.money - money;
System.out.println(name+"来取钱,余额剩余"+this.money);
}else {
System.out.println(name+"来取钱,余额不足");
}
我们可以对这串代码进行加锁,选中这串代码,使用快捷键ctrl+alt+t选择synchronized
在括号中也就是同步锁必须是同一个对象,例如字符串"测试"
加锁后执行代码查看控制台输出:
测试成功。因为A线程优先启动,所以会有限竞争到锁先执行方法
执行步骤:
当A和B线程同时启动,都执行到这个取钱方法的时候,就开始竞争同步锁,当A线程拿到锁后,会对这个对象进行标记,即表示被加锁。B线程就无法进入方法。当A线程执行方法结束后,会自动解锁,此时B线程拿到锁执行方法。
因为使用了固定字符串为同步锁,当其他账户来取钱时也因为加锁导致无法取钱 。因此应该使用他们的账户信息作为锁,不同账户互不干扰,也就是类中的this。
如果是静态资源,可以使用类名.class作为锁 Account.class
注意事项:
如果锁对象随便选一个唯一的对象,会影响其他无关线程的执行
使用规范:
应该使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
对于静态资源建议使用字节码(类名.class)对象作为锁对象
四、加锁的实现方式二:同步方法
作用:
把访问共享资源的核心方法上锁,保证线程安全
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
原理:
每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行
同步方法底层也是有隐式锁对象的,只是锁的范围是整个方法代码
取钱案例改造:
找到核心方法:
//A和B取钱时传过来的取钱金额
public void money(double money){
//搞清楚谁取钱(获取线程名)
String name = Thread.currentThread().getName();
//判断余额是否足够
if(this.money >= money){
System.out.println(name+"来取钱"+money+"成功");
this.money = this.money - money;
System.out.println(name+"来取钱,余额剩余"+this.money);
}else {
System.out.println(name+"来取钱,余额不足");
}
}
只需要在方法上加上synchronized
执行main方法:
执行步骤:
当多个线程到这个实例方法的时候,底层也是使用this作为锁,只是看不到
如果是静态方法,默认的锁是类名.class作为锁
五、同步方法和同步代码块的比较
范围上:同步代码块锁的范围更小,同步方法锁的范围更大(锁的范围越小性能越好)
可读性:同步方法更好
六、加锁的实现方式三:Lock锁
Lock是接口,不能直接实例化,可以采用他的实现类ReentrantLock来构建Lock锁对象
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活,更方便,更强大
使用:
创建一个lock锁对象
注意事项:
//创建一个锁对象 private Lock lock = new ReentrantLock();
时建议使用final进行修饰
对需要上锁的代码进行try catch finally 防止加锁后出现异常没有解锁
public void money(double money){
//搞清楚谁取钱(获取线程名)
String name = Thread.currentThread().getName();
lock.lock(); //加锁
//判断余额是否足够
try {
if(this.money >= money){
System.out.println(name+"来取钱"+money+"成功");
this.money = this.money - money;
System.out.println(name+"来取钱,余额剩余"+this.money);
}else {
System.out.println(name+"来取钱,余额不足");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}