悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
悲观锁的实现方式
适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
//=============悲观锁的调用方式
public synchronized void m1(){//加锁后的业务逻辑......
}// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {lock.lock();try {// 操作同步资源}finally {lock.unlock();}
}
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
乐观锁的实现方式
1.版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
2.最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升。
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
1.标准访问有ab两个线程,请问先打印邮件还是短信?邮件
class Phone {public synchronized void sendEmail() {System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendEmail
//-------------sendSMS
2.访问ab两个线程,a里面故意停3秒?邮件
class Phone {public synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendEmail
//-------------sendSMS
题目1和2说明
一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法
3.添加一个普通的hello方法,请问先打印邮件还是hello?hello
class Phone {public synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}public void hello(){System.out.println("-------hello");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.hello();}, "b").start();}
}
//输出结果
//-------hello
//-------------sendEmail
题目3说明
hello并未和其他synchronized修饰的方法产生争抢
4.有两部手机,请问先打印邮件(这里有个3秒延迟)还是短信?短信
class Phone {public synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendSMS
//-------------sendEmail
题目4说明
邮件和短信方法虽然都添加了synchronized修饰,但两个方法都是普通方法。因此锁的是对象,当调用邮件和短信使用不同对象去调用时,上锁的对象是不同的,因此这里sendSMS会优先执行再去执行sendEmail。
5.有两个静态同步方法(synchroized前加static,3秒延迟也在),有1部手机,先打印邮件还是短信?邮件
class Phone {public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public static synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendEmail
//-------------sendSMS
6.两个手机,有两个静态同步方法(synchroized前加static,3秒延迟也在),先打印邮件还是短信?邮件
class Phone {public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public static synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendEmail
//-------------sendSMS
题目5和6说明
5和6中,sendEmail和sendSMS都是静态方法且都被synchronized所修饰,因此两个方法是锁的是当前类的Class对象。这里虽然调用sendEmail和sendSMS方法是用的不同的对象,当都是同一把锁,因此会先执行sendEmail再去执行sendSMS。
补充说明
对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁→实例对象本身。
对于静态同步方法,锁的是当前类的Class对象,如Phone,class唯一的一个模板。
对于同步方法块,锁的是synchronized括号内的对象。synchronized(o)
7.一个静态同步方法,一个普通同步方法,有一部手机,请问先打印邮件还是短信?短信
class Phone {public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendSMS
//-------------sendEmail
8.两个手机,一个静态同步方法,一个普通同步方法,请问先打印邮件还是手机?短信
class Phone {public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("-------------sendEmail");}public synchronized void sendSMS() {System.out.println("-------------sendSMS");}
}public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
}
//输出结果
//-------------sendSMS
//-------------sendEmail
题目7和8说明
如上sendEmail方法是类锁,而sendSMS是对象锁。两个方法不是同一把锁不存在锁的争抢,因此会先执行sendSMS再去sendEmail。
javap -c ***.class
文件反编译,-c表示对代码进行反汇编javap -v ***.class
,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)/*** 锁同步代码块*/
public class LockSyncDemo {Object object = new Object();public void m1(){synchronized (object){System.out.println("-----hello synchronized code block");}}public static void main(String[] args) {}
}
javap -c LockSyncDemo.class
public class com.llp.juc.lock.LockSyncDemo {java.lang.Object object;public com.llp.juc.lock.LockSyncDemo();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."":()V4: aload_05: new #2 // class java/lang/Object8: dup9: invokespecial #1 // Method java/lang/Object."":()V12: putfield #3 // Field object:Ljava/lang/Object;15: returnpublic void m1();Code:0: aload_01: getfield #3 // Field object:Ljava/lang/Object;4: dup5: astore_16: monitorenter //monitorenter进入锁7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;10: ldc #5 // String -----hello synchronized code block12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V15: aload_116: monitorexit //monitorexit进入锁(正常退出)17: goto 2520: astore_221: aload_122: monitorexit //monitorexit进入锁(异常退出)23: aload_224: athrow25: returnException table:from to target type7 17 20 any20 23 20 anypublic static void main(java.lang.String[]);Code:0: return
}
synchronized同步代码块,实现使用的是moniterenter和moniterexit指令(moniterexit可能有两个)
那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
/*** 锁普通的同步方法*/
public class LockSyncDemo {public synchronized void m2(){System.out.println("------hello synchronized m2");}public static void main(String[] args) {}
}
public synchronized void m2();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED //ACC_SYNCHRONIZED 表示该方法是普通的同步方法Code:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String ------hello synchronized m25: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 12: 0line 13: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this Lcom/llp/juc/lock/LockSyncDemo;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 17: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 args [Ljava/lang/String;
}
/*** 锁静态同步方法*/
public class LockSyncDemo {public synchronized void m2(){System.out.println("------hello synchronized m2");}public static synchronized void m3(){System.out.println("------hello synchronized m3---static");}public static void main(String[] args) {}
}
public static synchronized void m3();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //ACC_STATIC ACC_SYNCHRONIZED访问标志 区分该方法是否是静态同步方法Code:stack=2, locals=0, args_size=00: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #5 // String ------hello synchronized m3---static5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 19: 0line 20: 8public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 25: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 args [Ljava/lang/String;
}
管程概念
管程:Monitor(监视器),也就是我们平时说的锁。监视器锁
信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
溯源
Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp
ObjectMonitor.cpp中引入了头文件(include)objectMonitor.hpp
140行ObjectMonitor() {_header = NULL;_count = 0; //用来记录该线程获取锁的次数_waiters = 0,_recursions = 0;//锁的重入次数_object = NULL;_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我_WaitSet = NULL; //存放处于wait状态的线程队列_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ;//存放处于等待锁block状态的线程队列_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
class Ticket {private int number = 30;//非公平锁ReentrantLock lock = new ReentrantLock();//公平锁//ReentrantLock lock = new ReentrantLock(true);public void sale() {lock.lock();try {if (number > 0) {System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);}} catch (Exception e) {e.printStackTrace();} finally {//释放锁lock.unlock();}}
}public class SaleTicketDemo {public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(() -> {for (int i = 0; i < 35; i++) ticket.sale();}, "a").start();new Thread(() -> {for (int i = 0; i < 35; i++) ticket.sale();}, "b").start();new Thread(() -> {for (int i = 0; i < 35; i++) ticket.sale();}, "c").start();}
}
ReentrantLock lock = new ReentrantLock(true);
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
synchronized
是java中的关键字,默认是可重入锁,即隐式锁在同步块中
public class ReEntryLockDemo {public static void main(String[] args) {final Object object = new Object();//一个线程打进来外层同步代码块获取到锁,在进入内层同步代码块时可以自动获取到锁new Thread(() -> {synchronized (object) {System.out.println("-----外层调用");synchronized (object) {System.out.println("-----中层调用");synchronized (object) {System.out.println("-----内层调用");}}}}, "a").start();}
}
//-----外层调用
//-----中层调用
//-----内层调用
在同步方法中
public class ReEntryLockDemo {public synchronized void m1() {//指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁System.out.println(Thread.currentThread().getName() + "\t" + "-----come in m1");m2();System.out.println(Thread.currentThread().getName() + "\t-----end m1");}public synchronized void m2() {System.out.println(Thread.currentThread().getName() + "\t" + "-----come in m2");m3();}public synchronized void m3() {System.out.println(Thread.currentThread().getName() + "\t" + "-----come in m3");}public static void main(String[] args) {ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();reEntryLockDemo.m1();}
}
//从下面的打印结果可以看到通一个线程获取到外层锁可以进入内存的的同步方法
//main -----come in m1
//main -----come in m2
//main -----come in m3
//main -----end m1
ObjectMoitor.hpp
140行ObjectMonitor() {_header = NULL;_count = 0; //用来记录该线程获取锁的次数_waiters = 0,_recursions = 0;//锁的重入次数_object = NULL;_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我_WaitSet = NULL; //存放处于wait状态的线程队列_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ;//存放处于等待锁block状态的线程队列_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
ObjectMoitor.hpp底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。_count _owner
首次加锁:当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
synchronized是隐式锁。Lock是显示锁,所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁的操作。
lock
unlock
要成对public class ReEntryLockDemo {static Lock lock = new ReentrantLock();public static void main(String[] args) {new Thread(() -> {lock.lock();System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");try {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");} finally {//和内层lock对应lock.unlock();}} finally {//和外层lock对应lock.unlock();}}, "t1").start();}
}
//t1 ----come in 外层调用
//t1 ------come in 内层调用
lock
unlock
不成对,单线程情况下问题不大,但多线程下出问题public class ReEntryLockDemo {static Lock lock = new ReentrantLock();public static void main(String[] args) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");} finally {lock.unlock();}} finally {//外层lock得不到释放,当另一个线程进入外层lock时一直获取不到锁一直堵塞//lock.unlock();//-------------------------不成对|多线程情况}}, "t1").start();new Thread(() -> {lock.lock();try {System.out.println("t2 ----外层调用lock");} finally {lock.unlock();}}, "t2").start();}
}
//t1 ----come in 外层调用
//t1 ------come in 内层调用
//(t2 ----外层调用lock 假如不成对,这句话就不显示了)
可以看到灯一直亮着,外层锁一直得不到释放,而第二线程一直拿不到锁就在那里一直等待
死锁
是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
a跟b两个资源互相请求对方的资源
死锁产生的原因
系统资源不足
进程运行推进的顺序不合适
资源分配不当
public class DeadLockDemo {public static void main(String[] args) {Object objectA = new Object();Object objectB = new Object();new Thread(() -> {synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 持有a锁,想获得b锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//使得线程b也启动synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 成功获得b锁");}}}, "A").start();new Thread(() -> {synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 持有b锁,想获得a锁");synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 成功获得a锁");}}}, "B").start();}}
//输出结果
//A 持有a锁,想获得b锁
//B 持有b锁,想获得a锁
jps -l
查看当前进程运行状况jstack 进程编号
查看该进程信息win
+ r
输入jconsole
,打开图形化工具,打开线程
,点击 检测死锁
。