如何做到类的线程安全

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何做到类的线程安全相关的知识,希望对你有一定的参考价值。

参考技术A 1.栈封闭  (所有的变量都是在方法内部声明的,这些变量都是处于栈封闭状态(也就是局部变量都是线程安全的))

2,无状态  (没有任何成员变量的类)

3.让类不可变   (String 基本类型包装类 都是不可变的类)

       1. 加final 关键字。 对于一个类,所有的成员变量应该都是私有的,同样的只要有可能,变量应该加上 final关键字

       2. 根本就不提供修改成员变量的地方,同时成员变量也不作为方法的返回值

4.volatile  保证类的可见性,最适合一个线程写,多个线程读的情景

5.加锁 和 CAS

6.ThreadLocal

java并发安全详解

类的线程安全定义 

如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。

类的线程安全表现为:

  • 操作的原子性
  • 内存的可见性

不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。

怎么才能做到类的线程安全?

栈封闭

所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态。

无状态

没有任何成员变量的类,就叫无状态的类

让类不可变

让状态不可变,两种方式:

1,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。

2、根本就不提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值

volatile

保证类的可见性,最适合一个线程写,多个线程读的情景,

加锁和CAS

synchronized、reentrantLock和cas

安全的发布

类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。

TheadLocal

死锁

死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象, 若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。

死锁的根本成因:获取锁的顺序不一致导致。

解决办法:保证加锁的顺序性

简单死锁示例

public class DeadLockDemo {
    public static void main(String[] args) {
        // 创建两个线程
        DeadLockThread dt1 = new DeadLockThread(false);
        DeadLockThread dt2 = new DeadLockThread(true);
        // 启动线程
        new Thread(dt1).start();
        new Thread(dt2).start();
    }
}

class DeadLockThread implements Runnable {
    // 标记变量
    private boolean flag;

    public DeadLockThread(boolean flag) {
        super();
        this.flag = flag;
    }

    public void run() {
        // dt1线程执行该方法
        if (flag) {
            synchronized (ThreadLock.locka) {
         Thread.sleep(100); System.out.println(
"if locka!"); synchronized (ThreadLock.lockb) { System.out.println("if lockb!"); } } } // dt2线程执行该方法 else { synchronized (ThreadLock.lockb) {
Thread.sleep(100); System.out.println(
"else lockb!"); synchronized (ThreadLock.locka) { System.out.println("else locka!"); } } } } } class ThreadLock { static Object locka = new Object(); static Object lockb = new Object(); }

运行结果:

if locka!
else lockb!

说白了,就是线程一等待获取被线程二占有的锁,线程二等待获取被线程一占有的锁,互相打架!!!

这种是最简单的,没什么好说的。。。

动态死锁

动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。

解决:

1、 通过内在排序,保证加锁的顺序性

2、通过尝试拿锁,也可以。

账户类:

package com.ty.thread.account;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *类说明:用户账户的实体类
 */
public class UserAccount {

    //private int id;
    private final String name;//账户名称
    private int money;//账户余额
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;
    }
}

转账接口:

package com.ty.thread.transfer;

import com.ty.thread.account.UserAccount;

/**
 *类说明:银行转账动作接口
 */
public interface ITransfer {
    /**
     * 
     * @param from 转出账户
     * @param to 转入账户
     * @param amount 转账金额
     * @throws InterruptedException
     */
    void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException;
}

转账线程类:

package com.ty.thread.worker;

import com.ty.thread.account.UserAccount;
import com.ty.thread.transfer.ITransfer;
/**
 * 执行转账动作的线程
 */
public 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();
        }
    }
}

下面会分为几种转账场景,一一来说明:

1、不安全的锁策略

TrasnferAccount

package com.ty.thread.transfer.impl;

import com.ty.thread.account.UserAccount;
import com.ty.thread.transfer.ITransfer;

/**
 *类说明:不安全的转账动作的实现
 */
