JAVA并发编程-9-并发安全
作者:互联网
线程并发安全的理解
上一篇看这里:JAVA并发编程-8-线程池
一、类的线程安全的定义
如果多线程下使用这个类,不论多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。
类的线程安全表现为:
- 操作的原子性,使用这个类中的方法进行操作的时候,对外界调用者来说,操作不可被切割中断,要么全部成功,要么全部失败
- 内存的可见性,当线程对类的数据进行修改了以后,其他线程可以马上看到修改后的值
不做正确的同步,在多个线程之间共享状态(即类的属性)的时候,就会出现线程不安全。
二、怎么才能做到类的线程安全
1、栈封闭
所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。
线程在执行方法时,会将方法打包成一个栈桢,放到自己的方法栈中去执行,由于栈是线程私有的,不同线程执行方法不会相互影响。
2、无状态
没有任何成员变量的类,就叫无状态的类
线程安全问题是由类在多个线程之间共享状态(即类的属性)产生的,如果类没有属性,即没有状态,则不会产生线程安全问题。
3、让类不可变
让状态不可变,两种方式:
1,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值
4、volatile
保证类的可见性,最适合一个线程写,多个线程读的情景,
5、加锁和CAS
加锁和CAS前面章节反复提到,是一种线程安全的操作
6、安全的发布
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。
7、TheadLocal
TheadLocal线程副本,是保存在每个线程中的key,value结构,线程之间不可见,可以保证线程安全。
三、线程不安全会产生的问题
1、死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁
资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。
public class NormalDeadLock {
private static Object valueFirst = new Object();//第一个锁
private static Object valueSecond = new Object();//第二个锁
//先拿第一个锁,再拿第二个锁
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueSecond) {
System.out.println(threadName+" get second");
}
}
}
//先拿第二个锁,再拿第一个锁
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueSecond) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueFirst) {
System.out.println(threadName+" get second");
}
}
}
}
如上代码,定义了两把锁(多个资源),fisrtToSecond先拿第一个锁再拿第二个锁,SecondToFisrt先拿第二个锁再拿第一个锁。启动两个线程,线程A拿到了第一把锁,线程B拿到了第二把锁,这时线程A去拿第二把锁,但是被线程B占有无法拿到,而线程B也在尝试拿第一把锁,被线程A占有。产生了死锁。
死锁的根本成因:获取锁的顺序不一致导致。
解决方式:要保证加锁的顺序
//先拿第一个锁,再拿第二个锁
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueSecond) {
System.out.println(threadName+" get second");
}
}
}
//先拿第二个锁,再拿第一个锁
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (valueFirst) {
System.out.println(threadName+" get first");
SleepTools.ms(100);
synchronized (valueSecond) {
System.out.println(threadName+" get second");
}
}
}
2、活锁
尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
定义一个转账操作,按照顺序对from和to加锁,finally里面解锁
/**
*
*类说明:用户账户的实体类
*/
public class UserAccount {
//private int id;
private final String name;//账户名称
private int money;//账户余额
//显示锁
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
public UserAccount(String name, int amount) {
this.name = name;
this.money = amount;
}
public String getName() {
return name;
}
public int getAmount() {
return money;
}
@Override
public String toString() {
return "UserAccount{" +
"name='" + name + '\'' +
", money=" + money +
'}';
}
//转入资金
public void addMoney(int amount){
money = money + amount;
}
//转出资金
public void flyMoney(int amount){
money = money - amount;
}
}
public class SafeOperateToo implements ITransfer {
@Override
public void transfer(UserAccount from, UserAccount to, int amount)
throws InterruptedException {
Random r = new Random();
while(true) {
if(from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+from.getName());
if(to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
//两把锁都拿到了
from.flyMoney(amount);
to.addMoney(amount);
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
}
}
}
模拟张三to李四,和李四to张三,两个操作,启动两个线程调用转账方法。
/**
* 类说明:模拟支付公司转账的动作
*/
public class PayCompany {
/*执行转账动作的线程*/
private static class TransferThread extends Thread {
private String name;//线程名字
private UserAccount from;
private UserAccount to;
private int amount;
private ITransfer transfer; //实际的转账动作
public TransferThread(String name, UserAccount from, UserAccount to,
int amount, ITransfer transfer) {
this.name = name;
this.from = from;
this.to = to;
this.amount = amount;
this.transfer = transfer;
}
public void run() {
Thread.currentThread().setName(name);
try {
transfer.transfer(from, to, amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount zhangsan = new UserAccount("zhangsan", 20000);
UserAccount lisi = new UserAccount("lisi", 20000);
ITransfer transfer = new SafeOperateToo();
TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi"
, zhangsan, lisi, 2000, transfer);
TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan"
, lisi, zhangsan, 4000, transfer);
zhangsanToLisi.start();
lisiToZhangsan.start();
}
}
可以看到执行结果,尝试加锁了很多次。
ReentrantLock.trylock在拿不到锁的时候不会进行等待,而是在finally中执行解锁操作。这样线程A拿到1锁拿不到2锁时,解锁进行下一次循环,线程B拿到2锁拿不到1锁时,解锁进行下一次循环,不断尝试,知道某个时间差将其错开。
解决办法:每个线程休眠随机数,错开拿锁的时间。
3、线程饥饿
低优先级的线程总是拿不到cpu的执行时间。这种情况在我们的开发过程中比较少碰到。
四、性能和思考
使用并发的目标是为了提高性能,但是引入多线程后,也会引入额外的开销。但是如果使用的不合理,往往可能使程序运行的效率降低。
衡量应用的程序性能:
- 服务时间,延迟时间(多快)
- 吞吐量(处理能力的指标,完成工作的多少)
- 可伸缩性
多快和多少完全独立,甚至是相互矛盾的。
这是架构设计最重要的需要权衡的部分
对服务器应用来说:多少(可伸缩性,吞吐量)比多快更受重视
做应用的时候:
1、先保证程序正确,确实达不到要求,再提高速度(黄金原则)
2、一定要以测试为基准
一个应用程序中,串行的部分总是有的
Amdahl定律:1/(F+(1-N)/N) F-串行部分,N-并行部分,最好的情况 1/F
影响性能的因素
1、上下文切换
如果当前可运行线程大于cpu的数量,cpu会把它的可执行时间进行分片,分给一个个线程去运行,当一个线程将时间分片用完了以后,操作系统需要将当前运行完了的线程从cpu上拿走,把其他线程load进来,让其他线程来使用cpu,这个过程就是一次上下文切换。上下文切换包括将原线程的各种数据等保存,将新线程所需要的数据读到cpu寄存器,这些都是上下文切换所要耗费的资源,大约需要5000-10000个时钟周期。
2、内存同步
即加锁,需要执行很多内存屏障等额外指令,这会带来性能上的损失
3、阻塞
线程挂起,包括两次额外的上下文切换
减少锁的竞争
- 缩小锁的范围
对锁的持有快进快出,尽量缩短持有锁的时间
比如sleep操作不会释放锁,不建议在加锁代码中使用 - 减少锁的粒度
在使用锁的时候,锁保护的对象是多个,多个对象其实是独立变化的,应该使用多个锁一一保护每个对象 - 锁分段
比如ConcurrentHashmap - 替换独占锁
使用读写锁等
标签:threadName,JAVA,编程,private,并发,amount,线程,UserAccount,public 来源: https://blog.csdn.net/weixin_40292704/article/details/106222765