文章目录
- 1、synchronized
- 2、公平锁和非公平锁
- 3、可重入锁
- 4、死锁
1、synchronized
写个demo,具体演示下对象锁与类锁,以及synchronized同步下的几种情况练习分析。demo里有资源类手机Phone,其有三个方法,发短信和发邮件这两个方法有synchronized关键字,另一个普通方法getHello。然后启动两个线程AA和BB,且二者进入就绪状态中间休眠100ms,给AA一个先抢夺CPU时间片的优势。
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
/**
* start后线程进入的是就绪状态,即具有抢夺CPU时间片(执行权)的能力,并不是直接执行
* 这里刻意休眠100毫秒,让AA线程先去抢时间片,给AA一个先执行的优势
*/
Thread.sleep(100);
new Thread(() -> {
try {
phone.sendEmail();
//phone.getHello();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}
class Phone {
public synchronized void sendSMS() throws Exception {
//TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
分析以下八种情况的输出结果:
Case1:就上面的代码,直接执行
Case2:sendSMS()方法体加一行TimeUnit.SECONDS.sleep(4),即停留4秒
分析:对于上面两种情况,synchronized出现在示例方法中占的是对象锁,而两线程共用同一个对象,因此先抢时间片的线程AA先执行,而sleep是抱着锁睡,所以输出都是:
------sendSMS
------sendEmail
Case3:BB线程改为调用普通方法getHello
分析:getHello方法不用对象锁,所以不用等,而AA线程的sendSMS要sleep4秒,因此getHello就先输出:
------getHello
------sendSMS
Case4:两个手机Phone对象,分别给AA和BB线程调用两个synchronized方法
分析:两个Phone对象,两个对象锁,各自调synchronized实例方法,没有抢锁和等待的情况,没有sleep的自然先输出:
------sendEmail
------sendSMS
Case5:两个synchronized方法均加static改为静态方法,两线程共用1个Phone资源对象
Case6:两个synchronized方法均加static改为静态方法,两线程分别用2个Phone资源对象
分析:synchronized两个静态方法,锁的就是类锁,即当前类的Class对象,一个类就一把类锁,所以尽管有两个Phone对象在调也没用,先拿到类锁的先执行并输出:
------sendSMS
------sendEmail
Case7:BB线程调用静态同步sendEmail、AA线程调用无static的同步方法sendSMS,两线程共用1个Phone资源对象
Case8:BB线程调用静态同步sendEmail、AA线程调用无static的同步方法sendSMS,两线程分别用2个Phone资源对象
分析:不管1个/2个Phone对象,AA线程用的对象锁,BB线程用的类锁,互不影响,对象锁代码中有sleep,晚输出:
------sendEmail
------sendSMS
总结:
synchronized实现同步时:
- synchronized加在静态方法上,锁的就是类锁,即当前类的Class对象,一个类就一把类锁
- synchronized加在实例方法上,锁的是调用该方法的当前对象,是对象锁,一个类创建100个对象,就有100把对象锁
- synchronized加在代码块上,锁的是synchronized括号里配置的对象
2、公平锁和非公平锁
还是之前三个线程卖30张票的例子:
//资源类
class LTicket{
private Integer number = 30;
private final ReentrantLock lock = new ReentrantLock();
public void sale(){
//上锁
lock.lock();
try {
if( number > 0 ){
System.out.println(Thread.currentThread().getName() + ": 卖出票,剩余" + number--);
}
} finally {
//释放锁写finally语句中,防止上面发生异常导致锁未释放
lock.unlock();
}
}
}
开启三个线程,调用资源类的方法:
public class LSaleTicket {
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(() -> {
for(int i = 0 ; i < 40; i++ ){
ticket.sale();
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0 ; i < 40; i++ ){
ticket.sale();
}
}
},"BB").start();
new Thread(() -> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}, "CC").start();
}
}
执行会发现,经常AA一个线程就把所有票一个人卖完了:
这就是非公平锁,上面new Lock对象时:
private final ReentrantLock lock = new ReentrantLock();
//不传参数,默认非公平锁
源码:
改为公平锁:
private final ReentrantLock lock = new ReentrantLock(true);
看下效果:
总结:
- 非公平锁:可能导致线程饿死,但效率高
- 公平锁:阳光普照,但效率比非公平锁低
二者相比,就像去图书馆自习,非公平锁是看到座位就座,公平锁则是先问下这里有人吗,如果有,就排队,公平锁的源码:
3、可重入锁
synchronized( 隐式)和 Lock( 显式)都是可重入锁,这里的显式隐式即指的Lock需要开发者手动加锁解锁。可重入锁,理解为你家大门上有锁、卧室门上有锁、卫生间有锁,但只要你打开了大门的锁,卧室、卫生间就不需要再次开锁了,这些房间就可以自由进入了。类比到代码中,就是他们用的是同一把锁。可重入锁又叫递归锁。
写个synchronized同步代码块
public class SyncLockDemo {
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 外层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 中层");
synchronized (o){
System.out.println(Thread.currentThread().getName() + " 内层");
}
}
}
},"t1").start();
}
}
关于可重入锁又叫递归锁:
public class SyncLockDemo {
public synchronized void add(){
add();
}
public static void main(String[] args) {
new SyncLockDemo().add();
}
}
可以看到递归调用synchronized实例方法add,会出现StackOverflowError,就可以说明可重入锁的特点,要是不可重入,那递归时再调add方法,就没对象锁给它用了。再用lock显示演示:
另外,lock与unlock必须成对,当然这里内层锁的unlock注释掉,也能运行成功,进入大门后你是可以自由活动的,但你少个unlock,后面线程再想lock,就等不到锁了,相当于你进门,自己休息好了再出来却不带钥匙就把门关了。
4、死锁
t1线程执行某同步代码块,用到了对象1的锁和对象2的锁,即t1线程需要先锁对象1,再锁对象2,全锁以后,算同步代码块执行结束,然后一下释放两个对象锁。(A加锁-B加锁-B解锁-A解锁)
t2线程执行另一个同步代码块,需要先锁对象2,再锁对象1才算这个同步代码块执行结束,然后释放两个对象锁。(B加锁-A加锁-A解锁-B解锁)
如此:t1锁到对象2的时候,发现已被锁,则等待,而另一边:t2锁到对象1的时候,发现对象1已被锁,两个线程同时陷入无休止的等待…尬住了。此时,若无外力干涉,就执行不下去了,表现在执行结果就是光标闪烁,无输出,但也没有执行结束。
产生死锁的原因:
- 系统资源不足
- 进程运行推进顺序不合理
- 资源分配不当
写个死锁的Demo:
public class DeadLock {
public static void main(String[] args) {
Object o1= new Object();
Object o2= new Object();
new Thread(() -> {
synchronized (o1){
System.out.println(Thread.currentThread().getName() + "===>持有o1对象锁,试图获取o2对象锁");
try {
Thread.sleep(200); //抱着o1锁睡会儿,别太快执行结束释放两个锁,以保证死锁必现
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName() + "===>获取到o2对象锁");
}
}
},"t1").start();
new Thread(() -> {
synchronized (o2){
System.out.println(Thread.currentThread().getName() + "===>持有o2对象锁,试图获取o1对象锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println(Thread.currentThread().getName() + "===>获取到o1对象锁");
}
}
},"t2").start();
}
}
运行:
当然,不能一看到不exit0,也无输出就说是死锁,死循环、远程调用也可能有这个情况,关于是否是死锁的验证:
- jps:类似Linux的ps -ef
- jstack:JVM自带堆栈跟踪工具
关于jps:
没配环境变量,在IDEA终端先cd到这儿,再执行:
然后在IDEA终端继续执行:
jstack 10212
返回的关键信息:
关于死锁和线程安全的优化:
synchronized会让程序执行效率变低,系统吞吐量降低,用户体验变差。解决线程安全,可考虑:
使用局部变量代替实例变量和静态变量
若必须使用实例变量,考虑多创建几个对象,别对象共享了也就没有安全问题了
- 若以上两条都做不到,则用synchronized