Java多线程(二)(synchronized,ReetrantLock)锁机制

一、锁的概念

1.什么是锁

1.1 简单的来说,有一间房屋,屋里面有很多钱。现在有很多人都想得到那笔钱,首先我们得想办法进入那件房屋吧,房屋的门上有一把锁,这把锁可能有一个钥匙也有可能有多个钥匙(),谁或得了这个钥匙,谁就能得到这比钱

1.2 现在我们把上面的例子映射到Java中,房屋就是一个加了同步的代码块/方法,里面的钱就是代码块/方法执行的内容,房屋外的每一个人代表一个线程,钥匙便是代码块/方法的锁。一把钥匙代表这是一个独享锁,多把钥匙代表这是一个共享锁(后面会讲到锁的分类)

1.3 当一个线程拿到了这个代码块/方法的锁,就能执行里面的内容。

2.不用锁如何

2.1 如果这个房屋没有锁,那么是不是任何人都可以进去了,老板给下面的员工布置了一个人物,去里面取5000元出来,本来里面共有2000元,由于没有锁,3个员工同时进去各自取了5000元出来,本来老板一位房屋里还应该有15000元,但是没想到最后只剩5000元,你是老板,你干不干这比生意哦。

2.2 main()让两个线程同时执行一个代码块/方法,由于没锁,最后方法的值按设置应该返回15000,结果返回5000。这样的程序显然不是我们想要的。

3.用了锁如何

3.1 有了锁之后,只有其中一个人能进去取钱,另外两个想进去,但是也进不去。所以老板很满意,老板满意,员工就跟着幸福。

3.2 main()给了让两个线程去同时执行代码块/方法,而这个方法只有一把锁,至于哪个线程抢到这把锁,它不关心,它只需要最后程序能按预期的结果输出便行。

二、ReentrantLock和synchronized

1.synchronized

1.1 synchronized属于java关键字,可作用于方法和代码块、

1.2Java另外一个关键字volatile也很重要,保证属性的可见性和防止指令重排序,今天不讲这个,只简单提一下。

2.ReentrantLock

2.1 ReentrantLock是java.util包下的一个类。继承于Lock接口。

2.2 获取锁的方法主要有
lock()获取锁,若锁已被其他线程所拥有,则进入等待。
tryLock() 通过返回判断是否拿到锁,意思就是尝试获取锁,如果没获取到,也不会等待,直接返回false
tryLock(long timeout, TimeUnit unit) 只是比上面一个方法多了一个等待时间,
lockInterruptibly() 也会等待获取锁,但是在还没获取到锁的情况下,可以通过调用interrupt()方法来中断这个等待状态。

2.3 我们获取了锁,肯定也必须释放锁,至于什么时候释放,这就是和snychronized的区别了,snychronized需同步代码块执行完毕或抛出异常后,会自动释放锁。而ReentrantLock的unlock() 需要用户调用此方法来手动释放锁。

2.4 需要注意的一点是,ReentrantLock的lock操作最好在try{}catch(){}语句中执行最后在finally{}中执行释放锁的操作,因为如果程序抛异常后,ReentrantLock也不会自动个释放锁,这就造成了死锁,也就是房屋的门只有一把钥匙,现在钥匙丢了,里面就算有再多的钱你也只能望着啊,气不气。所以在实际操作中,这一步不能忽略。

3.uml图

3.1 本次项目一共6个类,废话不多说。直接上UML图

在这里插入图片描述

4.代码

1先看walk接口。可有可无,但为了规范代码,可有。

package com.lock;

public interface Walk {
	 void walk();
}

2 SynWalk类,该类实现Walk接口的walk()方法,并在方法内部使用了synchronized(){}代码块,这里为什么要把业务单独建一个Walk类,而不在Thread类中的run方法直接写,原因在于,线程是线程,普通业务类是才是我们需要执行的任务,以前看别人在Tread类中调用wait()方法,notifyAll()方法,就很容易搞混,会误以为,这是线程的方法。其实主要要分清两个概念:
2.1 线程是线程,线程的主要目的是执行任务。
2.2 我们的业务类都是一些任务,需要经过执行来得到一些预期的结果,线程是服务于任务的
我们最终的目的并不是你创建了多少线程啊,最终目的是在最短的时间内高效无误的执行完我们想要的代码,只是这需要借助于线程来提高效率。千万不要因为线程而创建使用线程,你如果计算个i=1;没必要创两个线程吧,这是个原子操作,你怎么去执行?扯远了,看代码

package com.lock;

public class SynWalk implements Walk{

	private int count=0;
	
	@Override
	public void walk() {
		synchronized (this) {
			while(count<10){
				count++;
				System.out.println(Thread.currentThread().getName()+"走了"+count+"步");
			}
		}
	}
}

