什么是并发?怎么解决?
并发:会导致线程不安全,数据混乱,比如负数。
* 当多个线程同时操作一个对象就会出现并发问题。
* 怎么解决:现实生活中,当多个人买一样东西或者结账的时候,我们都是排队,
* 可以用这个思维。但我们怎么知道上一个线程使用完没,这个可以用一个锁来表示;
* 就好像我们住宾馆的时候,我们住的时候会得到一张卡,凭卡进,退房的时候就把卡
* 还给宾馆,这个卡就相当于锁。总结就是队列与锁,也就是线程同步。
* 线程同步:是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形参队列,
* 等待前面的线程使用完毕后,下一个线程再使用。
怎么实现?
实现:加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程
* 必需等待,使用完后释放锁即可。不过存在问题:
* 1.一个线程持有锁会导致其他所有需要此锁的线程挂起;
* 2.在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
* 3.如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
* 于是我们会感觉到比较慢,不过鱼与熊掌不可兼得,有些是需要牺牲性能来做的。
* 具体实现:因为可以用private关键字来保证对象只被方法访问,所以锁是针对方法的。
* 两种用法:synchronized方法和synchronized块;如果是一个比较大的方法那么会大大影响效率,
* 因为这个方法里的所有属性都被锁住了,其他线程想访问只能等待,但用块太小,有可能锁不住,
* 怎么使用这个锁就是个难点:1.锁的目标一定要对;2.效率要高。
同步方法:之前的实例:模拟抢票来实现runnable接口时,发现有负数,这个就是并发,我们拿这个来举例说明,将之前的代码进行一下修改:
public void run() {
while(flag) {
test();
}
}
/*
synchronized方法 线程安全 同步
锁住的不是方法,是锁资源;因为这个方法是TestRunnable类的成员方法,
那么这个类的对象(要创建出来才有对象)就是下面main里的runnable,也就是this对象
最终锁住的是runnable这个对象;下面方法里的ticketNums、flag都是属于
runnable对象的,所以能锁住。
*/
public synchronized void test(){
if(ticketNums<0){
flag = false;
return;
}
try {
Thread.sleep(200);//模拟网络延迟,可能会有并发问题,数据错误
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.currentThread().getName();//拿到当前线程的名字
System.out.println(Thread.currentThread().getName()+"-->"+name+ticketNums--);
}
以上代码主要是对之前run方法进行修改,代码中直接在方法上加上同步修饰词,执行,可以发现没有负数了,这就解决 了并发问题。
同步块:我们用取钱来模拟:
首先有个账户
//账户
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
然后有个取款机
class ATM extends Thread{
Account account;
int drawMoney;//取多少钱
int drawTotal;//取钱的总数
public ATM(Account account, int drawMony,String name) {
super(name);
this.account = account;
this.drawMoney = drawMony;
}
@Override
public void run() {
test();
}
public void test(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -=drawMoney;
drawTotal+=drawMoney;
System.out.println("账户余额为:"+account.money);
System.out.println(this.getName()+"--一共取了:"+drawTotal);
}
}
调用:
Account account = new Account(1000,"存款");
ATM me = new ATM(account,600,"自己");
ATM wife = new ATM(account,700,"妻子");
me.start();
wife.start();
执行:
账户余额为:400
妻子--一共取了:700
账户余额为:400
自己--一共取了:600
结果不正确。
如果按照之前同步方法一样,在方法面前加同步关键字
public synchronized void test()....以下省略,一样的
执行,结果
账户余额为:-300
妻子--一共取了:700
账户余额为:-300
自己--一共取了:600
并没有起到作用,因为没有锁到,这里是需要对账户进行同步,而锁的方法里的都是取款机的属性,并不包括账户,用同步块
/*
public synchronized void test()
目标不对,锁失败
因为我们要锁住的是account,账户,但这个方法是ATM的成员方法,
实际上我们锁的是ATM的,没有锁住account账户的。
*/
public void test(){
// 提高效率 因为线程同步很耗性能 提前判断有很大帮助
if(account.money<=0){
return;
}
// 锁定account synchronized块
synchronized (account){
if(account.money -drawMoney<0){
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.money -=drawMoney;
drawTotal+=drawMoney;
System.out.println("账户余额为:"+account.money);
System.out.println(this.getName()+"--一共取了:"+drawTotal);
}
}
再执行
账户余额为:400
自己--一共取了:600
结果正确。所以如何准确的使用锁,也就是同步关键字是比较难的,需要多观察多练习。
扩展:锁容器
先写一个list来循环
List<String> list = new ArrayList<>();
for (int i = 0; i <10000 ; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
Thread.sleep(10000);
System.out.println(list.size());
执行:多执行几次会发现结果并不是10000,不准确。在juc的并发编程中,list有对应的并发容器,直接供我们使用,不用我们写同步块。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i <10000 ; i++) {
new Thread(()->{
// synchronized (list) {
list.add(Thread.currentThread().getName());
// }
}).start();
}
Thread.sleep(10000);
System.out.println(list.size());