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
  • 替换独占锁
    使用读写锁等

下一篇看这里:JAVA并发编程-10-JMM和底层实现原理

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值