Java多线程详解-锁

483 阅读10分钟

前言

最近工作有点忙,周末也一直在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:notifywait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

3:notifynotifyAll的区别

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的年轻人,主要用来分享技术收获。