线程同步和线程状态

同步方法

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

格式

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

备注:同步锁是谁?

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

​ 对于static方法,我们使用当前方法所在类的字节码对象

同步方法代码示例如下:

public static /*synchronized void*/void saleTicket() {
		synchronized (RunnableImpl.class) {
			if (ticket > 0) {
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "-->" + ticket-- + "张票");
			}
		}

	}
 public synchronized void saleTicket() {
	 if(ticket>0) {
	 try {
	Thread.sleep(1);
	} catch (InterruptedException e) {
	// // TODO Auto-generated catch block
	 e.printStackTrace();
	 }
	 System.out.println(Thread.currentThread().getName()+"-->"+ticket--+"张票");
	 }
 }

Lock锁

java.util.concurrent.locks.lock 机制提供了比synchronized代码块和synchronized同步方法更加广泛的锁操作

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

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

public void lock();加同步锁

public void unlock();释放同步锁

package demo01;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//实现卖票案例
/*
 * 解决线程安全问题的第三种解决方案,使用Lock锁
 *    java.util.concurrent.locks.Lock接口
 *    Lock锁实现synchronized代码块和synchronized同步方法更加广泛的锁操作
 *    Lock锁接口中定义了两个锁操作:
 *    public void lock()上锁
 *    public void unlock()释放锁
 *    java.util.concurrent.locks.ReentrantLockimplements Lock 接口
 *    使用步骤:
 *         1.在成员的位置创建一个ReentrantLock对象
 *         2.在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁
 *         3.在可能会引发线程安全问题代码后调用Lock接口中的unlock释放锁
 */

public class RunnableImpl implements Runnable {
	// 定义一个多线程共享的资源 票
	private int ticket = 100;
	Lock lock = new ReentrantLock();

	// 设置线程的任务:卖票,此时窗口---->线程
	@Override
	public void run() {
		// 先判断票是否存在
		while (true) {
			//2.在可能会引发线程安全问题的代码前调用Lock接口中的lock方法获取锁
			lock.lock();
			if (ticket > 0) {
				// 票存在,卖出第ticket张票
				try {
					Thread.sleep(100);
					System.out.println(Thread.currentThread().getName() + "-->" + ticket-- + "张票");
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}finally {//无论程序出现异常,此时都会把锁释放掉
					//在finally语句块中一般用于资源的释放,关闭IO流,释放lock锁,关闭数据库链接等等
					//3.在可能会引发线程安全问题代码后调用Lock接口中的unlock释放锁
					lock.unlock();	
				}
			}
			
		}
	}

}
线程状态
线程状态概述

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

在JavaAPI帮助文档中java.lang.Thread.State这个枚举给出了线程的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方法而死亡。

六种状态切换描述:

线程6中状态ProMaxplues

Timed Waiting(计时等待)

timed Waiting在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.Waiti()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法,其实Waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

等待唤醒机制
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同。

比如说:线程A用来生产一个哇哈哈饮料,线程B用来消费哇哈哈饮料,哇哈哈饮料可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

通信

为什么要处理线程之间的通信

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

如何保证线程之间通信有效的利用资源:

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

等待唤醒机制

什么时等待唤醒机制呢?

这是多个线程间的一种协作机制。

就是一个线程进行了规定操作后,就进入到了等待状态(Wait())等待其他线程执行完他们的指定代码后,再将其唤醒(notify())

在有多个线程进行等待时,如果需要,可以使用notiflyAll()来唤醒所有的等待线程。

wait/notify就是线程间的一种协作机制。

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

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

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

notilfyAll();释放所通知对象的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必须要在同步代码块或者是同步方法中使用。因为,必须通过锁对象调用这两个方法实现等待与唤醒。

生产者与消费者问题

等待唤醒机制经典的案例就是生产者与消费者的问题。

举一个例子:生产包子与消费包子来描述等待唤醒机制如何有效的利用资源:

包子铺线程生产包子,吃货线程消费包子。当没有包子的时候(包子的状态为false)吃货线程需要等待,包子铺线程生产包子(包子的状态为true),并通知吃货线程(借出吃货等待的状态)因为已经有了包子,那么包子铺线程就需要进入到等待状态
    接下来,吃货线程能够进一步执行则取决于锁的获取情况,如果吃货线程获取到锁,那么就执行吃包子的动作,包子吃完了(包子的状态为false),需要通知包子铺线程(解除包子铺等待状态)此时吃货线程进入到等待状态,包子铺能否进一步执行则取决于锁的获取情况。
线程池
线程池的概念

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

由于线程池中有很多操作都是与优化系统资源有关的,我们今天先来介绍一下线程池的工作原理

线程池图解

线程池工作原理

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

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

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

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

线程池的使用

Java里面线程池的顶级接口时java.util.concurrent.Executor,但是严格意义讲,Executor它并不是一个线程池,它只是执行线程的一个工具,真正的线程池接口是java.util.concurrent.ExecutorService。

因此在java.util.concurrent.Executors线程工厂类提供了一些静态工厂,生产一些常用的线程池。官方建议使用Executors来创建线程池对象。

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

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

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

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

Future接口:用来记录线程任务执行完毕后产生的结果。线程的创建与使用。

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

  1. 创建线程池对象

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

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

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

    Lambda表达式
    函数式编程思想概述

    y = x+1,在数学中,函数就是有输入量,输出量的一套计算方案;也就是"拿什么东西做什么事情"相对而言,面向对象过程过分强调“必须通过对象的形式来做事情”而函数式编程思想则尽量忽略面向对象的复杂语法---->强调做什么,而不是以什么方式来做。

    面向对象的思想:

    做一件事情,找一个能解决这个事情的对象,调用对象的方法来完成事情。

    函数式编程的思想:

    只要能获得这个事情的结果,谁去做的,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。

    冗余的Runnable代码

    当需要手动一个线程去完成一项任务时,通常会通过Runnable接口来定义任务内容,并且使用Thread类来启动线程。

原文地址:https://www.cnblogs.com/lulubenlei/p/14122921.html