类的线程安全定义
如果多线程下使用这个类,不管多线程如何调度这个类,这个类总是表现出正确的行为,那么这个类就是线程安全的。
一般类的线程安全包括:操作的原子性、内存共享
栈封闭
所有的变量都是在方法内部中声明的,那么这些变量都属于栈封闭状态,是属于线程安全的。
无状态
没有任何成员变量的类,就叫无状态类。
让类不可变
让状态不可变。所有基本类型的包装类都是不可变类。对于一个类来说,所有的成员变量是私有的,同样的只要有可能,所有变量都应该加上final关键字。或者根本就不提供任何修改成员变量的地方,同时成员变量也不做返回值。
volatile
保证类的可见性。最适合一个线程写多个线程读场景
ConcurrenthashMap中的value值, volatile V val;get方法是线程共享的,put加锁只有一个线程可以写。
Servlet
不是线程安全,在需求上,很少有共享需求。其次接收到请求后,返回应答的时候,都是有一个线程负责。常见的需求,通次一个网站请求次数,如果不对变量做共享处理,最终统计的值会不准确。
线程不安全引发的问题
死锁
死锁概念:两个或者以上的线程相互竞争资源或者由于彼此的通信而造成的一种阻塞现象。若无外力作用,他们都无法继续执行下去。此时系统处于死锁状态。 死锁原因是引用获取锁的顺序不一致导致的
演示普通死锁
从代码直观的看到锁的顺序不一致
public class DealSync {
private static String firstSync = "第一把锁";
private static String secondSync = "第二把锁";
//先拿锁1,然后锁2
public void sync1(){
synchronized (firstSync){
System.out.println(Thread.currentThread().getId()+"拿到第一把锁");
SleepTools.sencod(1);
synchronized (secondSync){
System.out.println(Thread.currentThread().getId()+"拿到第二把锁");
}
}
}
//先拿锁2,然后拿锁1
public void sync2(){
synchronized (secondSync){
System.out.println(Thread.currentThread().getId()+"拿到第二把锁");
SleepTools.sencod(1);
synchronized (firstSync){
System.out.println(Thread.currentThread().getId()+"拿到第一把锁");
}
}
}
public static void main(String[] args) {
DealSync dealSync = new DealSync();
for (int i = 0; i < 2; i++) {
if (i==0){
new Thread(()->{
dealSync.sync1();
}).start();
}else {
new Thread(()->{
dealSync.sync2();
}).start();
}
}
}
}
13拿到第一把锁
14拿到第二把锁
理论上执行完后,main函数将退出程序的,但是现在程序一直处于运行状态,就产生了死锁。好比如你打架,两个人手都抓住对方头发,谁也不愿意先放手一个道理。可以利用jdk自带的线程查看
输入jkstack [程序号]
普通死锁解决
通过上面举例可以看出来,死锁原有是获取锁顺序不一致导致的。所以解决原因之一就是保证锁的顺序一致,要么同时获取第一把锁,要么同时获取第二把锁。
演示动态的死锁
何为动态死锁,就是说我获取锁的顺序一致,但是缺导致线程死锁
//转账的实体类
public class UserBean {
//账户名称
private String name;
//金额
private Integer money;
public UserBean(String name, Integer money) {
this.name = name;
this.money = money;
}
//转入的资金
public void addMoeny(Integer money) {
this.money += money;
}
//转出的资金
public void flyMoneny(Integer money) {
this.money -= money;
}
public String getName() {
return name;
}
public Integer getMoney() {
return money;
}
}
/*
* 转钱动作
* */
public class TrasnferAccount {
//from转出的人,to转入的人,money金额
public void trasnfer(UserBean from, UserBean to, Integer money) {
System.out.println("--------【"+from.getName()+"】转【"+money+"】元给【"+to.getName()+"】--------------");
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
}
public static void main(String[] args) {
TrasnferAccount obj = new TrasnferAccount();
//默认都是两千
UserBean zs = new UserBean("张三", 2000);
UserBean ls = new UserBean("李四", 2000);
Thread zsThread = new Thread(() -> {
//这里入参我们始终保持了先锁from然后在锁to的原则
obj.trasnfer(zs,ls,500);
});
Thread lsThread = new Thread(() -> {
//这里入参我们始终保持了先锁from然后在锁to的原则
obj.trasnfer(ls,zs,500);
});
//启动两个线程
zsThread.start();
lsThread.start();
}
}
Connected to the target VM, address: '127.0.0.1:51573', transport: 'socket'
--------【张三】转【500】元给【李四】--------------
张三现有余额【2000】
--------【李四】转【500】元给【张三】--------------
李四现有余额【2000】
张三开始转出【500】元!当前余额为【1500】
李四开始转出【500】元!当前余额为【1500】
按照上面图片,打开线程监控器。查看死锁线程。
观察输出结果可以发现,在拿to这个锁时候,产生了死锁。那既然获取锁的顺序都一致,为什么还会导致死锁呢?
上面我们从代码上看先锁from(转出)然后锁to(转入)是没问题的,但是实际上形参引用的对象实际上是可以改变的。
张三转给李四,此时形参from对象为张三,拿到张三锁,接着需要拿到李四锁。
李四转给张三,此时形参from对象为李四,拿到李四锁,接着需要拿到张三锁。
动态死锁解决方法-内置锁
上面总结后发现最终导致死锁的原有依然是拿锁的顺序导致的。如果我们这时候,可以给形参from和to进行有顺序的加锁不就解决了吗?那么对象怎么样可以保证顺序呢?第一个方法是取对象的hashCode方法,但是这个方法有个不好地方就是用可能重写hashCode导致无法使用,那么最稳妥的方法就是获取对象最原始的hashCode采用System.identityHashCode(obj)方法;
//比较安全的转账(有几率会出现一定的不安全的)
public void safeTrasnfer(UserBean from, UserBean to, Integer money) {
System.out.println("--------【" + from.getName() + "】转【" + money + "】元给【" + to.getName() + "】--------------");
//使用系统自带的获取对象最初始的System.identityHashCode方法。不用HashCode原因是因为用户肯可能会重写hashCode方法
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
System.out.println("fromHash=" + fromHash + "---toHash=" + toHash);
//下面逻辑是,hash小的认为from(转出),hash大的是to(转入)
if (fromHash < toHash) {
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
} else if (fromHash > toHash) {
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
}
}
}
}
细心已经发现上面hashcode比较没有等于这个情况,一般这个情况也不太可能出现,出现概率比你买彩票还低百倍(这就是所谓的hash冲突,但是System.identityHashCode方法发送hash冲突概率千万分之一)
这就好比一场比赛,结果是输赢,还有一种可能平局,那平局肯定要加比赛的,直至冠军产生。但是上面写法如果是hashcode一致,那么就没法正常转账。下面看下hash冲突时候解决方案。
//当hash冲突时候,谁先获取到这个锁,谁先执行
private Object object = new Object();
//接着之前的判断加上else,千万分之一概率走到这个else
else {
synchronized (object){
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
}
}
此外除了上面的hashcode比较方法还有一个更简单的就是,保证银行账号表的id是唯一的,然后用id进行比较大小即可。这是最简单最通用的方式
动态死锁解决方案-显示锁
//在原来UserBean新增一个显示锁获取方法
//构建一个显示锁
private final Lock lock = new ReentrantLock();
public Lock getLock(){
return lock;
}
//开启一个显示锁(锁可重入)当前线程,拿到from和to锁时候,在进行转账
public void safeTrasnferLock(UserBean from, UserBean to, Integer money) {
Random random = new Random();
while (true) {
//如果from获取到锁后
if (from.getLock().tryLock()) {
try {
//如果to获取锁
if (to.getLock().tryLock()){
try{
//from钱减少
System.out.println("------------------"+from.getName()+"转账给"+to.getName()+"--------------");
System.out.println(from.getName() + "转账前余额【" + from.getMoney() + "】");
from.flyMoneny(money);
System.out.println(from.getName()+"开始转账【"+money+"】元!");
System.out.println(from.getName() + "转账后余额为【" + from.getMoney() + "】");
//to加钱
System.out.println(to.getName() + "入账前余额【" + to.getMoney() + "】");
to.addMoeny(money);
System.out.println(to.getName() + "入账后余额【" + to.getMoney() + "】");
break;
}finally {
to.getLock().unlock();
}
}
} finally {
//释放
from.getLock().unlock();
}
}
//每循环一次,休眠短暂片刻,目的是为了让两个线程错开尝试拿锁的时间。防止活锁产生。可以把下面代码注释掉看下控制台打印的
SleepTools.ms(random.nextInt(10));
}
}
------------------张三转账给李四--------------
张三转账前余额【2000】
张三开始转账【500】元!
张三转账后余额为【1500】
李四入账前余额【2000】
李四入账后余额【2500】
------------------李四转账给张三--------------
李四转账前余额【2500】
李四开始转账【1000】元!
李四转账后余额为【1500】
张三入账前余额【1500】
张三入账后余额【2500】
观察结果发现最终两个人的余额是我们理想中的。但是为什么在while循环里加上SleepTools.ms(random.nextInt(10));这句话呢?
加上这句话避免线程产生【活锁】所谓的活锁就是说,两个线程在互相谦让,比如A线程拿到锁1,B线程拿到锁2,A想要锁2,B想要锁1,这时候AB都很谦让,就把自己持有锁全部释放了,重新拿锁,这时候恰好A又拿到锁1,B又拿到锁2…不断循环上面操作,直至拿到对应需要的锁为止,活锁是可以自己解开,而加上短暂的休眠就是为了让它两个线程错开拿锁。一定程度上可以避免活锁。
影响性能的因素
线程饥饿
指的是线程优先级低的总是拿不到执行时间
性能和思考
使用并发是为了提高性能和响应速度,但同时的带来是资源的开销变多了。所以一般做应用时候,先确保程序能够准时上线之后,经过测试反应速度慢后,在反过来进行优化比较好。
上下文切换
推荐线程池的线程配置数是系统的核数+1,为了避免的是cpu进行上下文切换所消耗时间,每次cpu切换耗时在几微秒之间。
阻塞
由于线程未拿到锁会进入阻塞队列中,拿唤醒时候,需要进行挂起和包括两次额外的上下文切换时间。
减少锁的粒度
在使用锁的时候,锁保护的对象有多个,多个对象之间其实是独立变化的。这个时候不如用多个锁一一保护对象。
缩小减的范围
对锁的持有,快进快去,尽量缩短锁占用的时间也就是指缩小锁的范围。
//普通的加锁方法,必须等待这个方法全部执行结束,才会释放锁,而我们真正需要加锁的是list.add方法。其他不需要加锁
public synchronized void addList2(int i) {
list.add(i);
//假设这里有一个耗时为100ms的操作
SleepTools.ms(10);
}
//优化后,只对list.add方法加锁,缩小锁的范围
public void addList(int i) {
synchronized (this) {
list.add(i);
}
//假设这里有一个耗时为100ms的操作
SleepTools.ms(10);
}
避免多余的缩减锁的范围
//在一个方法里采用多次加锁方法,
public void addList(int i) {
synchronized (this) {
list.add(i);
}
list.add(i*30);
synchronized (this) {
list.add(i*40);
}
//假设这里有一个耗时为100ms的操作
SleepTools.ms(10);
}
线程安全的单列模式
懒汉模式-双重检测
public class TestDemo10 {
private static TestDemo10 demo10;
public TestDemo10() {
}
//单线程的双重检测
public static TestDemo10 getDemo10() {
//如果对象已经存在,之间return
if (demo10 == null) {
//new对象时候,加锁
synchronized (TestDemo.class) {
//还需在判断一次,是音乐,在拿到锁的时候,其他线程在持有锁的时候,可能已经new出了对象
//举个列子,A、B两个线程同时拿对象,A先拿到锁,A等待锁释放后已经new出对象了。B拿到锁后,这个时候对象已经不为空了
if (demo10 == null) {
demo10 = new TestDemo10();
}
}
}
return demo10;
}
public static void main(String[] args) {
}
}
上面列子是线程安全的吗?答案不是的,看下图
如何保证线程安全呢?很简单 private volatile static TestDemo10 demo10;就可以,让对象属于内存可见。
Jconsole使用
cmd输入Jconsole打开
完整代码
动态死锁代码
package ms.studyjava.threads.demo10.account;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserBean {
//账户名称
private String name;
//金额
private Integer money;
//构建一个显示锁
private final Lock lock = new ReentrantLock();
public Lock getLock(){
return lock;
}
public UserBean(String name, Integer money) {
this.name = name;
this.money = money;
}
//转入的资金
public void addMoeny(Integer money) {
this.money += money;
}
//转出的资金
public void flyMoneny(Integer money) {
this.money -= money;
}
public String getName() {
return name;
}
public Integer getMoney() {
return money;
}
}
package ms.studyjava.threads.demo10.account;
import ms.studyjava.threads.utils.SleepTools;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
* 转钱动作
* */
public class TrasnferAccount {
//from转出的人,to转入的人,money金额
public void trasnfer(UserBean from, UserBean to, Integer money) {
System.out.println("--------【" + from.getName() + "】转【" + money + "】元给【" + to.getName() + "】--------------");
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
}
//当hash冲突时候,谁先获取到这个锁,谁先执行
private Object object = new Object();
//比较安全的转账(有几率会出现一定的不安全的)
public void safeTrasnfer(UserBean from, UserBean to, Integer money) {
System.out.println("--------【" + from.getName() + "】转【" + money + "】元给【" + to.getName() + "】--------------");
//使用系统自带的获取对象最初始的System.identityHashCode方法。不用HashCode原因是因为用户肯可能会重写hashCode方法
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
System.out.println("fromHash=" + fromHash + "---toHash=" + toHash);
//下面逻辑是,hash小的认为from(转出),hash大的是to(转入)
if (fromHash < toHash) {
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
} else if (fromHash > toHash) {
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
}
}
} else {
synchronized (object) {
synchronized (from) {
System.out.println(from.getName() + "现有余额【" + from.getMoney() + "】");
//扣钱
from.flyMoneny(money);
SleepTools.sencod(1);
System.out.println(from.getName() + "开始转出【" + money + "】元!当前余额为【" + from.getMoney() + "】");
synchronized (to) {
System.out.println(to.getName() + "现有余额【" + to.getMoney() + "】");
//增加钱
to.addMoeny(money);
SleepTools.sencod(1);
System.out.println(to.getName() + "收到转账【" + money + "】元!当前余额为【" + to.getMoney() + "】");
}
}
}
}
}
//开启一个显示锁(锁可重入)当前线程,拿到from和to锁时候,在进行转账
public void safeTrasnferLock(UserBean from, UserBean to, Integer money) {
Random random = new Random();
while (true) {
//如果from获取到锁后
if (from.getLock().tryLock()) {
try {
//如果to获取锁
if (to.getLock().tryLock()){
try{
//from钱减少
System.out.println("------------------"+from.getName()+"转账给"+to.getName()+"--------------");
System.out.println(from.getName() + "转账前余额【" + from.getMoney() + "】");
from.flyMoneny(money);
System.out.println(from.getName()+"开始转账【"+money+"】元!");
System.out.println(from.getName() + "转账后余额为【" + from.getMoney() + "】");
//to加钱
System.out.println(to.getName() + "入账前余额【" + to.getMoney() + "】");
to.addMoeny(money);
System.out.println(to.getName() + "入账后余额【" + to.getMoney() + "】");
break;
}finally {
to.getLock().unlock();
}
}
} finally {
//释放
from.getLock().unlock();
}
}
//每循环一次,休眠短暂片刻,目的是为了让两个线程错开尝试拿锁的时间。防止活锁产生。可以把下面代码注释掉看下控制台打印的
// SleepTools.ms(random.nextInt(10));
}
}
//完全不做任何操作的转钱,,你还发现最终的入账余额输出结果每次都花里胡哨的
public void noneDo(UserBean from ,UserBean to,Integer money){
System.out.println("------------------"+from.getName()+"转账给"+to.getName()+"--------------");
System.out.println(from.getName() + "转账前余额【" + from.getMoney() + "】");
from.flyMoneny(money);
System.out.println(from.getName()+"开始转账【"+money+"】元!");
System.out.println(from.getName() + "转账后余额为【" + from.getMoney() + "】");
//to加钱
System.out.println(to.getName() + "入账前余额【" + to.getMoney() + "】");
to.addMoeny(money);
System.out.println(to.getName() + "入账后余额【" + to.getMoney() + "】");
}
public static void main(String[] args) {
TrasnferAccount obj = new TrasnferAccount();
//默认都是两千
UserBean zs = new UserBean("张三", 2000);
UserBean ls = new UserBean("李四", 2000);
Thread zsThread = new Thread(() -> {
obj.noneDo(zs, ls, 500);
});
Thread lsThread = new Thread(() -> {
obj.noneDo(ls, zs, 1000);
});
//启动两个线程
zsThread.start();
lsThread.start();
}
}