1、ReentranLock(瑞恩纯特Lock)
1、特性
1、可中断
synchronize锁不能用其他方式中断,ReentranLock就可以
举例:reentrantLock.lockInterruptibly();
该锁可以被线程的interrupt()打断,如果是lock则不会被打断
2、可以设置超时时间
synchronize会一直等待其他线程释放锁,ReentranLock可以设置一定时间后就不再去等待锁
3、可以设置公平锁
也就是可以设置线程先到先得
4、支持多个等待唤醒方式
值synchronize中,如果想要让线程休息,可以调用锁的wait和notify方法,这种会有问题,例如有多个线程同时休息,notify只能唤醒一个 ,notifyAll又全都唤醒,此时只想唤醒其中一个,就会出现问题
在reentrantLock中,可以给每个线程都有自己的wait和notify方法,就能很好的解决这个问题
static ReentrantLock reentrantLock = new ReentrantLock();
//休息室 可以控制 1线程的等待和唤醒
static Condition condition1 = reentrantLock.newCondition();
//休息室 可以控制 2线程的等待和唤醒
static Condition condition2 = reentrantLock.newCondition();
static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (i < 100) {
try {
reentrantLock.lock();
i++;
System.out.println(Thread.currentThread().getName() + " :" + i);
//1线程休息
condition1.await();
//2线程启动
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
}, "thread1").start();
new Thread(() -> {
while (i < 100) {
try {
reentrantLock.lock();
i++;
System.out.println(Thread.currentThread().getName() + " : " + i);
//1线程启动
condition1.signal();
//2线程休息
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}, "thread2").start();
}
2、乐观锁CAS
1、什么是CAS
以不需要锁的方式解决线程安全的问题, 传入修改前和修改后的值,比较修改后是否和修改后的值相同,如果修改失败则循环再次赋值,直到执行成功为止。需要配合volatile 关键字保证准确
2、CAS效率比较
cas比synchronize效率高,因为每次不需要阻塞,修改失败再次修改就好了,而synchronize则每次都会导致线程阻塞,所以效率会低。
但是如果线程并发大的情况下,cas会导致修改失败很多次,效率可能会低
3、乐观锁实现
1、原子整数
安全类有AtomicInteger、AtomicBoolean 、AtomicLong 等原子整数
1、拿AtomicInteger举例,我们使用AtomicInteger实现cas操作
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(0);
Thread thread1 = new Thread(() -> {addInt(atomicInteger);}, "thread1");
Thread thread2 = new Thread(() -> {addInt(atomicInteger);}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终结果:" + atomicInteger.get());
}
private static void addInt(AtomicInteger atomicInteger) {
while (atomicInteger.get() < 100) {
//获得当前值
int prev = atomicInteger.get();
//修改后的值
int next = prev + 1;
//这里返回true 即为修改成功 否则就一直修改
while (atomicInteger.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + " 修改后" + atomicInteger.get());
break;
}
}
}
2、也可以直接使用内部已经写好的方法进行操作,例如atomicInteger.compareAndSet(prev,next);,结果是一样的
2、原子引用
原子引用类型给了并发更多的可能性,例如不是int,long,是float类型,就需要用到原子引用类型
1、AtomicReference:
引用类型,可以放任何类型
2、AtomicStampedReference:
解决ABA问题,ABA问题就是只判断内容是否一致,而不判断内容是否被修改过。如果想要就算值一致,但是只要是修改过,就修改失败,就可以用这个引用类型,解决方案是增加一个版本号,每次修改都将版本号+1
getReference :获取值
getStamp:获取版本号
3、AtomicMarkableReference:
可以判断是否修改过,有一个构造参数就是true或者false
4、 AtomicReferenceFieldUpdater:保护对象中的字段的线程安全问题
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
User user = new User();
AtomicReferenceFieldUpdater<User, String> name = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
name.compareAndSet(user,null,"b");
System.out.println(user);
}
}
class User{
//必须是volatile修饰 不能用private修饰 否则找不到
volatile String name;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
3、原子累加器
1、LongAdder、DoubleAdder等
主要是做自增操作,相比AtomicLong速度快
2、原理
缓存行和伪共享
3、final不可变类
String为什么是final修饰的
1、线程安全
string不可变,修改值只会出现重新new一个string,这样的好处就是线程安全
2、hashcode不可变
不需要考虑hashcode会修改,这样传入hashmap中当做key时,不会出现找不到值的情况。例如传入时hashcode为1,结果值变了,hashcode变为了5,用5去找链表根本找不到该值,存在问题
4、ReentrantReadWriteLock(读写锁)
1、介绍
当出现可能会有脏读的情况的时候,也就是读取的数据不是最新的数据,此时就应该用到读写锁,该锁的特点是读写阻塞,写写阻塞,读读不阻塞
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock rLock = rwLock.readLock();
static ReentrantReadWriteLock.WriteLock wLock = rwLock.writeLock();
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{write();}).start();
Thread.sleep(500);
new Thread(() ->{read();}).start();
}
private static void read(){
rLock.lock();
try{
System.out.println("读操作");
Thread.sleep(1000);
System.out.println("读操作完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
}
private static void write(){
wLock.lock();
try{
System.out.println("写操作");
Thread.sleep(5000);
System.out.println("写操作完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
wLock.unlock();
}
}
2、注意
1、读锁不支持条件变量
2、重入时不支持锁升级,只支持锁降级,读锁去获取写锁,会导致死锁,但是可以先写锁再读锁,这样就可以正常执行
private static void read(){
wLock.lock();
rLock.lock();
try{
System.out.println("读操作");
Thread.sleep(1000);
System.out.println("读操作完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
wLock.unlock();
}
}
5、StampedLock
1、介绍
1、读锁和写锁都是需要配合一个戳使用
2、相对于读写锁,还多了一个乐观锁,读取后先做一次锁校验,如果这期间没有被修改,那么数据就可以正常使用,如果修改了,再去加锁,也就可以增加效率
StampedLock stampedLock = new StampedLock();
//读锁 获得到戳
long readStamped = stampedLock.readLock();
//解锁时必须使用这个戳
stampedLock.unlockRead(readStamped);
//写锁 获得到戳
long writStamped = stampedLock.writeLock();
//解锁时必须使用这个戳
stampedLock.unlockWrite(writStamped);
long tryStamped = stampedLock.tryOptimisticRead();
//true没有被修改 false为被修改了 没有加锁 只是判断了
boolean validate = stampedLock.validate(tryStamped);
if(validate){
//可以正常执行
}else{
//加锁
}
6、CountDownLatch
1、介绍
等待执行线程数执行完之后,调用await方法才能继续执行下去,否则就会一直阻塞,等待减完,例如 lol中10个玩家必须全部到达百分之百才可以进入游戏
static CountDownLatch countDownLatch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
// 2修改为3 会将倒计时减完 也就能正常运行下去
for (int i = 0; i < 3; i++) {
new Thread(() ->{service();},"thread" + i).start();
}
//等待线程数为0,如果线程数为0的情况,才可以继续,否则会阻塞
countDownLatch.await();
}
public static void service(){
System.out.println(Thread.currentThread().getName() + " 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行结束");
//线程数减1
countDownLatch.countDown();
}
7、ConcurrentHashMap
jdk7中,hashMap会导致循环链表
原理
get方法:没有使用锁
put方法:
第一次没有map,懒加载:初始化map方法:使用cas来确保并发安全
初始化链表头:cas方式
扩容方法:在put时,如果发现别的线程正在扩容,则会去帮忙扩容,在数据重新计算放入链表的时候使用了synchronize锁
8、LinkedBlockingDeque
原理
使用了双向链表的数据结构,内部使用了ReentrantLock来保证线程的安全性
9、CopyOnWriteArrayList CopyOnWriteArraySet
原理
使用了写入式拷贝的思想,增删改操作会将底层数组拷贝一份新数组,拷贝操作加了锁,更新操作会在新数组上操作。
好处:不影响别的线程并发读,别的线程读的还是旧数组
坏处:拷贝操作比较耗费时间