3 LockWalk类,也实现了Walk接口的walk()方法,一个私有属性ReentrantLock的实例。

package com.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockWalk implements Walk{
	
	private  int  count;
	
	private Lock lock=new ReentrantLock();
	
	@Override
	public void walk() {
		if(lock.tryLock()){
			try {
				while(count<10){
					count++;
					System.out.println(Thread.currentThread().getName()+"走了"+count+"步");
					if(count!=0&&count%3 == 0){
						lock.unlock();
					}
				}
			} catch (Exception e) {
				
			}finally {
				if(!lock.toString().contains("Unlocked")){
					lock.unlock();
				}
			}
		}
	}
}

4 ThreadSyn线程类, 这个线程用来执行我们SynWalk类里面的任务。

package com.lock;

public class ThreadSyn extends Thread{

	private SynWalk synWalk;
	
	public ThreadSyn(String name,SynWalk synWalk) {
		super(name);
		this.synWalk=synWalk;
	}
	
	@Override
	public void run() {
		synWalk.walk();
	}
}

5 ThreadLock线程类, 这个线程用来执行我们LockWalk类里面的任务

package com.lock;

public class ThreadLock extends Thread{
	
	private LockWalk lockWalk;
	
	public ThreadLock(String name,LockWalk lockWalk) {
		super(name);
		this.lockWalk=lockWalk;
	}

	@Override
	public void run() {
		lockWalk.walk();
	}
}

6.Test测试类,一共创建了两个线程去执行synWalk的任务,两个线程去执行lockWalk的任务。

package com.lock;

public class Test {
	
