线程安全问题
首先考虑一个场景,当两个人同一时间去同一账户取出账户等额的金钱,如果不去考虑资源共享时候的冲突情况,极易可能出现在两个人取钱之后,出现负的等额的情况,如果这种事情发生在当前的诸多场景中很容易出现,软件将完全丧失可用性。
1、场景引入
//Account实体类,并且提供了取钱方法
public class Account {
private Integer money;
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public Account(Integer money) {
this.money = money;
}
public void getMoneyFromAccount(Integer money) {
if (money <= this.money) {
this.money -= money;
System.out.println(Thread.currentThread().getName() + "已从账户中取出:" + money + "当前账户剩余:" + this.money);
} else {
System.out.println("当前账户里面没有那么多钱");
}
}
}
//取钱线程
public class AccountThread extends Thread {
private Integer money;
private Account account;
public AccountThread(String name, int money, Account account) {
//直接继承的无参构造是默认对名称进行修改的
super(name);
this.money = money;
this.account = account;
}
@Override
public void run() {
account.getMoneyFromAccount(money);
}
}
注:super直接继承的是setName()相当于给当前线程设置对应的名称
public static void main(String[] args) {
//初始账户一万元
Account account = new Account(10000);
//相当于两个人从同一个账户中去进行取钱
Thread ming = new AccountThread("ming", 10000, account);
Thread sang = new AccountThread("sang", 10000, account);
ming.start();
sang.start();
}
这个场景中明和桑共同操作account这个账户,在线程中操作取钱方法,对账户进行操作,在这个场景中资源就是Account,抢夺的就是Account账户中的资源
2、那么如何保障线程安全呢?
毫无疑问就是线程同步,在一个线程对该资源进行操作的时候,其余线程首先去知晓这个状态,且不可对该线程进行可操作
同步的三种方式
(1)同步代码块-synchronized对同步该资源的代码块进行上锁
public void getMoneyFromAccount(Integer money) {
// lock.lock();
try {
synchronized (this) {
if (money > this.money) {
System.out.println("当前账户里面没有那么多钱");
return;
}
System.out.println(Thread.currentThread().getName() + "已从账户中取出:" + money + "当前账户剩余:" + (this.money -= money));
}
}finally {
// lock.unlock();
}
}
在操作该对象的money时,在这块进行加锁
(2)在操作该资源的方法上进行加锁
public synchronized void getMoneyFromAccount(Integer money) {
// public void getMoneyFromAccount(Integer money) {
// lock.lock();
try {
// synchronized (this) {
if (money > this.money) {
System.out.println("当前账户里面没有那么多钱");
return;
}
System.out.println(Thread.currentThread().getName() + "已从账户中取出:" + money + "当前账户剩余:" + (this.money -= money));
// }
}finally {
// lock.unlock();
}
}
与方法一相比,方法二是在整个执行方法上进行加锁,但锁越精细越好,越能最大的发挥CPU的性能,因此推荐方法一
另外,锁的对象只要保证唯一性即可,但在动态方法中,锁的对象常常是this,即当前对象,在静态方法中,锁的对象常常是当前的字节类
(3)第三种方式为显式锁,通过lock创建对应的锁对象,通过锁对象去进行加锁解锁
Lock lock = new ReentrantLock();
public void getMoneyFromAccount(Integer money) {
lock.lock();
try {
// synchronized (this) {
if (money > this.money) {
System.out.println("当前账户里面没有那么多钱");
return;
}
System.out.println(Thread.currentThread().getName() + "已从账户中取出:" + money + "当前账户剩余:" + (this.money -= money));
// }
}finally {
lock.unlock();
}
}
显示锁与隐式锁有什么区别和优劣差异,后期再去看吧,算了,就现在吧,他娘的
显示锁又称乐观锁,当中间发生资源抢占或者其它,直接进行回退,有种原子操作的感觉
隐式锁又称悲观锁,假设其它线程会使用该资源,因此是独占操作
显示锁是jdk1.5版本开始出现,隐式锁是JVM本身维护且较耗资源,随着版本迭代,sync的使用在逐渐优化且是官方推荐~
线程池
使用容器化技术,对线程的创建进行约束,不可能随意的去进行线程的创建,相当于去使用的一种容器化的技术,实现了线程反复利用,省去了频繁创建和销毁线程对象的操作——这就又印出了一个问题,线程池种的核心线程不会销毁且一直存在
1、简述一下,为什么要使用线程池?
(1)降低资源的消耗
(2)提高相应速度
(3)提高线程的可管理性
线程的核心思想即是:线程一次创建,反复利用执行各种任务
2、创建线程池的方式
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new MyRun());
executorService.submit(new MyRun());
executorService.submit(new MyRun());
executorService.submit(new MyRun());
//停止线程池 — 在所有线程全部运行完之后
executorService.shutdown();
//立即对线程池进行关闭,不去关注线程中是否还存在线程,支取进行资源关闭
executorService.shutdownNow();
注意shutdown和shutdownNow这两种使用方式
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 2L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
目前创建的方式大多使用其子方法,对内部参数进行填充
注:线程池是有自己的运行机制的,这套机制很有意思,后期再补充