线程安全问题
什么是线程安全问题
当使用多个线程访问同一个资源数据时,非常容易出现线程安全问题;比如前后对同一个数据进行操作,因为线程执行时是抢占式的,一个线程在执行一个操作,可能会被其他线程打断,导致操作没有完全完成,可能会造成数据出现不一致的情况。就这些问题而言我们就有了相对应的解决办法:同步机制
实现同步的几种方法:
1.同步代码块
给一段代码上锁,粒度比同步方法小,粒度越小越灵活,性能更高
//同步代码块
synchronized (锁对象) {
代码
}
锁对象(指的是调用改代码块的对象),可以对当前线程进行控制;例如wait、和notify通知等。
2.同步方法
public synchronized 数据返回类型 方法名(){}
就是使用synchronized关键字对方法进行修饰,作用就是给整个方法上锁,其他线程无法执行,当前方法调用结束后释放锁。
/**
* 模拟转账
*/
public synchronized void transfer(int from,int to,int money){
if(accounts[from] < money){
throw new RuntimeException("余额不足");
}
accounts[from] -= money;
System.out.printf("从%d转出%d%n",from,money);
accounts[to] += money;
System.out.printf("向%d转入%d%n",to,money);
System.out.println("银行总账是:" + getTotal());
}
两种方法都被关键字synchronized修饰,synchronized是干什么的?
如果代码或方法被synchronized包含,那么JVM会启动monitor(监视器)对其进行监控,因为这个时候线程就持有了锁,其他线程想要执行代码就会被monitor拒绝访问,持有锁的线程执行完释放锁,其他线程就能执行;如果锁没有被其他线程持有,当前线程就持有了锁,就可以执行代码。
同步块和同步方法的区别:
- 同步块可以控制代码的范围,同步方法是整个方法上锁
- 同步块可以将任意的成员变量作为锁,同步方法只能以this作为锁
- 同步块的性能高于同步方法
3.同步锁
我们可以实现Lock接口,它的两种基本方法lock()上锁和unlock()释放锁;
常见的实现类:ReentryLock重入锁,控制线程进入;WriteLock 写锁,控制线程写入;ReadLock读锁,控制线程读取;ReadWriteLock 读写锁,控制线程读写
简单的使用方法:
//定义同步锁对象(成员变量)
Lock lock = new ReentrantLock();
//方法内部上锁
lock.lock();
try{
代码...
}finally{
//释放锁
lock.unlock();
}
注意:锁对象不能是局部变量
三种上锁机制的总结:
- 编程简便:同步方法 > 同步代码块 > 同步锁
- 性能:同步锁 > 同步块 > 同步方法
- 同步锁提供了大量的方法,也可以if配合使用,更加灵活
悲观锁和乐观锁
乐观锁就像生活中乐观的人总是想着事情往好的方向发展,悲观锁就像生活中悲观的人总是想着事情往坏的方向发展。
悲观锁:认为线程很容易会出现线程安全问题,会对代码上锁,之前所讲的哪几种都属于悲观锁;悲观锁的锁定和释放锁消耗的资源时比较多的,这样降低了程序的性能;
乐观锁:不会对代码上锁,它认为线程安全问题不是很常见,但是它会去判断别人有没有更新过,如何判断可以利用版本号机制和CAS算法来实现。
两者区别
悲观锁更加重量级,占用比较多的资源适应于多读少写的场景;乐观锁更轻量级,性能高,适应于多写少读的场景。两种锁就像开篇说的对应的两种人一样,都有各自的优缺点,不能认为一种锁就好于另一种锁。
乐观锁的两种实现方法
版本号机制:
一般就是在数据上加一个version字段来代表被修改的次数,每修改一次version都会+1;假如一个线程要更新时,在读取数据的同时也会读取version值,在提交更新时,会判断刚才读取到的version值是不是当前数据库中的version值,如果相等才更新,否则重试更新操作,直到更新成功。
CAS(Compare And Swap )算法:
就我浅薄的知识来看就是可以在不使用锁的情况下来实现多线程之间的变量同步,CAS算法涉及到三个值得操作:需要读写的内存的值(通过内存偏移量来获得的值)、计算得出的预计值(要比较的值)、提交的实际的值,如果实际值和预计值相同,那么就用新值来更新内存的值,否则就不修改。
原子类
AtomicInteger类
原子整数,底层使用了CAS算法实现整数递增和递减操作
常用方法:incrementAndGet 原子递增、decrementAndGet 原子递减
static int count = 0;
static AtomicInteger integer = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(() ->{
count++;
//递增
integer.incrementAndGet();
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count:"+count);
System.out.println("atomic:"+integer.get());
}
CAS算法存在的问题:
1.简单来说就是一个线程将a值改成了b值,接着又改成了a,此时CAS认为是没有发生变化的,被称为CAS操作的“ABA问题”;
2.如果实际值和预计值不一样,就会处于循环状态,一直到成功才会结束,对CPU的消耗很大。
ThreadLocal
线程的局部变量,每个线程都是单独的,线程中的变量是不会互相影响的
//线程局部变量
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
//设置初始值
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
count++;
local.set(local.get() + 1);
System.out.println(Thread.currentThread().getName() + "--->" + local.get());
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
System.out.println(local.get());
}
ThreadLocal的实现
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
从中我们可以看出它可以通过当前线程得到其中的ThreadLocalMap集合,将该数据绑定到该map中
以上是小编今天的学习,如有不足之处还请指出,感谢!