多线程
七、悲观锁和乐观锁
1.悲观锁
1.1概念
总是假设最坏的情况,也就是每一次去拿数据都默认别人会修改,所以每次拿数据都会上锁,这会导致有其他人想要拿到数据就会阻塞知道获取到这把锁
synchornized关键字的实现就是:悲观锁
1.2 悲观锁机制存在的问题
1、多线程竞争下,加锁,解锁都会导致比较多的上下文切换和调度延迟,引起性能低下
2、一个线程池有锁会导致其他需要此锁的线程堵塞
3、数据量较大时,独占锁会导致效率低下
4、如果是一个优先级高的线程等待一个优先级低的线程释放锁会导致性能低下问题
2、乐观锁
2.1概念
每次拿数据都默认别人不会修改,所以不会上锁,在更新的时候判断,有没有人修改这个数据。乐观锁适用于多读的场景,这个可以提高吞吐量
2.2 乐观锁的实现方式
1、CAS操作(上面第五章,第4.2小节内容对CAS操作的实现有详细说明)
Java对CAS的支持:jdk1.5之后新增了java.util.concurrent.atomic包下的Atomic…的类都是建立在CAS的操作基础上,在性能上有很大提升
我们用以:AtomicInterger这个类为例子,看看采用不加锁的方式如何实现线程安全
我们的想法:
value1 没有进行任何线程安全保护
value2 采用CAS操作
value3 采用悲观锁 synchronized
三种不同方式对value进行加加操作,我们看运行结果:
public class Test07 {
private static int value1 = 0;//线程不安全
private static AtomicInteger value2 = new AtomicInteger(0);//CAS操作
private static int value3 = 0;//synchornized
private static synchronized void sumValue3(){
value3++;
}
public static void main(String[] args) {
for(int i = 0;i<1000;i++){
new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
sumValue3();
}
}.start();
}
//查看活跃的线程数目
//活跃线程要大于2,因为idea工具的原因,会有一个人monitor线程
while ( Thread.activeCount()>2){
Thread.yield();
}
try {
TimeUnit.SECONDS.sleep(10);
System.out.println("线程不安全:"+value1);
System.out.println("乐观锁:"+value2);
System.out.println("悲观锁:"+value3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分析:
1、对于线程不安全的value1,我们并未对其采用任何加锁操作,所以它是一个小于1000的随机值;
2、对于value3,我们把value3++操作放在synchronized同步方法里面,我们知道synchronized是满足编程三大特性的,它保证了value3++的原子操作,所以值是1000;
3、对于value2++,我们调用的是:AtomicInteger 中的getAndIncrement()方法,让我们来从源码分析,看看这个方法的怎么把加加这个非原子操作变成原子操作的:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
分析:
1>unsafe是用来帮助java访问操作系统底层的类,如分配、释放内存;getAndAddInt(this, valueOffset, 1);中的三个参数:this指的是AtomicInteger的引用,也就是我们代码中的value2,valueOffset就是当前this值在内存中的偏移量,1就是我们要进行+1操作,所以这个方法实现的就是我们的CAS操作
2>
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
我们在调用AtomicInteger有参方法时候,会把参数赋值给value,而value是用volatile修饰的,保证了可见性和有序性
总结:AtomicInteger中volatile和cas一起保证线程安全
2、版本号机制
共享数据中增加一个:version字段,表示该数据的版本号。如果当前数据发生改变,版本号+1
2.3 乐观锁存在的问题
CAS存在的问题:
1、A->B->A问题
什么是ABA问题?
答:我们知道,CAS有三个值:内存值A,旧的预期值A,要替换的值B。现在我们线程1要采用CAS操作把我们的内存值A变成B,但是在此之前,有个线程2经过一系列操作,把我们的内存值A变成B,然后又变成A,轮到线程1操作,貌似还是和原来一样,内存值还是A。
但是问题就出现了:
,举个例子:内存A看作我们的链表头,我现在把链表头A经过一系列操作,链表头A换成B,又换成A,现在的这个链表头是A的链表还是原来的链表吗??
我觉得不一定吧,我可能加几个链表节点,删除几个链表节点等等操作,最后只保证你链表头是A就可以了,对吧,所以说A-B-A操作是CAS存在的一个问题
如何解决A-B-A问题机制?
答:版本号机制(上面小节讲过)
2、CAS只能保证一个共享变量的原子性
java AtomicReference 可以操作多个共享变量
3、自旋CAS如果长时间不成功,给cpu带来很大的开销
八、死锁
1.造成死锁的条件
a、互斥条件:某个资源在某一时间只能有一个线程占用
b、不可抢占条件:线程所得到的资源在为使用完毕前,资源申请者不能强行夺取资源者手中的资源
c、占有且申请条件:线程至少占有一个资源,又要申请别的资源,而别的资源有被其他线程占用,该线程堵塞
d、循环等待条件:一个线程等待其他线程释放资源,其他线程又在等待另外的线程释放资源,直到最后一个线程等待第一个线程释放资源,这使得所有线程都锁住
2.常见的死锁问题
a、交叉锁
public class DeadLock1 implements Runnable {
public int flag = 1;
private static Object o1 = new Object();
private static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0){
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock1 t1 = new DeadLock1();
DeadLock1 t2 = new DeadLock1();
t1.flag = 1;
t2.flag = 0;
new Thread(t1).start();
new Thread(t2).start();
}
}
b、内存不足
并发请求系统内存,如果当前内存不足,有可能出现死锁问题
c、一问一答式的数据交互:
服务器开启某个端口,等待客户去访问,客户端访问发起请求等接收服务器端返回的资源,可能由于网络问题,服务器端错过了客户端的请求
d、死循环引起的死锁
3.哲学家就餐问题
哲学家就餐问题就是,几个人围着桌子(黑圆圈)吃饭,吃饭就要有筷子(红色画的),需要两只筷子(可能这几个人都拿自己左边/右边的一根筷子,谁也不愿意放下筷子让别人先吃,从而发生死锁)
代码实现哲学家就餐问题:
//筷子
class ChopStick{
protected String name;
public ChopStick(String name) {
this.name = name;
}
}
//哲学家
class PhilosopherThread extends Thread{
private ChopStick left;
private ChopStick right;
private String name;
public PhilosopherThread(ChopStick left, ChopStick right, String name) {
this.left = left;
this.right = right;
this.name = name;
}
@Override
public void run() {
synchronized (left){
System.out.println(name+"获得"+left.name);
synchronized (right){
System.out.println(name+"获得"+right.name);
System.out.println(name+"在吃");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+"吃完了");
}
}
}
}
public class TestDemo10 {
public static void main(String[] args) {
ChopStick chopStick1 = new ChopStick("1");
ChopStick chopStick2 = new ChopStick("2");
ChopStick chopStick3 = new ChopStick("3");
ChopStick chopStick4 = new ChopStick("4");
new PhilosopherThread(chopStick4,chopStick1,"thread1").start();
new PhilosopherThread(chopStick1,chopStick2,"thread2").start();
new PhilosopherThread(chopStick2,chopStick3,"thread3").start();
new PhilosopherThread(chopStick3,chopStick4,"thread4").start();
}
}
上述哲学家就餐问题的代码可能会发生死锁,我们怎么解决呢?
代码实现:
//筷子
class ChopStick{
protected static HashMap<Integer,Boolean> map = new HashMap<>();
static {
map.put(0,false);
map.put(1,false);
map.put(2,false);
map.put(3,false);
map.put(4,false);
}
//获得筷子
public synchronized void getCh(){
String curren = Thread.currentThread().getName();
int left = curren.charAt(curren.length()-1)-'0';
int right = (left+1)% 5;//left+1;
while (map.get(left)|| map.get(right)){
//说明有哲学家在使用当前筷子
//当前线程需要等待
try {
this.wait();//释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
map.put(left,true);
map.put(right,true);
System.out.println(Thread.currentThread().getName()+"获得"+left+" and "+right);
}
//释放筷子
public synchronized void freeCh(){
String curren = Thread.currentThread().getName();
int left = curren.charAt(curren.length()-1)-'0';
int right = (left+1)% 5;//left+1;
map.put(left,false);
map.put(right,false);
//唤醒所有等待的哲学家
this.notifyAll();
}
}
public class TestDemo10 {
public static void main(String[] args) {
ChopStick chopStick = new ChopStick();
for (int i = 0;i<5;i++){
new Thread("thread"+String.valueOf(i)){
@Override
public void run() {
while (true){
//获得两个筷子
chopStick.getCh();
System.out.println(Thread.currentThread().getName()+"eating");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//吃完了,放筷子
chopStick.freeCh();
}
}
}.start();
}
}
}
注意:wait(),notifyAll();方法需要在我们的synchronized方法/同步代码块里面的
4.死锁的预防
死锁的预防:打破产生死锁的四个必要条件中一个或者多个条件
1、银行家算法
在资源分配之前预先去判断此次分配是否会导致当前系统进入不安全状态,以此决定是否答应资源的分配请求
一、银行家算法视频讲解
二,银行家算法代码实现
大家看代码前,最好看下上述的视频讲解的例题,看看里面的各类资源都是怎么计算,代表的什么
public class TestDemo11 {
//可用资源
private static int [] avaliable = new int[]{3,3,2};
//每个线程最大的资源数
private static int[][] max = new int[][]{{8,5,3},{3,2,2},{9,0,2},{2,2,2},{4,3,3}};
//每个线程已经分配的资源
private static int[][] alloction = new int[][]{{0,1,1},{2,0,0},{3,0,2},{2,1,1},{0,0,2}};
//每个线程需要的资源数
private static int[][] need = new int[][]{{8,4,2},{1,2,2},{6,0,0},{0,1,1},{4,3,1}};
public static void showData(){
System.out.println("线程编号 最大需求 已经分配 还需要");
for (int i = 0;i<5;i++){
System.out.print(i+" ");
for (int j = 0;j<3;j++){
System.out.print(max[i][j]+" ");//i表示线程,j表示资源数
}
for (int j = 0;j<3;j++){
System.out.print(alloction[i][j]+" ");
}
for (int j = 0;j<3;j++){
System.out.print(need[i][j]+" ");
}
System.out.println();
}
}
/**
*
* @param requestNum 表示所请求的线程号
* @param request 当前线程请求的资源数
* @return
*/
public static boolean allocate(int requestNum,int request[]){
if (!(request[0]<=need[requestNum][0] && request[1]<=need[requestNum][1]&&request[2]<=need[requestNum][2])){
System.out.println("请求的资源数目超过了当前线程需要的资源数");
return false;
}
if (!(request[0]<=avaliable[0]&&request[1]<=avaliable[1]&&request[2]<=avaliable[2])){
System.out.println("目前没有足够的资源分配,现需等待");
return false;
}
//尝试预分配
for (int i = 0;i<3;i++){
//剩余资源
avaliable[i] = avaliable[i]-request[i];
//已经分配的资源+请求资源数目
alloction[requestNum][i] = alloction[requestNum][i] + request[i];
//还需要的资源
need[requestNum][i] = need[requestNum][i]-request[i];
}
//进行安全检查
boolean flag = checkSaft();
if (flag){
System.out.println("能安全分配");
return false;
}else {
System.out.println("不能安全分配");
for (int i = 0;i<3;i++){
//剩余资源
avaliable[i] = avaliable[i]+request[i];
//已经分配的资源+请求资源数目
alloction[requestNum][i] = alloction[requestNum][i] - request[i];
//还需要的资源
need[requestNum][i] = need[requestNum][i]+request[i];
}
return false;
}
}
public static boolean checkSaft(){
//循环去遍历线程,查看可用资源是否可以满足其余线程的资源请求
//满足则判断下一个线程,否则则判断下一个线程
int i = 0;
boolean[]finish = new boolean[5];
while (i<5){
if (finish[i] == false&& need[i][0]<=avaliable[0]&& need[i][1]<=avaliable[1]&&need[i][2]<=avaliable[2]){
System.out.println("分配成功线程"+i);
//执行成功之后释放所有资源
for (int j = 0;j<3;j++){
avaliable[j] = avaliable[j] + alloction[i][j];
}
finish[i] = true;
i = 0;
}else {
i++;
}
}
for (int m = 0;m<5;m++){
if (finish[m] == false){
return false;
}
}
return true;
}
public static void main(String[] args) {
showData();
System.out.print("当前系统可用资源:");
for (int i1 = 0;i1<3;i1++){
System.out.println(avaliable[i1]+" ");
}
//请求线程资源存放的数组
int []request = new int[3];
int re;
String sourse[] = new String[]{"A","B","C"};
Scanner s = new Scanner(System.in);
String ch = new String();
while (true){
System.out.println("输入要请求的线程编号:");
re = s.nextInt();
System.out.println("输入要请求的线程资源数目:");
for (int i = 0;i<3;i++){
System.out.println(sourse[i]+"资源数目:");
request[i] = s.nextInt();
}
//分配资源
allocate(re,request);
System.out.println("是否再次请求分配:y/n");
ch = s.next();
if (ch.equals("n")){
break;
}
}
}
}
九、线程通信
线程通信及就是线程之间通信合作,一个事情由不同的线程协作完成
1.解析wait/notify/notifyAll
synchronzied加锁的线程的Object类中的wait/notify/notifyAll
1、总结三个方法:
1.wait/notify 是final native的方法,不可被重写
2.通过某一个对象调用wait方法,表示使得调用当前的方法的线程堵塞,并且当前线程必须要拥有这个对象的monitor lock
3.调用某一个对象的notify方法,表示唤醒其中一个调用了wait方法的线程
4.调用某一个对象的notifyAll方法时,表示唤醒了所有调用了wait方法的线程
1>wait()方法:
该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用
wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入
wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用
wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是 RuntimeException
的一个子类,因此,不需要 try-catch 结构。
2>notify()方法
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用 notify()时没有持有适当的锁,也会抛出
IllegalMonitorStateException。该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个
wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait
所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized
代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的
wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait
状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或
notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。这与下面的
notifyAll()方法执行后的情况不同。
3>notifyAll()方法
该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
面试题: 为什么这三个方法不是Thread中的方法,而是Object中声明的方法
答:每个对象都拥有对象锁(monitor lock),当某个线程等待一个对象锁,需要使用对象去操作,即表明需要获取的当前对象锁的线程等待/唤醒
2、使用这些方法存在的"坑"
- 使用wait()方法要注意的的问题
问题1:
public class TestDemo12 {
public static void main(String[] args) {
String lock = new String("test");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 问题:java.lang.IllegalMonitorStateException ,没有用到监视器锁,所以会出现异常
*/
}
}
问题2:
public class TestDemo12 {
public static void main(String[] args) {
String lock = new String("test");
synchronized (lock){
try {
lock.wait();
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* wait()之后使得当前线程堵塞,wait之后的代码是不会运行的,释放当前所拥有的monitor lock
*/
}
}
}
问题3:
public class TestDemo12 {
public static void main(String[] args) {
String lock = new String("test");
new Thread("A"){
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
System.out.println("线程id:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("B") {
@Override
public void run() {
synchronized (lock) {
System.out.println("开始notify time" + System.currentTimeMillis());
lock.notify();
System.out.println("开结束notify time" + System.currentTimeMillis());
}
}
}.start();
}
/**
* 运行结果:
* 开始notify time1600250993548
* 开结束notify time1600250993548
* 线程idA
* 说明:notify();方法执行后并不会立刻释放锁
*/
}
2.锁池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池::假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
下图是线程状态间的转换:
详解版:
简化版:
3.生产者消费者模型
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞。
代码实现:
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
class BlockingQueue<E>{
private final LinkedList<E> queue = new LinkedList<>();
private static int max;//表示堵塞队列存储的最大值
private static final int DEFAULT_MAX_VALUE = 10;
public BlockingQueue(){
this(DEFAULT_MAX_VALUE);
}
public BlockingQueue(int max) {
this.max = max;
}
//生产数据
public void put(E value){
synchronized (queue){
if (queue.size()>=max){
System.out.println(Thread.currentThread().getName()+":is full");
try {
//没有位置,当前生产数据就会等待
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"添加"+value);
queue.addLast(value);
queue.notifyAll();
}
}
//消费数据
public E take(){
synchronized (queue){
//判断是否有可消费的数据
if (queue.isEmpty()){
System.out.println(Thread.currentThread().getName()+":isEmpty");
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E result = queue.removeFirst();
System.out.println("删除"+result);
queue.notifyAll();
return result;
}
}
}
public class TestDemo13 {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new BlockingQueue<>();
new Thread("product"){
@Override
public void run() {
while (true){
queue.put((int)(Math.random()*1000));
}
}
}.start();
new Thread("Consumer"){
@Override
public void run() {
while (true){
queue.take();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
十、ReentrantLock
jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
ReentrantLock几个特有方法:
tryLock(timeout):可定时的
lockInterruptibly:可响应中断的
tryLock():可轮询
ReentrantLock的使用格式:
Lock lock = new ReentrantLock();
lock.lock();//加锁
try{
//共享代码
}catch{
//
}finally{
lock.unlock();//释放锁
}
ReentrantLock锁的释放是显性的,有可能会忘记清除锁,使用起来更加“危险”
1.ReentrantLock实现和使用
1、使用:
public class TestDemo1 {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+":get lock");
}finally {
System.out.println(Thread.currentThread().getName()+":release lock");
lock.unlock();
}
}
}
2、源码
//无参构造
public ReentrantLock() {
sync = new NonfairSync();
}
//有参构造
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们可以看到ReentrantLock()有两个构造方法,其中:无参构造创建的是非公平锁
,有参构造fair如果为true,就创建的公平锁
1>我们写段代码来看下什么是公平锁和非公平锁:
public class TestDemo1 {
private static final Lock lock = new ReentrantLock();
// private static final Lock lock = new ReentrantLock(true);
public static void test(){
for (int i = 0;i<3;i++){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "get lock");
TimeUnit.MILLISECONDS.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
for (int i = 0;i<5;i++){
new Thread("线程"+i){
@Override
public void run() {
test();
}
}.start();
}
}
}
上述代码我们new 5个线程,分别执行test()方法,分别看看公平锁和非公平锁运行结果:
1.非公平锁
2.公平锁
结果:我们看到非公平锁:的运行结果是无顺序,也就是随机获取,谁运气好,谁就可以获得cpu的使用权,哪个线程就可以先执行。
公平锁:第一次获取锁的线程,下一次执行也会优先获取锁
2> 我们来写个代码例子看看,ReentrantLoc的可中断
public class TestDemo2 {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread("thread1"){
@Override
public void run() {
try {
lock1.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(100);
lock2.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock1.unlock();
lock2.unlock();
System.out.println(Thread.currentThread().getName()+"finish");
}
}
};
Thread thread2 = new Thread("thread2"){
@Override
public void run() {
try {
lock2.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(100);
lock1.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock1.unlock();
lock2.unlock();
System.out.println(Thread.currentThread().getName()+"finish");
}
}
};
thread1.start();
thread2.start();
//thread1.interrupt();
}
}
我们看到:线程1执行获得:lock1,然后睡眠,我们的线程2执行获得:lock2,然后睡眠,然后线程1又执行,想要获取lock2,但是lock2已经被线程2获取了,就发生死锁。
想要解决得话,要么我们直接中断当前程序,要么我们就执行thread1.interrupt();中断线程1,从而让线程2执行,不用中断程序。
2.ASQ
3.Condition
4.读写锁
4.1读写锁介绍
在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized(关于synchronized可以看这篇文章)或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
4.2.代码应用
/**
* 读写锁
* ReadWriteLock<->ReentrantReadWriteLock
* 读写可以共享读,但只能有一个写
* 总结:读读不互斥 读写,写写互斥
* 读远远大于写高并发情况下,一般会使用读写锁
*/
class ReadWriteLockDemo{
private int sum = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void setNum(int sum){
this.sum = sum;
}
public void read(){
lock.readLock().lock();
try {
for (int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"读取:"+sum);
}
}finally {
lock.readLock().unlock();
}
}
public void write(Integer newNum){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入:"+newNum);
setNum(newNum);
}finally {
lock.writeLock().unlock();
}
}
}
public class TestDemo4 {
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
for (int i = 0;i<5;i++){
new Thread("write"+i){
@Override
public void run() {
demo.write(new Random().nextInt(1001));
}
}.start();
}
for (int i = 0;i<5;i++){
new Thread("read"+i){
@Override
public void run() {
demo.read();
}
}.start();
}
}
}
5.阻塞队列
阻塞队列:针对一个有界队列,当前队列满时,添加操作会被堵塞;当前队列空时,从队列中获取元素操作会被阻塞
JDK 1.5 阻塞队列:
ArrayBlockingQueue
LinkedBlockingDeque
PriorityBlockingQueue
DelayQueue
SynchronousQueue
-
非阻塞队列的主要方法:
add(E e)/ offer(E e)
remove() / poll()
element() / peek() -
阻塞队列的主要方法:
put(E e):队尾添加元素,队列满,需要等待阻塞
take():去除队头元素,队列空,需要阻塞等待
offer(E e,long time,Timeunit t):对尾添加元素,如果队列满,等待一定时间,如果还没插入成功,就返回:false
poll(long time,Timeunit t):去除队头元素,如果队列空,等待一定时间,如果还没获取成功就返回:null ,否则返回获取到的元素