写锁和写锁不能共存, 两个线程都要写一个数据,存在线程安全问题 读锁和写锁不能以共存, 一个要写数据一个要读数据,存在线程安全问题 读锁和读锁可以共存 ,两个线程要读一个数据,不存在线程安全问题,直接并发读就好了。
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
执行流程:address里面的值和expectValue比较,如果相等,九江要设置的新值跟内存里面的值更新,如果不相等,就进行下一次比较
针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
基于 CAS 实现更灵活的锁, 获取到更多的控制权
自旋锁伪代码
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销) 如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态. 偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁
举个栗子理解偏向锁 假设男主是一个锁, 女主是一个线程. 如果只有这一个线程来使用这个锁, 那么男主女主即使不领证 结婚(避免了高成本操作), 也可以一直幸福的生活下去. 但是女配出现了, 也尝试竞争男主, 此时不管领证结婚这个操作成本多高, 女主也势必要把这个动作 完成了, 让女配死心
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁). 此处的轻量级锁就是通过 CAS 来实现 – 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用) – 如果更新成功, 则认为加锁成功 – 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源. 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 “自适应”
执行加锁操作, 先进入内核态. 在内核态判定当前锁是否已经被占用 如果该锁没有占用, 则加锁成功, 并切换回用户态. 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒. 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁
这里补充一点:在jdk17取消了偏向锁,在jdk8依旧保留着偏向锁
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除
程序员写代码时候,JVM是管不了什么时候加锁和不加锁的。但是当代码编译之后运行的时候JVM就知道synchronizied的代码是对变量的读还是些,还知道当前是单线程还是多线程, 如果所有加synchronizied的代码块,并没有对变量进行写操作,或者是单线程执行,那么synchronizied对应的锁就不会生效(不会编译成LOCK) 这个发生情况,只有当JVM100%确定的时候才会执行锁消除操作,并不一定所有的代码都会发生
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
举个栗子理解锁粗化
方式一: 打电话, 交代任务1, 挂电话. 打电话, 交代任务2, 挂电话. 打电话, 交代任务3, 挂电话. 方式二: 打电话, 交代任务1, 任务2, 任务3, 挂电话. 显然, 方式二是更高效的方案
Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本 创建一个类 Result , 包含一个 sum 表示最终结果, lock 表示线程同步使用的锁对象. main 方法中先创建 Result 实例, 然后创建一个线程 t. 在线程内部计算 1 + 2 + 3 + … + 1000. 主线程同时使用 wait 等待线程 t 计算结束. (注意, 如果执行到 wait 之前, 线程 t 已经计算完了, 就不必等待了). 当线程 t 计算完毕后, 通过 notify 唤醒主线程, 主线程再打印结果
public class Test {
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
//创建一个线程进行累加操作
Thread t1=new Thread(()->{
int sum=0;
for(int i=0;i<1000;i++) {
//执行累加
sum+=i;
}
//为结果赋值
result.sum=sum;
//唤醒其他线程
synchronized (result.lock){
result.lock.notifyAll();
}
});
//启动线程
t1.start();
//t1.join();也可以等待t1执行完毕
synchronized(result.lock) {
//检查累加是否完成
while(result.sum==0) {
//没有完成,等待结果
result.lock.wait();
}
}
//打印结果
System.out.println(result.sum);
}
}
class Result {
//累加和
public int sum;
//锁对象
public Object lock=new Object();
}
代码示例: 创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本
创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型. 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果. 把 callable 实例使用 FutureTask 包装一下. 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中. 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果 定义Callable的三种方式:
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for (int i = 0; i <1000 ; i++) {
sum+=i;
}
return sum;
}
};
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t1=new Thread(futureTask);
t1.start();
int result=futureTask.get();
System.out.println(result);
}
可以看到, 使用 Callable 和 FutureTask 之后, 代码简化了很多, 也不必手动写线程同步代码了
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务, Runnable 描述的是不带返回值的任务. Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. FutureTask 就可以负责这个等待结果出来的工作
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你一张 “小票” . 这个小票就是 FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 "可重入“
public static void main(String[] args) throws InterruptedException {
//创建一个ReentrantLock对象
ReentrantLock lock=new ReentrantLock();
//加锁
lock.lock();
//解锁
lock.unlock();
//尝试加锁,如果加锁成功,返回true,否则返回false
lock.tryLock();
//尝试加锁,如果加锁成功,并且可以指定等待时间
lock.tryLock(1, TimeUnit.SECONDS);
}
但是这种情况有个问题,如果加锁后执行异常,就一种无法解锁,所以改进代码。
public static void main(String[] args) throws InterruptedException {
//创建一个ReentrantLock对象
ReentrantLock lock=new ReentrantLock();
//加锁
try {
lock.lock();
System.out.println("业务代码");
TimeUnit.SECONDS.sleep(10);
throw new Exception("业务异常");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
System.out.println("释放锁");
}
}
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//实现
ReentrantLock lock=new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
//定义一把锁
ReentrantLock lock=new ReentrantLock();
//定义很多个唤醒条件和休眠条件
//条件1
Condition male=lock.newCondition();
//条件2
Condition female=lock.newCondition();
lock.lock();
new Thread(() -> {
while (true) {
lock.lock();
male.signal();
female.signal();
lock.unlock();
}
}).start();
lock.lock();
male.await();
lock.unlock();
System.out.println("男生来了");
System.out.println("唤醒男生");
female.await();
System.out.println("女生来了");
//唤醒条件1
female.signal();
System.out.println("唤醒女生");
}
public static void main(String[] args) {
//创建一个读写锁
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
//创建一个读锁
ReentrantReadWriteLock.ReadLock readLock=new ReentrantReadWriteLock().readLock();
//创建一个写锁
ReentrantReadWriteLock.WriteLock writeLock=new ReentrantReadWriteLock().writeLock();
//读锁加锁,共享锁,多个读锁可以共存
readLock.lock();
//写锁加锁,独占锁,一个写锁只能有一个
writeLock.lock();
}
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个 AtomicBoolean AtomicInteger AtomicIntegerArray AtomicLong AtomicReference AtomicStampedReference 以 AtomicInteger 举例,常见方法有 addAndGet(int delta); i += delta; decrementAndGet(); --i; getAndDecrement(); i–; incrementAndGet(); ++i; getAndIncrement(); i++;
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
理解信号量 可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源. 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作) 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作) 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用
代码示例
创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源. acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作) 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果
public class Test {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);
for (int i = 0; i <20 ; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"申请资源");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"获得资源");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
semaphore.release();
System.out.println(Thread.currentThread().getName()+"释放资源");
}, "线程" + i).start();
}
}
}
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成. 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减. 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
public static void main(String[] args) throws InterruptedException{
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+"开跑");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"到达终点");
countDownLatch.countDown();
},"运动员"+i+"号").start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("比赛进行中...");
countDownLatch.await();
System.out.println("比赛结束颁奖");
}
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(() -> {
synchronized(lock1) {
System.out.println("获得锁1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(lock2) {
System.out.println("获得锁2");
}
System.out.println("释放锁2");
}
System.out.println("释放锁1");
}).start();
new Thread(() -> {
synchronized(lock2) {
System.out.println("获得锁2");
synchronized(lock1) {
System.out.println("获得锁1");
}
System.out.println("释放锁1");
}
System.out.println("释放锁2");
}).start();
}
上面这段代码就产生了死锁情况。 我们从3和4的角度去修改一下获取锁的策略,修改代码如下:
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
new Thread(() -> {
synchronized(lock2) {
System.out.println("获得锁2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized(lock1) {
System.out.println("获得锁1");
}
System.out.println("释放锁2");
}
System.out.println("释放锁1");
}).start();
new Thread(() -> {
synchronized(lock2) {
System.out.println("获得锁2");
synchronized(lock1) {
System.out.println("获得锁1");
}
System.out.println("释放锁1");
}
System.out.println("释放锁2");
}).start();
}
}
原来的集合类, 大部分都不是线程安全的
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++){
int finalI = i;
new Thread(() -> {
list.add(finalI);
System.out.println(list);
}).start();
}
}
public static void main(String[] args) throws InterruptedException {
ArrayList<Integer> list = new ArrayList<>();
Object lock = new Object();
for (int i = 0; i < 10; i++){
TimeUnit.MILLISECONDS.sleep(1);
synchronized (lock) {
int finalI = i;
new Thread(() -> {
list.add(finalI);
System.out.println(list);
}).start();
}
}
}
public static void main(String[] args) {
List<Integer> arraylist = new ArrayList<>();
List<Integer> list = Collections.synchronizedList (arraylist);
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(() -> {
list.add(finalI);
System.out.println(list);
}).start();
}
}
CopyOnWrite容器即写时复制的容器。 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy, 复制出一个新的容器,然后新的容器里添加元素, 添加完元素之后,再将原容器的引用指向新的容器。 这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会 添加任何元素。 所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 优点: 在读多写少的场景下, 性能很高, 不需要加锁竞争. 缺点: 占用内存较多. 新写的数据不能被第一时间读取到.
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(() -> {
list.add(finalI);
System.out.println(list);
}).start();
}
}
1.ArrayBlockingQueue 基于数组实现的阻塞队列 2.LinkedBlockingQueue 基于链表实现的阻塞队列 3.PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列 4.TransferQueue 最多只包含一个元素的阻塞队列
4.HashTable相当于把整个HashTable锁住了,而真正操作的只是一个hash是桶
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率. 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况. 优化了扩容方式: 化整为零发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去. 扩容期间, 新老数组同时存在. 后续每个来操作,ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素. 搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加. 这个期间, 查找需要同时查新数组和老数组
ConcurrentHashTable每个桶都有一把锁,只有两个线程同时访问同一个桶时候才会发生锁冲突