lock、synchronized相关
{
"title":"Lock相关",
"date":2020-05-09T16:21:59+08:00,
"draft":true,
"tags":["lock、synchronized"],
"comments":true,
"share":true
}
- lock、synchronized异同
【synchronized 和 Lock 的区别? 用Lock有什么好处?举例说明】
1.1原始构成
(1).synchronized是关键字,属于jvm层面(底层是monitorenter和monitorexit(1个monitorenter2个monitorexit,多余的哪一个是防止异常退出),通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块和同步方法中才能使用wait/notify等方法)
(2). Lock(ReentrantLock)是具体类,api层面的锁。
1.2使用方法
(1).synchronized不需要用户手动去释放锁,当synchronized代码执行完成后线程自动的释放对锁的占用
(2).Lock(ReentrantLock)是需要手动释放锁,就有可能出现死锁情况。
1.3等待是否可中断
(1).synchronized不可以中断,除非抛出异常或者运行成功
(2).ReentrantLock可以中断(更灵活),
a:设置超时方法:tryLock(long time, TimeUnit unit)
b:ReentrantLock中的lockInterruptibly()方法,底层会调用interrupt()方法可中断
1.4加锁是否公平
(1).synchronized是非公平锁
(2).ReentrantLock两种都可以,默认是非公平锁 (true是公平,false是非公平)
1.5锁是否可以绑定condition
(1).synchronized没有
(2).ReentrantLock中有condition,可以通过condition实现精准唤醒,定点通知相应的线程。
(ReentrantLock的优点:精准唤醒,定点通知)
1.6 代码验证Lock的优点
(1).synchronized版本生产者、消费者
要求:两个线程,一个线程对一个初始值为0的变量+1,另一个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。
线程操作资源类 ,判断通知干活, 防止虚假唤醒。
package com.xpf.juc;
/**
- @Author: Xia
- @Date: 2020/5/10 23:35
- @Email:x2358114512@163.com
- /
class ShareData{ // 资源类
private int num = 0;
public synchronized void prdo(){//生产者while(num != 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } num++; System.out.println(Thread.currentThread().getName()+"\t:"+num); this.notify();
}
public synchronized void cust(){//消费者while(num != 1){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } num--; System.out.println(Thread.currentThread().getName()+"\t:"+num); this.notify();
}
}
public class PrdoCustomer {
public static void main(String[] args) {ShareData shareData = new ShareData();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.prdo(); } },"AAA").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.cust(); } },"BBB").start();
}
}
synchronized版本结果:
AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0
(2).Lock版本生产者、消费者
要求:两个线程,一个线程对一个初始值为0的变量+1,另一个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。
package com.xpf.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
- @Author: Xia
- @Date: 2020/5/10 23:35
- @Email:x2358114512@163.com
- /
class ShareData{ // 资源类
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void prdo(){//生产者
lock.lock();
try{
while(num != 0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"\t:"+num);
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void cust(){//消费者
lock.lock();
try{
while(num != 1){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"\t:"+num);
condition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class PrdoCustomer {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.prdo();
}
},"AAA").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.cust();
}
},"BBB").start();
}
}
Lock结果:
AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0 AAA :1 BBB :0
(3).虚假唤醒代码验证
要求:四个线程,两个线程对一个初始值为0的变量+1,另两个线程对一个初始值为0的变量-1,实现交替5轮,最后该变量值还是0。
虚假唤醒 : 需要重新判断一次该值【也就是num的状态】,当判断的地方由while()变为if()时,if不能对变量的状态重新判断造成的,虚假唤醒。
解决方法:将代码中的if()判断改为while()。
package com.xpf.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
- @Author: Xia
- @Date: 2020/5/10 23:35
- @Email:x2358114512@163.com
- /
class ShareData{
private int num = 0;
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void prdo(){//生产者lock.lock(); try{ if(num != 0){ condition.await(); } num++; System.out.println(Thread.currentThread().getName()+"\t:"+num); condition.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
public void cust(){
lock.lock(); try{ if(num != 1){ condition.await(); } num--; System.out.println(Thread.currentThread().getName()+"\t:"+num); condition.signal(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
}
public class PrdoCustomer {
public static void main(String[] args) {ShareData shareData = new ShareData();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.prdo(); } },"AAA").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.cust(); } },"BBB").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.cust(); } },"CCC").start();
new Thread(() -> { for (int i = 0; i < 5; i++) { shareData.cust(); } },"DDD").start();
}
}
代码结果:
AAA :1
BBB :0
AAA :1
BBB :0
AAA :1
CCC :0
BBB :-1
AAA :0
AAA :1
CCC :0
BBB :-1
CCC :-2
BBB :-3
CCC :-4
DDD :-5
CCC :-6
DDD :-7
(4).Lock实现定点通知
要求:多线程之间的线程调用顺序:A->B->C,且A打印1次,B打印2次,C打印3次,来2轮。
package com.xpf.juc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
- @Author: Xia
- @Date: 2020/5/10 23:35
- @Email:x2358114512@163.com
- /
class ShareData{
private int num = 1; //A->1;B->2;C->3;
ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void print(int num, int counts){for (int i = 0; i < counts; i++) { System.out.println(Thread.currentThread().getName()+"\t:"+num); }
}
public void print1(){lock.lock(); try{ if(num != 1){ condition1.await(); } print(num,1); num = 2; condition2.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
public void print2(){lock.lock(); try{ if(num != 2){ condition2.await(); } print(num,2); num = 3; condition3.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
public void print3(){lock.lock(); try{ if(num != 3){ condition3.await(); } print(num,3); num = 1; condition1.signalAll(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
}
public class PrdoCustomer {
public static void main(String[] args) {ShareData shareData = new ShareData();
new Thread(() -> { for (int i = 0; i < 2; i++) { shareData.print1(); } },"AAA").start();
new Thread(() -> { for (int i = 0; i < 2; i++) { shareData.print2(); } },"BBB").start();
new Thread(() -> { for (int i = 0; i < 2; i++) { shareData.print3(); } },"CCC").start();
}
}
定点通知结果:
AAA :1
BBB :2
BBB :2
CCC :3
CCC :3
CCC :3
AAA :1
BBB :2
BBB :2
CCC :3
CCC :3
CCC :3
1.7留一个作业:使用阻塞队列如何实现消费者、生产者模式
- synchronized中对象锁、全局锁
使用Synchronized关键字处理有两种模式:同步代码块,同步方法 。
同步代码块:如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象。并且synchronized 同步的代码块,在同一时刻只允许一个线程进入代码块处理。
同步方法: 同一时刻内只有一个线程能进入到改方法中的。
对象锁:锁住同一个对象。
全局锁: 锁住这个类对应的Class对象 。
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
对于普通同步方法,锁是当前实例对象。
对于同步方法块,锁是Synchonized括号里配置的对象。
对于静态同步方法,锁是当前类的Class对象。
package com.xpf.juc;
/**
- @Author: Xia
- @Date: 2020/4/21 14:56
- @Email:x2358114512@163.com
- /
import java.util.concurrent.TimeUnit;
class Phone{
public static synchronized void sendEmail() throws Exception{TimeUnit.SECONDS.sleep(3); System.out.println("使用Email");
}
public synchronized void sendMS(){System.out.println("使用MS");
}
public void sayHello(){ //普通方法System.out.println("hello");
}
}
public class Lock8Demo04 {
//注意线程的调度和代码的先后没有关系,底层操作系统的调度,谁抢到了谁就先运行。
public static void main(String[] args) throws Exception {Phone phone1 = new Phone(); Phone phone2 = new Phone();
new Thread(() -> { try { phone1.sendEmail(); } catch (Exception e) { e.printStackTrace(); } },"AAA").start();
Thread.sleep(100); //让"AAA"线程先执行
new Thread(() -> { phone2.sendMS();
// phone1.sayHello();},"BBB").start();
}
}
/**
*
(1)一个对象里如果有多个synchronized方法,某一时刻内只能有一个线程去调用其中的一个synchronized方法,其他的线程只能在等待,换句话说,某一个时刻只能有一个线程去访问含有synchronized方法的对象。
synchronized方法锁的是当前对象this(其实只会锁住当前对象this中所有的synchronized方法,不包含p普通方法),被锁定之后,其他的线程都不能进入当前对象的其他synchronized方法中,但是可以进入其他的普通方法(2)静态synchronized方法,此时synchronized锁的不仅是当前对象this,而锁的是当前对象的类模板,不同锁对象不会影响。
同1部手机: Phone phone1 = new Phone();
有2部手机: Phone phone1 = new Phone();
Phone phone2 = new Phone();
1.同1部手机,标准访问,先访问sendEmail()还是sendMS()?
结果:先访问sendEmail()后是sendMS()。
注意:synchronized锁的是当前类的对象,而不是当前方法
2.同1部手机,在sendEmail()中先睡眠4秒,先访问sendEmail()还是sendMS()?
结果:先访问sendEmail()后是sendMS()。
注意:一旦进入sendEmail()后,synchronized锁的是当前类的对象,
执行完睡眠时间和相应操作后才释放锁,sendMS()才能才能获得synchronized锁
3.同1部手机,在Phone中新增普通sayHello(),sendEmail()中还是睡眠4秒,先访问sendEmail()还是普通sayHello()?
结果:先访问普通sayHello()后是sendEmail()。
注意:普通方法和synchronized锁没有关系
4.有2部手机,sendEmail()中还是睡眠4秒,先访问手机1中的sendEmail()还是手机2中的sendMS()?
结果:先访问endMS()后是sendEmail()。
注意:访问多个资源,不是同一个资源
————————————————上面4个案例中的synchronized方法不含static静态方法——————————————————
5.同1部手机,Phone中的同步方法sendEmail()和同步方法sendMS()都加了静态方法,先访问sendEmail()还是sendMS()?
结果:先访问静态同步方法sendEmail()后是静态同步方法sendMS()。和第二个锁结果一样
6.有2部手机,Phone中的sendEmail()和sendMS()变成静态方法,先访问sendEmail()还是sendMS()?
结果:先访问静态同步方法sendEmail()后是静态同步方法sendMS()。
注意:staice使得两个同步方法不仅属于该对象,更属于该类。此时synchronized锁的是类对象
7.同1部手机,Phone中的同步方法sendEmail()加了静态方法,先访问sendEmail()还是sendMS()?
结果:先访问同步方法sendMS()后是静态同步方法sendEmail()。
注意:这两把锁是两个不同的对象,同步方法sendMS()中使用的锁对象是this,静态同步方法sendEmail()中使用的锁对象是类对象Class本身,
,所以AAA线程进入静态同步方法sendEmail()后锁住类对象Class本身,而同步方法sendEmail()不需要等待其释放锁,而是自己可以直接执行
所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
8.有2部手机,Phone中的同步方法sendEmail()加了静态方法,先访问sendEmail()还是sendMS()?
结果:先访问phone2的同步方法sendMS()后是phone1的静态同步方法sendEmail()。
注意:同上
/
- 常见锁集合
3.1 公平锁与非公平锁
公平锁(fair): 是指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到。
非公平锁(unfair):是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下, 有可能造成优先级反转或者饥饿现象(某个线程可能一直都不能抢到锁)。
注意:对于synchronized而言也是一种非公平锁;并发包下的ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 (默认是非公平锁)。
3.2 可重入锁(又名递归锁)
(1).可重入锁(又名递归锁):指的是是同一线程外层函数获得锁之后,内层函数仍然能获取该锁的代码,【在同一个线程外层方法获取锁的时候,在进入内层方***自动获取锁】也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。
(2).注意:ReentrantLock/synchronized就是一个典型的可重入锁(下面代码验证);可重入锁最大的作用就是避免死锁。
(3).代码验证synchronized是可重入锁
package com.xpf.Interview.juc.lock;
/**
- @Author: Xia
- @Date: 2020/5/12 10:10
- @Email:x2358114512@163.com
- /
class ShareData{
public synchronized void write(){System.out.println(Thread.currentThread().getName()+"\t write方法"); read();
}
private synchronized void read() {System.out.println(Thread.currentThread().getName()+"\t read方法");
}
}
public class LockDemo {
public static void main(String[] args) {ShareData shareData = new ShareData(); new Thread(() -> { shareData.write(); },"AAA").start();
}
}
结果:
AAA write方法
AAA read方法
(4)代码验证ReentrantLock是可重入锁
package com.xpf.Interview.juc.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
- @Author: Xia
- @Date: 2020/5/12 10:10
- @Email:x2358114512@163.com
- /
class ShareData{
ReentrantLock lock = new ReentrantLock();
public void write(){lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t write方法"); read(); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
private void read() {lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t read方法"); }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); }
}
}
public class LockDemo {
public static void main(String[] args) {ShareData shareData = new ShareData(); new Thread(() -> { shareData.write(); },"AAA").start();
}
}
结果:
AAA write方法
AAA read方法
注意:当write()中出现多对lock.lock() 和 lock.unlock()方法时,只要对应匹配(两两配对)则会正确输出,当lock.lock()比lock.unlock()多一个时,第二个线程执行是会卡死,一直等待前一个线程释放lock.unlock()。
3.3 自旋锁(spinlock)
(1).自旋锁(spinlock):其实就是用循环去代替阻塞。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU(下面代码验证)。
(2).代码验证自旋锁的原理
package com.xpf.Interview.juc.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
- @Author: Xia
- @Date: 2020/5/12 10:10
- @Email:x2358114512@163.com
- /
class ShareData{
private AtomicReference<thread> atomicReference = new AtomicReference<>();
public void myLock(){Thread thread = Thread.currentThread(); System.out.println(thread.getName()+"\t进入myLock方法"); while(!atomicReference.compareAndSet(null,thread)){ System.out.println(thread.getName()+"循环获取锁"); }
}
public void myUnLock(){Thread thread = Thread.currentThread(); System.out.println(thread.getName()+"\t进入myUnLock方法"); atomicReference.compareAndSet(thread,null);
}
}
public class LockDemo {
public static void main(String[] args) {ShareData shareData = new ShareData(); new Thread(() -> { shareData.myLock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //"AAA"线程睡眠之后,"BBB"循环获取锁 shareData.myUnLock(); },"AAA").start();
try { TimeUnit.MICROSECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 让"AAA"线程先执行
new Thread(() -> { shareData.myLock(); shareData.myUnLock(); },"BBB").start();
}
}
结果:</thread>
AAA 进入myLock方法
BBB 进入myLock方法
BBB循环获取锁
BBB循环获取锁
BBB循环获取锁
..........
BBB循环获取锁
BBB循环获取锁
AAA 进入myUnLock方法
BBB循环获取锁
BBB 进入myUnLock方法
3.4 读/写锁
(1).独占锁(写锁):指的是该锁只能被一个线程所持有, ReentrantLock和synchronized都是独占锁。
(2).共享锁(读锁):指的是该锁可以被多个线程所持有,ReentrantReadWriterLock其读锁是共享锁,其写锁是独占锁。(下面代码验证)
注意:读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
(3).代码验证ReentrantReadWriterLock的读写锁
package com.xpf.Interview.juc.lock;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
- @Author: Xia
- @Date: 2020/5/12 10:10
- @Email:x2358114512@163.com
- /
class Cache{ // 缓存的读写分离
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
HashMap<String,String> hashMap = new HashMap<>();
public void read(String key){ //读锁(共享锁)readWriteLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在读取:"+key); TimeUnit.MICROSECONDS.sleep(100); String value = hashMap.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成"); }catch (Exception e){ e.printStackTrace(); }finally { readWriteLock.readLock().unlock(); }
}
public void write(String key, String value){ //写锁(独享锁)readWriteLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key); TimeUnit.MICROSECONDS.sleep(100); hashMap.put(key,value); System.out.println(Thread.currentThread().getName()+"\t 写入完成"); }catch (Exception e){ e.printStackTrace(); }finally { readWriteLock.writeLock().unlock(); }
}
}
public class LockDemo {
public static void main(String[] args) {Cache cache = new Cache();
for (int i = 0; i < 3; i++) { final int j = i; new Thread(() -> { cache.write(j+"",j+""); },String.valueOf(i)).start(); }
for (int i = 0; i < 3; i++) {
final int k = i; new Thread(() -> { cache.read(k+""); },String.valueOf(i)).start(); }
}
}
结果:
0 正在写入:0
0 写入完成
1 正在写入:1
1 写入完成
2 正在写入:2
2 写入完成
0 正在读取:0
2 正在读取:2
1 正在读取:1
1 读取完成
2 读取完成
0 读取完成
结果分析:
写操作是原子+独占的过程,整个过程是一个完整的整体,中间没有被分割,没有被打断。
例如:0线程写入0,紧接着执行的是0线程写入完成。
读操作是共享的过程,可保证并发读是非常高效。
例如:0线程正在读取,紧接着执行的2、1线程正在读取,最后才执行的是0线程读取完成
3.5 死锁相关
死锁相关
1.什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去。如果系统资源充足,线程的资源请求都能够得到满足,死锁出现的可能性比较低,否则就会因争夺有限资源而陷入死锁。
2.产生死锁的主要原因?
2.1 系统资源不足
2.2 进程运行推进的顺序不合适
2.3 资源分配不当
3.死锁代码演示。
4.死锁的解决方案?
4.1 jps命令定位进程编号
4.2 jstack找到死锁查看
args) {
String lockA = "lockA";
String lockB = "lockB";
package com.xpf.Interview.juc.ThreadPool;
import java.util.concurrent.TimeUnit;
/**
- @Author: Xia
- @Date: 2020/4/25 20:55
- @Email:x2358114512@163.com
- /
class HoldThread implements Runnable {
private String lockA;
private String lockB;
public HoldThread(String lockA, String lockB) {this.lockA = lockA; this.lockB = lockB;
}
@Override
public void run() {synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "\t 自己持有锁:" + lockA + ",尝试获得:" + lockB);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "\t 自己持有锁:" + lockB + ",尝试获得:" + lockA); } }
}
}
public class DeadLockDemo24 {
public static void main(String[] args) {String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldThread(lockA, lockB), "AAA").start(); new Thread(new HoldThread(lockB, lockA), "BBB").start();
}
}
select a language
运行结果:
BBB 自己持有锁:lockB,尝试获得:lockA
AAA 自己持有锁:lockA,尝试获得:lockB
卡死不能结束程序。