	public static void main(String[] args) {
		SynWalk synWalk=new SynWalk();
		LockWalk lockWalk=new LockWalk();
		Thread t1=new ThreadSyn("syn1",synWalk);
		Thread t2=new ThreadSyn("syn2",synWalk);
		Thread t3=new ThreadLock("lock1",lockWalk);
		Thread t4=new ThreadLock("lock2",lockWalk);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

5.结果

5.1运行结果:
syn1走了1步
syn1走了2步
syn1走了3步
syn1走了4步
syn1走了5步
syn1走了6步
syn1走了7步
syn1走了8步
syn1走了9步
syn1走了10步
lock1走了1步
lock1走了2步
lock1走了3步
lock1走了4步
lock1走了5步
lock1走了6步
lock2走了7步
lock2走了8步
lock2走了9步
lock2走了10步

5.2结果分析,虽然我们共创建了4个线程,每两个线程去执行同一个任务。
使用snychronized的任务被一个线程执行完毕
使用lock的任务被两个线程执行了不同的部分,在6之后,6为3的倍数,所以后面是lock2执行了,这也是我们想要的,
5.3但是有一个问题,不知道大家发现没有,我写的是count%30;难道3%3不为0,肯定不是的,这里的一个原因我想,我们lock1启动之后,便执行for循环,在执行到count3的时候,lock2虽然在lock1.start之后就start。但是调用start()方法启动也是需要花费时间的,这时候lock2还没有启动完毕。
5.4你如果在多执行两次,你会发现结果有可能不一致,这也是上面出现那个问题的第二种原因,我使用的是tryLock()方法,这个方法并不会等待,如果锁被其他线程占有时,便直接返回false,这就意味着,可能我们的任务被lock1全部执行了。
5.5 你可以试着改下,去掉if判断句,将tryLock()方法改为lock();在打印语句处再调用一线线程的休眠sleep(1000),改方法并不会释放锁,查看结果,试着去分析下结果为什么会是这样的,只有自己分析进步才是最快的,别人讲在多也不如自己动动手。

6.两者区别

6.1 synchronized是关键字,也是重量级锁,为什么会是重量级锁,后面有介绍。不需要手动释放锁,不必担心出现死锁的情况。

6.2 ReentrantLock是util包下的一个类,里面封装好了方法,我们只需调用即可,需要手动释放锁。也是重量级锁。lock可明确的知道是否获得了锁,而synchronized不行,ReentrantLock可手动中断等待操作,synchronized不行

6.3 如果在不熟悉多线程的使用,可以使用synchronized,尽量使用lock,在线程较多的,资源竞争较大的情况下,ReentrantLock的性能要高于synchronized。

五、锁分类

1.公平锁

1.1什么是公平锁,有公平锁,是否就有不公平锁

1.2公平锁就是我们高中的时候在食堂排队吃饭,谁排在第一位,谁就能先吃到第一个螃蟹,而非公平锁,就像我们买彩票一样,为什么别人能中500万,却总轮不到我中一次。ReetrantLock默认是非公平锁,可通过构造韩素传参来设置是否公平,可通过isFair() 方法来查看是否是公平锁。synchronized也是非公平锁

2.可重入锁

2.1 如果是可重入锁,一切都好说,如果是不可能重入锁极有可能造成死锁。就是可递归获取锁。说得我相信没了解过这块的同学觉得一脸懵逼,就我自己看到这些也同样是一脸懵逼。

2.2我们还是通俗点好,容易懂。先举个例子,可重入锁:你老公是个高富帅,当你嫁给了他,你就得到了他,财产归你管,属于你的名下,同时也得到了他的全部财产。不可重入锁:你嫁给了你老公,你得到了他,财产归你管,属于你的名下。但是你需要拥有财产的人放弃财产时,你才能把财产写在你的名下。但是你当前在等待别人去财政局取消财产名字,你自己便无法同时干两件事,所以你便一直等待,等一辈子,都没得财产,实际财产已经在你的名下了。就造成了死锁。

2.3 言归正传,可重入锁:执行setA(),setA()中调用setB(),默认便拥有了setB()的锁,所以可直接执行。如果是不可重入锁,便需要等待拥有setB()锁的线程释放锁之后,才可能获取,但当前线程已经占有了setB()的锁,所以便会一直等待。造成死锁。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

3.共享锁

3.1 有共享锁,当然就有独享锁了,共享代表可被多个线程同时拥有该锁,也就是我一开始讲的那个例子,多个人拥有这间屋子的钥匙。读锁便是共享锁。这篇文章中就不详细介绍读锁了,主要目的在程序按照正常的逻辑执行过程中,提高效率。

3.2 独享锁,synchronized,ReetrantLock都是独享锁,一个屋子只有一把钥匙。

4.互斥锁

4.1 有没有同学觉得互斥锁和独享锁很相近,没错,就是这样的,互斥锁是上面的具体实现,有你没他,有他没你。读写锁就是互斥锁,ReetrantReedWriteLock。

5.乐观锁

5.1 乐观锁,就是你加不加锁,它的运行结果都一样,不会有什么改变。那还需要锁干嘛
5.2 悲观锁,多个线程同时执行一段代码,不加锁,一定会发生改变。上面讲的几个锁的类型全是悲观锁。

6.分段锁

6.1 我们都知道ConcurrentHashMap是线程安全的,HashMap是线程不安全的。
hashmap的结果为<key ,value> 其实hashmap就是一个数组类型,key就是它的下标,不过里面经过了运算。而每一个value又是一个链表。然后我们在链表上加锁。进行多个put操作时,几个线程同时操作,你操作一个链表,我操作另一个链表,他又操作另外一个链表,这就提高了效率,这也是并行操作。并发是逻辑上的并行操作,但并不真正意义的是并行操作。

7.偏向锁

7.1 讲之前还是先举个例子。有一个荒岛,荒岛上有一个男生,一个美女,男生虽然长得不咋的,但是比较在荒岛上,也没有其他人,所以这位美女很愿意和他交流,比较偏向于他。
突然某一个,有一位遇到海难的帅哥不知不觉飘到了此岛,美女救起了他,一看,这么帅,从此便一发不可收拾。一开始长得不怎么样的那位男生看到人家比他长得帅,心里阵阵痛楚。但是他没有放弃,他想,毕竟我和这位美女相处的时间比较久一些。也许她是一个在乎感情的人。一开始美女也觉得为难,到底是该相信感情,还是相信帅,美女一直模糊不定
最后美女痛下决心,感情不能当饭吃,不如找个帅一点的。最后那个男生没想到等了她这么久,她还是不会回心转移。最后昏倒在地上,不醒人事。
后面陆陆续续逃难来了很多人,有长得帅的,也有丑的,但都得排队,最后大家都感觉心已死,除了拥有那位美女的男生坚持活下来外,其他的都昏死在地上。

7.2 还是言归正传 扯了那么多没用的 前面我们说到了synchronized为什么是重量级锁。
一段代码加上了synchronized,当只有一个线程访问时,这时锁会偏向这个线程,便是偏向锁
在这过程中,另外一个线程也想访问该代码的锁。这时候就升级成了轻量级锁。其他线程可以通过自旋的方式等待锁的获取,不会阻塞,提高性能,因为线程阻塞是很耗性能的
当自旋了一定次数都没得到该锁,便会升级为重量级锁,重量级锁便会让其他试图获得该锁的线程进入阻塞状态,从而影响性能。

8.自旋锁

8.1 前面说的轻量级锁便是一直在自旋,就是循环的试图去得到那位美女,但还不至于昏死,心里还存有一点点希望。

五、总结

也没什么好总结的,学习靠自己,多动手,没事写点段子,拿出来大家乐呵乐呵。
这就叫做劳逸结合吧,在学习中享受快乐。大家有好文,也希望能在评论下方发链接,共勉之。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值