前言
最近工作有点忙,周末也一直在996加班,所以没有时间来写博客,今早忙里偷闲来写一下多线程的第二篇。
入门篇地址:Java多线程详解-入门篇
废话不多说,开始这次的主题。
复习
上一张我们学习了Java多线程的基础知识。回顾一下:
1:实现多线程的两种方式,继承Thread类以及实现Runable接口,然后重写里面的run方法即可。
2:线程的状态:
新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
其中最重要的地方,也是难点就是线程的阻塞状态,线程在什么情况下会进入阻塞状态,如何解除阻塞状态。
3:线程的调度:
线程的优先级,线程睡眠,线程等待,线程让步,线程加入,线程唤醒。
这些都有相应的方法我再上一篇也都提到过。
4:线程中的常用函数:
sleep(long millis),join(),yield(),setPriority(),interrupt(),wait(),notify(),notifyAll()。
前面都讲过了,后面的interrupt(),wait(),notify(),notifyAll()这几个方法会结合今天的锁和中断来讲一下如何使用。
线程锁
首先来说说为什么要加锁。这里就要提到了共享资源这个话题,比如说:一个卖票系统,售票员有三个,总共有100张票,那么共享资源就是这100张票,有三个线程在一起消费它,当这100张票卖完时,三个线程就都得停止了。
我们都知道,线程在CPU中的机制时竞争机制,也就是说谁抢到了下一张票,谁就卖下一张票。但是要注意的是,当线程一抢到要卖的下一张票,准备抢占CPU资源运行时,线程二抢到了下一张票,并且抢到了CPU的资源,那么就会提前将下一张票卖出。这样就会导致票的顺序呗打乱。
可能我文字描述不太清楚,上一段代码看看。
class Count {
public static int count = 100;
}
@Data
class TicketThread implements Runnable {
private String name;
public TicketThread(String name){
super();
this.name = name;
}
@Override
public void run() {
int all = 1;
while (Count.count >0){
System.out.println(this.getName()+":::卖出了"+(all++)+"张,还剩"+ (--(Count.count))+"张");
}
}
}
class main {
public static void main(String[] args){
new Thread(new TicketThread("售票员1")).start();
new Thread(new TicketThread("售票员2")).start();
new Thread(new TicketThread("售票员3")).start();
}
}
上面这段代码大意就是有100张票(count=100),共有三个售票员(三个多线程)来同时卖票。我们来看下结果。
售票员2:::卖出了23张,还剩8张
售票员2:::卖出了24张,还剩7张
售票员2:::卖出了25张,还剩6张
售票员2:::卖出了26张,还剩5张
售票员2:::卖出了27张,还剩4张
售票员2:::卖出了28张,还剩3张
售票员3:::卖出了24张,还剩14张
售票员1:::卖出了45张,还剩33张
售票员1:::卖出了46张,还剩0张
售票员3:::卖出了25张,还剩1张
售票员2:::卖出了29张,还剩2张
我只是截取了100张最后的部分,可以看到整个的结果是比较乱的,那么我们来找到售票员1--卖出了46张,还剩0张这一条来分析,就是说售票员1总共卖出了46张票,当他卖出第46张时,已经没有票了,而程序中售票员2,3还卖出了两张。这就出现了问题了。
这里的原因我上面也提到过,总共100张票,1,2,3各自抢到了一张票,然后他们三进行CPU资源的竞争,说竞争到了谁运行,那么就会出现当前的问题。
这样的问题如何解决呢?
顺应的锁的概念就应该提出来了,给这个买票过程加锁,当一个线程进去取到一张票并卖出的时候,加上锁,这样另外两个线程就只能在外面等待该线程把这张票卖出之后,三个人再进行资源的竞争。这样的情况下,就不会出现上面的情况了。
如何加锁
有三种方法:
1:synchronized代码块
也叫同步代码块
@Override
public void run() {
int all = 1;
while (Count.count >0){
synchronized (""){
if (Count.count<=0){
return;
}
System.out.println(this.getName()+":::卖出了"+(all++)+"张,还剩"+ (--(Count.count))+"张");
}
}
}
2:同步方法
我们直接写一个方法,给count(票)加锁。
如果一个类里面所有东西都是线程需要的,可以将synchronized加在方法上修饰。
private synchronized static void count() {
int all = 1;
if (Count.count <= 0) {
return;
}
System.out.println(new TicketThread().getName()+":::卖出了"+(all++)+"张,还剩"+ (--(Count.count))+"张");
}
然后再run方法中引用该方法
@Override
public void run() {
int all = 1;
while (Count.count >0){
count();
}
}
3:Lock接口的使用
class Thread1 {
public static void main(String[] args) {
//实例化上锁对象 ReentrantLock
Lock lock = new ReentrantLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
while (Count.count>0){
//对临界资源进行上锁
lock.lock();
if (Count.count<=0){
return;
}
System.out.println("数量减少1,总数count"+ --(Count.count));
//解锁
lock.unlock();
}
}
};
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
Thread t3 = new Thread(runnable,"t3");
Thread t4 = new Thread(runnable,"t4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
直接给临界资源上锁。
这三种方法都可以加锁,加锁之后,再次运行程序。
售票员3:::卖出了48张,还剩6张
售票员3:::卖出了49张,还剩5张
售票员3:::卖出了50张,还剩4张
售票员3:::卖出了51张,还剩3张
售票员1:::卖出了46张,还剩2张
售票员1:::卖出了47张,还剩1张
售票员1:::卖出了48张,还剩0张
这样就算是正常情况了。
所以说,在多线程情况下,如果一个变量会被几个线程同时用到,那么要给该变量加锁,让一个线程进去使用完之后,在进行CPU资源竞争,这样能保证程序不出现一些看不懂的异常。
死锁
多线程中有一种情况叫做死锁,就是A线程拿到A和B线程的锁,B线程也拿到A和B线程的锁,他们在同时等对方释放对方手中拿到的自己需要的锁,这样就造成死锁现象了。看代码
class DeadLock {
public static void main(String[] args) {
Runnable runnable1 = () -> {
synchronized ("A"){
System.out.println("A线程持有A锁,等待B锁");
synchronized ("B"){
System.out.println("A线程同时持有A,B锁");
}
}
};
Runnable runnable2 = () -> {
synchronized ("B"){
System.out.println("B线程持有B锁,等待A锁");
synchronized ("A"){
System.out.println("B线程同时持有A,B锁");
}
}
};
Thread t1 = new Thread(runnable1);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();
}
}
运行main方法,会出现下面情况
B线程持有B锁,等待A锁
A线程持有A锁,等待B锁
只输出了这两句话,然后程序还在运行中,这就是上面所说到的死锁现象,A,B互相持有对方的锁,都在等待对方释放自己所需要的锁,进行下面的操作。
那么如何避免死锁现象呢?
这里就要引出我们一直准备说的三个方法了:
wait(),notify(),notifyAll()
1:wait()
wait()方法是使当前线程阻塞,前提是必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用wait()、notify/notifyAll() 方法。
当线程使用wait()方法时,该线程进入阻塞状态,会释放出当前的锁,然后让出CPU,进入等待状态。
2:notify(),notifyAll()
而notify,notifyAll两个方法的作用是唤醒一个或者多个正处于等待状态的线程,让他们继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区,以唤醒其他线程让其获得锁。
需要注意的是:
1:wait()需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
2:notify 和wait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
3:notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
4:在多线程中要测试某个条件的变化,使用if 还是while?
要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑;显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while进行等待,直到满足条件才继续往下执行。
我们修改一下上面的代码
public static void main(String[] args) {
Runnable runnable1 = () -> {
synchronized ("A"){
System.out.println("A线程持有A锁,等待B锁");
try {
//释放A锁,进入等待队列
"A".wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized ("B"){
System.out.println("A线程同时持有A,B锁");
}
}
};
Runnable runnable2 = () -> {
synchronized ("B"){
System.out.println("B线程持有B锁,等待A锁");
synchronized ("A"){
System.out.println("B线程同时持有A,B锁");
//唤醒持有A锁的线程
"A".notifyAll();
}
}
};
Thread t1 = new Thread(runnable1);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();
}
运行一下
A线程持有A锁,等待B锁
B线程持有B锁,等待A锁
B线程同时持有A,B锁
A线程同时持有A,B锁
main函数直接输出完后结束掉,没有再像前面一样的死锁现象发生。
这里的具体操作是:当A线程持有了A锁时,遇到wait()方法,A线程进入阻塞状态,释放了A锁,让出了CPU资源,这是B线程就可以通顺的获得B锁,再获得A锁,然后在B线程执行完成任务后,用notifyAll方法唤醒所有等待A锁的线程,让他们进行CPU竞争。当然我写的代码只有一个A线程在等待A锁。这样A线程就会接着自己前面的程序继续执行下去,完成任务。
由于篇幅过长,中断那就放在下一次再写吧,中断写完还会写写线程池。
这些也是我自己再学习过程中的一些总结。有什么错误的地方希望大家看到了指出来。互勉~
欢迎关注我的微信公众号,一个喜欢代码和NBA的年轻人,主要用来分享技术收获。