Java线程锁,等待唤醒和线程池

线程的同步

当我们使用多线程访问同一资源的时候,且这多个线程中对资源有的写的操作,就容器出现线程安全问题。

要解决多线程并发访问一个资源的安全问题,java中提供了同步机制(synchronized)来解决。

有三种方式实现同步机制:

  1. 同步代码块格式:

synchronized(同步锁) {
    // 需要同步操作的代码。
}

同步锁

同步锁是一个对象,是一个抽象的概念,可以想象成在对象上标记了一个锁。

  1. 锁对象可以是任意类型的。Object

  2.多个线程对象,要使用同一把锁。

在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就拥有资格进入代码块中,其他线程只能在外面等待着。(Blocked阻塞状态)

同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队。

public synchronized void method() {
    // 可能会产生线程安全问题的代码
}

  对于非static方法,同步锁就是this

  对于static方法,我们使用当前方法所在类的字节码对象(类名.class)

        @Override
    public void run() {
        System.out.println(RunnableImpl.class);
        // com.zhiyou100.thread.demo01.RunnableImpl@15db9742
        System.out.println("this ---->" + this);
        // 先判断票是否存在
        System.out.println();
        while(true){
            saleTicket();
        }
    }
    /*
     * 静态的同步方法
     * 锁对象
     * 不能是this
     * this是创建对象之后产生的,静态方法优先于对象的创建
     * 静态同步方法中的锁对象是本类的class属性--->class文件对象(反射)
     */
    public  static synchronized void saleTicket() {
        
        /*synchronized (RunnableImpl.class) {*/
            if (ticket > 0) {
                // 提高卖票的体验感 ,让程序睡眠下
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 票存在,卖出第ticket张票
                System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
                ticket--;
            }
        /*}*/
    }    

Lock锁

同步代码块/同步方法具有的功能,Lock都有,除此之外更强大,更能体现出面向对象特征。

Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:

- public void lock():加同步锁
- public void unlock():释放同步锁。

备注:锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。

public class RunnableImpl implements Runnable {
    // 定义一个多线程共享的资源 票
    private int ticket = 100;    
    // 1. 在成员的位置创建一个ReentrankLock对象
    Lock Lock = new ReentrantLock();
    // 设置线程的任务:卖票  此时窗口--->线程
    @Override
    public void run() {
        // 先判断票是否存在
        while(true){
            // 2. 在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁
            Lock.lock();
            if (ticket > 0) {
                // 提高卖票的体验感 ,让程序睡眠下
                try {
                    Thread.sleep(10);
                    // 票存在,卖出第ticket张票
                    System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 无论程序出现异常,此时都会把锁释放掉
                    // 在finally语句块中一般用于资源的释放,关闭IO流,释放lock锁,关闭数据库连接等等
                    // 3.在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
                    Lock.unlock();
                }
            }
        }
    }
}

线程状态

当线程被创建并启动之后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态

线程状态导致状态发生条件
NEW(新建) 线程刚被创建,但是还没有启动,还没有调用start方法
RUNNABLE(可运行) 线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器
BLOCKED(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程就进入到Runnable状态
WAITING(无限等待) 一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
TIMED_WAITING(计时等待) 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知。带有超时参数的常用方法有Thread.sleep(),Object.wait().
TERMINATED(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

Timed Waitng在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态

其实当调用了sleep方法之后,当前正在执行的线程就进入到了计时等待状态。

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1;i <= 100 ; i ++) {
            if (i % 10 == 0) {
                System.out.println("------------------>" + i);
            }
            System.out.println(i);
            // 在每个数字之间暂停1秒
            try{
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }       
    } 
    // 准备一个main函数
    public static void main(String[] args) {
        new MyThread().start();
    }
}

  1.进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系

  2.为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠

  3.sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行。

Blocked锁阻塞状态

Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。 比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态。

Waiting无限等待状态

Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法

其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

等待唤醒

线程间通信

多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据。

多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。

等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法有三个如下:

  • wait():线程不再活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是唤醒通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中。

  • notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等候就餐最久的顾客最先入座。

  • notifyAll():释放所通知对象的wait set中的全部线程。

  • 哪怕只通知了一个等待线程,被通知的线程也不能立即回复执行,因为它当初中断的地方是在同步块内,而此刻他已经不持有锁了,所以他需要再次尝试着去获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。

如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态

否则,从wait set中出来,又进入set中,线程就从WAITING状态转变成BLOCKED状态。

调用wait和notify方法的注意细节:

  1. wait方法与notify方法必须由同一个锁对象调用。因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

  2. wait方法与notify方法是属于Object类的方法的。因为,锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

  3. wait方法与notify方法必须要在同步代码块或者同步方法中使用。因为,必须通过锁对象调用这两个方法来实现等待与唤醒。

线程池

一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程对象的操作,无需反复创建线程而消耗过多的系统资源。

 

合理利用线程池能够带来什么样的好处:

 

  • 降低资源消耗。减少了线程的创建与销毁的次数,每个工作线程都可以被重复利用,可执行多个任务。

  • 提高了响应速度。当任务到达时,任务可以不需要等到线程的创建就能立即执行。

  • 提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,死机的风险也就更高)。

Executors有创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回的就是线程池对象。(创建的是有界的线程池,也就是池中的线程个数可以指定最大数量)。

获取到了一个线程池ExecutorService对象,在该类中定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。

使用线程池中线程对象的步骤:

  1. 创建线程池对象

  2. 创建Runnable接口子类对象。(task)

  3. 提交Runnable接口子类对象。 (take task)

  4. 关闭线程池(一般不做)。

by-25

 

posted @ 2020-12-17 22:29  大萝卜萌萌哒  阅读(375)  评论(0编辑  收藏  举报