锁是什么
synchronize
- vt :使同步;使同时发生.
- vi :与…同时发生;与…一起发生,
多线程共享一个内存资源,每个线程都从主内存copy资源到自己的工作内存中,大家都各改各的资源,再写入到主内存中,线程并发执行下,就可能会导致主内存数据失帧,产生的数据与我们预期的不一致。synchronize就是使线程的工作内存与主内存的数据进行同步。
公平锁、非公平锁
就是线程是否按时间有序的获取锁,还是无序的获取锁。Object的notify()和notifyAll()方法都是唤醒正在阻塞获取锁的线程,前者随机唤醒一个线程获取锁,后者唤醒全部线程去争抢锁。都是随机的获取锁,可知synchronized同步是非公平锁。
公平锁的实现靠Lock对象,我们再他的构造方法中可以设置是否是公平锁。
public ReentrantLock(boolean var1) {
this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
重入锁(递归锁)
线程可以进入任何一个它己经拥有的锁所同步着的代码块,递归比较好理解,就是最外层获取到锁,那么子方法的锁都一并获取到,下面是代码演示。其实锁都是重入锁,这个概念理解一下即可,如果不是则非常容易照成死锁。
public class Test {
public static void main(String[] args) throws Exception {
Test test = new Test();
new Thread(() -> {
test.test1();
}, "t1").start();
new Thread(() -> {
test.test2();
}, "t2").start();
new Thread(() -> {
test.test3();
}, "t3").start();
}
private synchronized void test1() {
System.out.println(Thread.currentThread().getName() + " sync. invoke test1()");
test2();
}
private synchronized void test2() {
System.out.println(Thread.currentThread().getName() + " sync. invoke test2()");
test3();
}
private synchronized void test3() {
System.out.println(Thread.currentThread().getName() + " sync. invoke test3()");
}
}
自旋锁
自:线程自己
旋:循环
锁:同步
相对于互斥锁,如果资源已经被占用,资源申请者只能进入阻塞状态。但是自旋锁不会引起调用者阻塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。自旋锁的效率远高于互斥锁。我们可以拿一个JUC中原子引用包的AtomicInteger源码来演示。(这个没有加synchronized、lock,主要靠判断当前主内存的值是否符合我们的预期值,如果是则修改,这个同步是靠我们的预期值来维护的)
public final int getAndUpdate(IntUnaryOperator var1) {
int var2;
int var3;
do {
var2 = this.get();
var3 = var1.applyAsInt(var2);
} while(!this.compareAndSet(var2, var3));
return var2;
}
读写锁
读写锁把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。它允许同时有多个读者来访问共享资源,能提高并发性。而写者是排他性的。
只要理解:读读可以共享锁(多线程并发读取),读写不能共存(多线程阻塞,单线程运行),写写不能共存(多线程阻塞,单线程运行)。
代码演示:并发执行,写不能共存,只能顺序执行。完毕后,读取时可以并发读取。
public class Test {
public static void main(String[] args) throws Exception {
Cache cache = new Cache();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
cache.put(finalI, Thread.currentThread().getName());
}, "put" + String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
cache.get(finalI);
}, "get" + String.valueOf(i)).start();
}
}
}
//资源类
class Cache {
//共享数据
private volatile Map<Integer, String> map = new HashMap<>();
//读写锁
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
void put(Integer key, String value) {
//写锁定
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入 = " + key);
//模拟 暂停一会
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//写数据
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成 = ");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
void get(Integer key) {
//读锁
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取 = ");
//模拟 暂停一会
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//读数据
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成 = " + o);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
死锁
多线程的编程过程中,我们首先要保证线程安全性,对共享变量进行加锁。但是加太多锁,并发性又加降低。为了保证并发性能,所以加锁的同时也要注意锁的粒度,尽可能让锁的粒度变细(就像synchronized尽量不要放到方法上,要放到具体的同步代码块上)。但是锁的粒度变细,就会碰到死锁的问题。
代码演示:单线程请求下没有问题,但是多线程情况下就会又线程安全问题(假设from同时向多个帐号转账,可能会出现余额少减的情况,因为amount并没有同步,多线程都可以读写)。所以我们要加锁,保证from不会少减,to不会少增。
public class Test {
//模拟 转账
void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
//转出 扣减
from.setAmount(from.getAmount() - amount);
//转入 增加
to.setAmount(to.getAmount() + amount);
}
}
}
}
class Account {
void setAmount(int amount) {
}
int getAmount() {
return 0;
}
}
进一步分析:线程在运行方法时,是可以在任何地方进行线程切换的,甚至是在一条语句中间。如果设想用户AB同时相互转账,恰好两个线程都获取到的第一行的锁,然后又在等待获取第二行的锁。这样子就发生死锁了。
总结:死锁的条件
- 互斥等待
- hold and wait 拿到锁 等待另外的锁
- 循环等待
- 无法剥夺的等待 没有超时时间
满足上面4条就会发生死锁,我们只要破除其中一个就可以避免死锁。
- 破除互斥等待,一般无法破除,要保证同步。
- 破除hold and wait,只要一次性获取所有锁资源,设置一个全局锁。
- 破除循环等待,按顺序获取资源,可以用主键排序获取锁。
- 破除无法剥夺的等待,设置超时时间。一般不推荐,耗时浪费资源。
上述几种破除都可以,但是都有各自的利弊,开发就是解决一个问题又会引出另外一个问题,再去解决,螺旋向上。
万一发生了死锁,我们在项目中又怎么去排查呢?我们先写个正真的死锁代码。
public class DeadLockDemo {
public static void main(String[] args) {
//俩锁 让其相互等待
String lockA = "A";
String lockB = "B";
new Thread(() -> {
//获取A锁
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t 拿到lockA锁,等待lockB锁释放");
//等待其他线程获取B锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再去获取B锁
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 计算");
}
System.out.println(Thread.currentThread().getName() + "\t 完成");
}
}, "AAA").start();
new Thread(() -> {
//获取B锁
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 拿到lockB锁,等待lockA锁释放");
//等待其他线程获取A锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取A锁
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t 计算");
}
System.out.println(Thread.currentThread().getName() + "\t 完成");
}
}, "BBB").start();
}
}
执行结果
AAA 拿到lockA锁,等待lockB锁释放
BBB 拿到lockB锁,等待lockA锁释放
分析死锁:
jps -l #查看java进程信息,查询PID
jstack PID #查看具体的堆栈信息
下面是打印出来的,结论发现一个死锁()
Java stack information for the threads listed above:
===================================================
"BBB":
at 死锁.DeadLockDemo.lambda$main$1(DeadLockDemo.java:45)
- waiting to lock <0x00000000d5ce8bd0> (a java.lang.String)
- locked <0x00000000d5ce8c00> (a java.lang.String)
at 死锁.DeadLockDemo$$Lambda$2/1769597131.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"AAA":
at 死锁.DeadLockDemo.lambda$main$0(DeadLockDemo.java:30)
- waiting to lock <0x00000000d5ce8c00> (a java.lang.String)
- locked <0x00000000d5ce8bd0> (a java.lang.String)
at 死锁.DeadLockDemo$$Lambda$1/97730845.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
也可以用jconsole.exe,选择进程,点击线程,检测死锁。