public class TrasnferAccount implements ITransfer {

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount) 
            throws InterruptedException {
        synchronized (from){//先锁转出
            System.out.println(Thread.currentThread().getName() + " get"+from.getName());
            Thread.sleep(100);
            synchronized (to){//再锁转入
                System.out.println(Thread.currentThread().getName() + " get"+to.getName());
                from.flyMoney(amount);
                to.addMoney(amount);
            }
        }
    }
}

运行主类:

/**
 *类说明:模拟支付公司转账的动作
 */
public class PayCompany {

    public static void main(String[] args) {
        UserAccount zhangsan = new UserAccount("zhangsan",20000);
        UserAccount lisi = new UserAccount("lisi",20000);
        ITransfer transfer = new TrasnferAccount();
        TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi",zhangsan,lisi,2000,transfer);
        TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan",lisi,zhangsan,4000,transfer);
        zhangsanToLisi.start();
        lisiToZhangsan.start();

    }
}

运行结果:

技术图片

 

会发现一直卡在这,发生了死锁。

原因:上面启动了两个线程,每个线程的from其实是不同的,对于zhangsanToLisi线程来说,from是zhangsan,to是lisi,但是对于lisiToZhangsan线程来说,则是反的,所以两个线程启动后,会产生死锁的情况。

2、安全但是不好理解的锁策略

package com.ty.thread.transfer.impl;

import com.ty.thread.account.UserAccount;
import com.ty.thread.transfer.ITransfer;

/**
 *类说明:不会产生死锁的安全转账
 */
public class SafeOperate implements ITransfer {
    private static Object tieLock = new Object();//加时赛锁

    @Override
    public void transfer(UserAccount from, UserAccount to, int amount)
            throws InterruptedException {
        //通过对象的hash值比较,这样就不会出现from to混乱的情况
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //先锁hash小的那个
        if(fromHash<toHash) {
            synchronized (from){
                System.out.println(Thread.currentThread().getName()
                        +" get"+from.getName());
                Thread.sleep(100);
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                            +" get"+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }            
        }else if(toHash<fromHash) {
            synchronized (to){
                System.out.println(Thread.currentThread().getName()
                        +" get"+to.getName());
                Thread.sleep(100);
                synchronized (from){
                    System.out.println(Thread.currentThread().getName()
                            +" get"+from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                }
            }            
        }else {//解决hash冲突的方法
            synchronized (tieLock) {
                synchronized (from) {
                    synchronized (to) {
                        from.flyMoney(amount);
                        to.addMoney(amount);                        
                    }
                }
            }
        }
        
    }
}

这种是比较麻烦的方式,不过却是线程安全的。

3、安全好理解的策略

首先修改账户类,增加lock锁

package com.ty.thread.account;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 *类说明:用户账户的实体类
 */
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;
    }
}
package com.ty.thread.transfer.impl;

import java.util.Random;

import com.ty.thread.account.UserAccount;
import com.ty.thread.transfer.ITransfer;

/**
 *类说明:不会产生死锁的安全转账第二种方法,尝试拿锁
 */
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();
                }
            }
            //这地方加休眠也是有原因的,因为代码逻辑是锁两个账户对象,假如线程一拿到了A账户的锁,线程二拿到了B账户的锁,这是线程一获取B的锁失败,释放A的锁,线程二则获取A失败,又释放了B,相互礼让。。。通过休眠降低这种竞争概率!
            Thread.sleep(r.nextInt(10));
        }
    }
}

运行方法:

    public static void main(String[] args) {
        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实现线程安全。

以上是关于如何做到类的线程安全的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 AppDomain 限制静态类的范围以进行线程安全使用?

我是怎样测试Java类的线程安全性的

图文详解:如何做到操作系统和并发同步的结合?

SimpleDateFormat类的线程安全问题和解决方案

C++/MFC/ATL 线程安全字符串读/写

[Java 并发编程实战] 设计线程安全的类的三个方式(含代码)