谈谈 Java 中的那些“琐”事
一、公平锁&非公平锁
是什么
公平锁:线程按照申请锁的顺序来获取锁;在并发环境中,每个线程都会被加到等待队列中,按照 FIFO 的顺序获取锁。

非公平锁:线程不按照申请锁的顺序来获取锁;一上来就尝试占有锁,如果占有失败,则按照公平锁的方式等待。

通俗来讲,公平锁就相当于现实中的排队,先来后到;非公平锁就是无秩序,谁抢到是谁的;
优缺点
公平锁
- 优:线程按照顺序获取锁,不会出现饿死现象(注:饿死现象是指一个线程的CPU执行时间都被其他线程占用,导致得不到CPU执行)。
- 缺:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒线程的开销比非公平锁要大。
非公平锁
- 优:可以减少唤起线程上下文切换的消耗,整体吞吐量比公平锁高。
- 缺:在高并发环境下可能造成线程优先级反转和饿死现象。
Java中的公平&非公平锁
在 Java 中,synchronized 是典型的非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁,可以在初始化的时候指定。
查看 ReentrantLock 的源码会发现,初始化时可以传入 true 或 false,来得到公平或非公平锁。
//源码
//默认为非公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public class FairLockDemo {
public static void main(String[] args) {
//公平锁
Lock fairLock = new ReentrantLock(true);
//非公平锁
Lock unFairLock = new ReentrantLock(false);
}
}
二、可重入锁
是什么
可重入锁也叫递归锁,是指线程可以进入任何一个它已经拥有的锁所同步的代码块。
通俗来讲,就好比你打开了你家的大门,就可以随意的进入客厅、厨房、卫生间......
优缺点
- 优:可以一定程度上避免死锁
- 缺:暂时不知道
Java中的可重入锁
synchronized和ReentrantLock都是典型的可重入锁
synchronized
public class ReentrantDemo1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendSMS();
}).start();
new Thread(() -> {
phone.sendSMS();
}).start();
}
}
class Phone {
public synchronized void sendSMS() {
System.out.println(Thread.currentThread().getId() + ":sendSMS()");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sendEmail();
}
public synchronized void sendEmail() {
System.out.println(Thread.currentThread().getId() + ":sendEmail()");
}
}
ReentrantLock
public class ReentrantDemo2 {
public static void main(String[] args) {
User user = new User();
new Thread(() -> {
user.getName();
}).start();
new Thread(() -> {
user.getName();
}).start();
}
}
class User {
Lock lock = new ReentrantLock();
public void getName() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + ":getName()");
TimeUnit.SECONDS.sleep(1);
getAge();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void getAge() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + ":getAge()");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
八锁问题
点击查看我之前的博客 多线程之8锁问题,搞懂八锁问题,可以更深刻的理解 synchronized 锁的范围
实现一个不可重入锁
public class UnReentrantLockDemo {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
//自旋
while(!atomicReference.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
atomicReference.compareAndSet(current, null);
}
}
三、自旋锁
是什么
尝试获取锁的线程不会立即阻塞,而是以循环的方式不断尝试获取锁
优缺点
- 优:减少线程上下文切换的消耗
- 缺:循环消耗CPU
Java中的自旋锁
CAS:CompareAndSwap,比较并交换,它是一种乐观锁。
CAS 中有三个参数:内存值V、旧的预期值A、要修改的新值B;只有当预期值A与内存值V相等时,才会将内存值V修改为新值B,否则什么都不做
public class CASTest {
public static void main(String[] args) {
AtomicInteger a1 = new AtomicInteger(1);
//V=1, A=1, B=2
//V=A,所以修改成功,此时V=2
System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
//V=2, A=1, B=2
//V!=A,修改失败,返回false
System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
}
}
源码解析:以 AtomicInteger 中的 getAndIncrement() 方法为例
//获取并增加,相当于i++操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//调用UnSafe类中的getAndAddInt()方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//获取当前内存值
var5 = this.getIntVolatile(var1, var2);
//循环比较内存值和预期值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS 也存在一些问题:
- 如果一直交换不成功,会一直循环,开销大
- 只能保证一个共享变量的原子操作
- ABA 问题:即 A 被修改为 B,又被改为 A,虽然值没发生变化,但这种操作还是存在一定风险的
可以通过加时间戳或版本号的方式解决 ABA 问题:
public class ABATest {
public static void main(String[] args) {
showABA();
}
/**
* 重现ABA问题
*/
private static void showABA() {
AtomicReference<String> atomicReference = new AtomicReference<>("A");
//线程X,模拟ABA问题
new Thread(() -> {
atomicReference.compareAndSet("A", "B");
atomicReference.compareAndSet("B", "A");
}, "线程X").start();
//线程Y睡眠一会儿,等待X执行完
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet("A", "C");
System.out.println("最终结果:" + atomicReference.get());
}, "线程Y").start();
}
/**
* 解决ABA问题
*/
private static void solveABA() {
//初始版本号为1
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);
new Thread(() -> {
asr.compareAndSet("A", "B", 1, 2);
asr.compareAndSet("B", "A", 2, 3);
}, "线程X").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet("A", "C", 1, 2);
System.out.println(asr.getReference() + ":" + asr.getStamp());
}, "线程Y").start();
}
}
动手实现一个自旋锁
public class SpinLockDemo {
/**
* 初始值为 null
*/
AtomicReference<Thread> atomicReference = new AtomicReference<>(null);
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "线程A").start();
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "线程B").start();
}
public void lock() {
//获取当前线程对象
Thread thread = Thread.currentThread();
do {
System.out.println(thread.getName() + "尝试获取锁...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当赋值成功才会跳出循环
} while (!atomicReference.compareAndSet(null, thread));
}
public void unLock() {
//获取当前线程对象
Thread thread = Thread.currentThread();
//置为null,相当于释放锁
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "释放锁...");
}
}
四、共享锁&独占锁
是什么
- 共享锁:也可称为读锁,可被多个线程持有
- 独占锁:也可称为写锁,只能被一个线程持有,synchronized和ReentrantLock都是独占锁
- 互斥:读读共享、读写互斥、写写互斥
优缺点
读写分离,适用于大量读、少量写的场景,效率高
java中的共享锁&独占锁
ReentrantReadWriteLock 中的读锁是共享锁、写锁是独占锁
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 写锁控制写入
*/
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始写入...");
//睡一会儿
TimeUnit.SECONDS.sleep(1);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入完成...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
/**
* 读锁控制读取
*/
public Object get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "开始读取...");
//睡一会儿
TimeUnit.SECONDS.sleep(1);
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取结束...value=" + value);
return value;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return null;
}
public void clear() {
map.clear();
}
}
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
cache.put(String.valueOf(finalI), String.valueOf(finalI));
cache.get(String.valueOf(finalI));
}, "线程" + i).start();
}
cache.clear();
}
}
谈谈 Java 中的那些“琐”事的更多相关文章
- 谈谈JAVA中的安全发布
谈谈JAVA中的安全发布 昨天看到一篇文章阐述技术类资料的"等级",看完之后很有共鸣.再加上最近在工作中越发觉得线程安全性的重要性和难以捉摸,又掏出了<Java并发编程实战& ...
- 谈谈java中静态变量与静态方法在有继承关系的两个类中调用
谈谈java中静态变量与静态方法在有继承关系的两个类中调用 学习的中如果遇到不明白或者不清楚的的时候,就是自己做些测试,自己去试试,这次我就做一个关于静态变量和静态方法在有继承关系的两个类中的问题测试 ...
- 谈谈java中成员变量与成员方法继承的问题
谈谈java中成员变量与成员方法继承的问题 关于成员变量和成员方法的的继承问题,我也可以做一个小测试,来看看结果. 首先我们先创建一个父类:
- 谈谈java中的WeakReference
Java语言中为对象的引用分为了四个级别,分别为 强引用 .软引用.弱引用.虚引用. 本文只针对java中的弱引用进行一些分析,如有出入还请多指正. 在分析弱引用之前,先阐述一个概念:什么是对象可到达 ...
- 谈谈java中遍历Map的几种方法
java中的map遍历有多种方法,从最早的Iterator,到java5支持的foreach,再到java8 Lambda,让我们一起来看下具体的用法以及各自的优缺点 先初始化一个map public ...
- java基础(五):谈谈java中的多线程
1.多线程 1.1.多线程介绍 学习多线程之前,我们先要了解几个关于多线程有关的概念. 进程:正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一 ...
- java基础(四):谈谈java中的IO流
1.字节流 1.1.字节输出流output 1.1.1.数据写入文件中 通过api查找output.找到很多,其中java.io.OutputStream,OutputStream: 输出字节流的超类 ...
- 谈谈Java中的代理模式
首先来看一下代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问.在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用, 其特征是代理类与 ...
- 浅拷贝和深拷贝(谈谈java中的clone)
clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那么在java语言中,有 ...
随机推荐
- 对于CSS里面我之前不太清楚的伪类,我做一些总结
格式: 标签 + : + 参数 +{ 可填背景颜色,字体颜色,鼠标样式,加粗等 } a:hover{ color:#f40;} :link表示鼠标点击之前的样式 :hover表示鼠标放上去的样式 :a ...
- Python脚本实现在cmd执行相关命令
通过Python脚本实现,在cmd命令执行文件的cp(复制).rm(删除).rename(重命名).move(文件移动).mkdir(创建目录) cmd执行命令格式:python xxx.py 用户名 ...
- Java多线程_Master-Worker设计模式
Master-Worker模式是常用的并行模式之一,它的核心思想是:系统由Master进程和Worker进程两类进程协同工作,Master负责接收和分配任务,Wroker负责处理子任务.当各个Work ...
- Java枚举简述
Java 枚举(enum) Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等. Java 枚举类使用 enum 关键 ...
- idea中展开折叠的文件夹
1. 按键盘中的向右箭头 选中要展开的目录,按下键盘的向右箭头,这时候会展开一层当前目录.如果要展开所有,一直按住向右箭头不放. 左箭头则是折叠 2. 按数字键盘中的 * 选中要展开的目录,按下数字键 ...
- vs使用fscanf和fprintf错误警告处理
严重性代码说明项目文件行 禁止显示状态错误 C4996 fopen('fscanf'.strcmp):This function or variable may be unsafe. 最全解决办法(转 ...
- vue组件获取和vue-cli的基本了解
Vue获取组件的一些方法 this.$refs.xxx 给标签绑定ref属性,获取的是当前DOM对象 给组件绑定ref属性,获取的是组件实例对象 this.$parent 获取当前组件的父组件,为一个 ...
- 基于Log4Net记录日志到SQLServer(自定义字段)
本文记录通过log4net将日志信息记录到SQLServer数据库中. 1.新建控制台应用程序 Log4NetDemo: 2.通过NuGet安装Log4Net (项目版本2.0.8): 3.项目根目录 ...
- Java 的八种排序算法
Java 的八种排序算法 这个世界,需要遗忘的太多. 背景:工作三年,算法一问三不知. 一.八种排序算法 直接插入排序.希尔排序.简单选择排序.堆排序.冒泡排序.快速排序.归并排序和基数排序. 二.算 ...
- LR监听Linux
1.准备 1)CentOS6.0 2)LR11.0 3)Linux安装包: xinetd-2.3.14-33.el6.i686.rpm (CentOS安装盘自带) rsh-server-0.17-60 ...